change
This commit is contained in:
53
src/shared/api/apiClient.ts
Normal file
53
src/shared/api/apiClient.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { API_URL } from "@/shared/constants/apiEndpoints";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import { getCurrentLocale } from "@/shared/lib/getCurrentLocale";
|
||||
import { useAuthStore } from "@/shared/store/authStore";
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: API_URL || "https://api.quyoshli.uz/api",
|
||||
});
|
||||
|
||||
apiClient.interceptors.request.use(async (config) => {
|
||||
console.log("API request", config);
|
||||
const token = useAuthStore.getState().user?.access_token;
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
let language = "uz";
|
||||
try {
|
||||
language = await getLocale();
|
||||
} catch (e) {
|
||||
language = getCurrentLocale() || "uz";
|
||||
}
|
||||
|
||||
config.headers["Accept-Language"] = language;
|
||||
return config;
|
||||
});
|
||||
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
console.error("API error:", error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export const GET = <T>(
|
||||
url: string,
|
||||
params?: object
|
||||
): Promise<AxiosResponse<T>> => apiClient.get(url, { params });
|
||||
export const POST = <T>(url: string, data: object): Promise<AxiosResponse<T>> =>
|
||||
apiClient.post(url, data);
|
||||
export const PUT = <T>(url: string, data: object): Promise<AxiosResponse<T>> =>
|
||||
apiClient.put(url, data);
|
||||
export const PATCH = <T>(
|
||||
url: string,
|
||||
data: object
|
||||
): Promise<AxiosResponse<T>> => apiClient.patch(url, data);
|
||||
export const DELETE = <T>(
|
||||
url: string,
|
||||
data: object
|
||||
): Promise<AxiosResponse<T>> => apiClient.delete(url, data);
|
||||
|
||||
export default apiClient;
|
||||
14
src/shared/api/authSvc.ts
Normal file
14
src/shared/api/authSvc.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {POST} from "@/shared/api/apiClient";
|
||||
import {useAuthStore} from "@/shared/store/authStore";
|
||||
import {OAUTH, OAUTH_VERIFY} from "@/shared/constants";
|
||||
|
||||
export const sendPhoneNumber = async (phone: string) => {
|
||||
await POST(OAUTH, { phone });
|
||||
};
|
||||
|
||||
export const verifyCode = async (phone: string, verify_code: number): Promise<any> => {
|
||||
const response = await POST(OAUTH_VERIFY, { phone, verify_code });
|
||||
const {data} = response?.data;
|
||||
useAuthStore.getState().login(data);
|
||||
return data;
|
||||
};
|
||||
13
src/shared/api/brandsSvc.ts
Normal file
13
src/shared/api/brandsSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {BRANDS, PRODUCTS} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import { BrandProductsType, Brands } from "../types/brands";
|
||||
|
||||
export const getBrands = async (): Promise<Brands> => {
|
||||
const res = await GET<Brands>(BRANDS);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export const getBrandProducts = async (brandId: number): Promise<BrandProductsType> => {
|
||||
const res = await GET<BrandProductsType>(`${brandId}${PRODUCTS}`);
|
||||
return res.data;
|
||||
}
|
||||
8
src/shared/api/compilationsSvc.ts
Normal file
8
src/shared/api/compilationsSvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {COMPILATIONS} from "@/shared/constants/apiEndpoints";
|
||||
import {GetCompilationResponse} from "@/shared/types/compilations";
|
||||
|
||||
export const getCompilation = async () => {
|
||||
const res = await GET<GetCompilationResponse>(COMPILATIONS)
|
||||
return res.data;
|
||||
}
|
||||
11
src/shared/api/contactSvs.ts
Normal file
11
src/shared/api/contactSvs.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AxiosResponse } from "axios";
|
||||
import { SUPPORT } from "../constants";
|
||||
import { POST } from "./apiClient";
|
||||
|
||||
export const contactInfoSubmit = async (
|
||||
phone: string,
|
||||
name: string
|
||||
): Promise<AxiosResponse> => {
|
||||
const response = await POST(SUPPORT, { phone, name });
|
||||
return response;
|
||||
};
|
||||
12
src/shared/api/feedbackSvc.ts
Normal file
12
src/shared/api/feedbackSvc.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {FEEDBACK} from "@/shared/constants";
|
||||
|
||||
export const getFeedback = async ()=>{
|
||||
const {data}= await GET<{
|
||||
data: {
|
||||
"phone": string
|
||||
"telegram_support": string
|
||||
}
|
||||
}>(FEEDBACK)
|
||||
return data
|
||||
}
|
||||
5
src/shared/api/index.ts
Normal file
5
src/shared/api/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import apiClient from "@/shared/api/apiClient";
|
||||
export * from "./authSvc"
|
||||
export {
|
||||
apiClient
|
||||
}
|
||||
13
src/shared/api/partnersSvc.ts
Normal file
13
src/shared/api/partnersSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {PARTNERS} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {GetPartnersResponse} from "@/shared/types/partners";
|
||||
|
||||
export const getPartners = async (): Promise<GetPartnersResponse> => {
|
||||
const res = await GET<GetPartnersResponse>(PARTNERS);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export const getPartnerById = async (id: number): Promise<GetPartnersResponse> => {
|
||||
const res = await GET<GetPartnersResponse>(`${PARTNERS}/${id}`);
|
||||
return res.data;
|
||||
}
|
||||
10
src/shared/api/policySvc.ts
Normal file
10
src/shared/api/policySvc.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {PAGE_POLICY} from "@/shared/constants";
|
||||
|
||||
export const getPolicy = async ()=>{
|
||||
const {data} = await GET<{data: {
|
||||
name: string
|
||||
body: string
|
||||
}}>(PAGE_POLICY)
|
||||
return data
|
||||
}
|
||||
8
src/shared/api/productCategorySvc.ts
Normal file
8
src/shared/api/productCategorySvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {ProductCategory} from "../types/productCategory"
|
||||
import {CATEGORIES} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
|
||||
export const getCategory = async (): Promise<{data:ProductCategory[]}>=>{
|
||||
const res = await GET<{data:ProductCategory[]}>(CATEGORIES);
|
||||
return res.data;
|
||||
}
|
||||
23
src/shared/api/productSvc.ts
Normal file
23
src/shared/api/productSvc.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {CATEGORIES, PRODUCTS} from "@/shared/constants/apiEndpoints";
|
||||
import {GetProductsResponse} from "@/shared/types/product";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import { BrandProductsType } from "../types/brands";
|
||||
|
||||
interface GetProductsProps {
|
||||
categoryId: number
|
||||
currentPage?: number
|
||||
}
|
||||
|
||||
export const getProducts = async ({categoryId, currentPage}: GetProductsProps): Promise<{
|
||||
data: GetProductsResponse
|
||||
}> => {
|
||||
const res = await GET<GetProductsResponse>(`${CATEGORIES}/${categoryId}${PRODUCTS}`, {
|
||||
...(currentPage !== undefined && { page: currentPage })
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
export const getProductById = async (productId: number): Promise<any> => {
|
||||
const res = await GET(`${PRODUCTS}/${productId}`);
|
||||
return res;
|
||||
}
|
||||
8
src/shared/api/regionSvc.ts
Normal file
8
src/shared/api/regionSvc.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {REGIONS} from "@/shared/constants";
|
||||
import {GetRegionsResponse} from "@/shared/types/region";
|
||||
|
||||
export const getRegions = async ()=>{
|
||||
const {data} = await GET<GetRegionsResponse>(REGIONS)
|
||||
return data
|
||||
}
|
||||
13
src/shared/api/servicesSvc.ts
Normal file
13
src/shared/api/servicesSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {SERVICES} from "@/shared/constants/apiEndpoints";
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {GetServiceByIdResponse, GetServicesResponse} from "@/shared/types/services";
|
||||
|
||||
export const getServices = async (): Promise<GetServicesResponse> => {
|
||||
const {data} = await GET<GetServicesResponse>(`${SERVICES}`);
|
||||
return data
|
||||
}
|
||||
|
||||
export const getServiceById = async (id: number): Promise<GetServiceByIdResponse> => {
|
||||
const {data} = await GET<GetServiceByIdResponse>(`${SERVICES}/${id}`);
|
||||
return data
|
||||
}
|
||||
18
src/shared/api/usefulSvc.ts
Normal file
18
src/shared/api/usefulSvc.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {USEFUL_INFORMATION} from "@/shared/constants/apiEndpoints";
|
||||
import {GetUsefulResponse} from "@/shared/types/useful";
|
||||
|
||||
export const getUseful = async (): Promise<GetUsefulResponse> => {
|
||||
const {data} = await GET<GetUsefulResponse>(USEFUL_INFORMATION)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUsefulById = async (id: number): Promise<any> => {
|
||||
const {data} = await GET(`${USEFUL_INFORMATION}/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUsefulItems = async (useful_id: number, items_id:number): Promise<any> => {
|
||||
const {data} = await GET(`${USEFUL_INFORMATION}/${useful_id}/items/${items_id}`)
|
||||
return data
|
||||
}
|
||||
17
src/shared/api/userMeSvc.ts
Normal file
17
src/shared/api/userMeSvc.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import {GET, POST, PUT} from "@/shared/api/apiClient";
|
||||
import {USER_ME} from "@/shared/constants";
|
||||
import {GetUserMeResponse} from "@/shared/types/user";
|
||||
|
||||
export const getUserMe = async () => {
|
||||
const {data} = await GET<GetUserMeResponse>(USER_ME)
|
||||
return data
|
||||
}
|
||||
export const updateUserMe = async (userData: {
|
||||
first_name?: string
|
||||
last_name?: string
|
||||
middle_name?: string
|
||||
phone?: string
|
||||
}) => {
|
||||
const {data} = await PUT(USER_ME, userData)
|
||||
return data
|
||||
}
|
||||
18
src/shared/api/userOrdersSvc.ts
Normal file
18
src/shared/api/userOrdersSvc.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import {GET, POST} from "@/shared/api/apiClient";
|
||||
import {CHECKOUT, USER_ORDERS} from "@/shared/constants";
|
||||
import {GetUserOrderByIdResponse, GetUserOrdersResponse} from "@/shared/types/userOrders";
|
||||
|
||||
export const getUserOrders = async ():Promise<GetUserOrdersResponse>=>{
|
||||
const {data} = await GET<GetUserOrdersResponse>(USER_ORDERS)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUserOrdersById = async (id: number):Promise<GetUserOrderByIdResponse>=>{
|
||||
const {data} = await GET<GetUserOrderByIdResponse>(`${USER_ORDERS}/${id}`)
|
||||
return data
|
||||
}
|
||||
|
||||
export const createUserOrder = async (data: any)=>{
|
||||
const res = await POST(CHECKOUT, data)
|
||||
return res
|
||||
}
|
||||
13
src/shared/api/userRequestsSvc.ts
Normal file
13
src/shared/api/userRequestsSvc.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import {GET} from "@/shared/api/apiClient";
|
||||
import {USER_ORDERS, USER_REQUESTS} from "@/shared/constants";
|
||||
import {GetUserRequestByIdResponse, GetUserRequestsResponse} from "@/shared/types/userRequests";
|
||||
|
||||
export const getUserRequests = async ():Promise<GetUserRequestsResponse>=>{
|
||||
const {data} = await GET<GetUserRequestsResponse>(USER_REQUESTS)
|
||||
return data
|
||||
}
|
||||
|
||||
export const getUserRequestsById = async (id: number):Promise<GetUserRequestByIdResponse>=>{
|
||||
const {data} = await GET<GetUserRequestByIdResponse>(`${USER_REQUESTS}/${id}`)
|
||||
return data
|
||||
}
|
||||
9
src/shared/config/fonts.ts
Normal file
9
src/shared/config/fonts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {Golos_Text} from "next/font/google";
|
||||
|
||||
const golosText = Golos_Text({
|
||||
weight: ["400", "500", "600", "700", "800"],
|
||||
variable: "--font-golos-text",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export { golosText };
|
||||
130
src/shared/config/i18n/messages/ru.json
Normal file
130
src/shared/config/i18n/messages/ru.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"Sozlamalar": "Настройки",
|
||||
"Ma'lumotlarni yangilash": "Обновление данных",
|
||||
"Sahifa topilmadi, Iltimos qayta urinib ko`ring": "Страница не найдена, попробуйте еще раз",
|
||||
"Orga qaytish": "Возвращаться",
|
||||
"Login": "Авторизоваться",
|
||||
"Hisobingizga kirish uchun telefon raqamingizni kiriting": "Введите свой номер телефона, чтобы получить доступ к вашей учетной записи",
|
||||
"Telefon": "Телефон",
|
||||
"terms_of_use": "Я ознакомился с <tag>{tag}</tag>",
|
||||
"Tasdiqlash kodini kiriting": "Введите код подтверждения",
|
||||
"Tasdiqlash": "Подтвердить",
|
||||
"Offer va shartlar": "Предложение и условия",
|
||||
"Offerta shartlari": "Офферта термины",
|
||||
"Bu yerda offerta shartlarini o'qib chiqishingiz mumkin": "Вы можете прочитать здесь термины Offerta.",
|
||||
"Telefon raqam": "Номер телефона",
|
||||
"Email": "Электронная почта",
|
||||
"Adress": "Адрес",
|
||||
"Ish vaqtlari": "Рабочее время",
|
||||
"Biz haqimizda": "О нас",
|
||||
"about_us_subtitle": "Наш интернет-магазин предлагает полный ассортимент оборудования и своевременный доступ ко всем новым моделям. Здесь вы найдете все необходимое для себя и своего дома, принимая во внимание последние обновления и скидки. Таким образом, вы всегда получите самый новый продукт по самым оптимальным ценам.",
|
||||
"about_us_desc": "Наши каталоги постоянно пополняются новыми брендами и их продуктами, и вы всегда будете знать о сетевых поджелудочных зачинках, солнечных батареях и недавних разработках во всех спектрах услуг, связанных с энергией.",
|
||||
"Kategoriyalar": "Категории",
|
||||
"Kontaktlar": "Контакты",
|
||||
"Ilovamizni yuklab oling": "Загрузите наше приложение",
|
||||
"download_our_app_desc": "Наш интернет - магазин предоставляет вам полный ассортимент продукции для солнечного оборудования и уникальный доступ к соответствующей модели. Здесь вы найдете все необходимое для себя и своего дома, принимая во внимание последние обновления и скидки. Таким образом, вы всегда получите самый новый продукт по самым оптимальным ценам.",
|
||||
"Bepul maslahat uchun ro'yxatdan o'ting": "Зарегистрируйтесь на бесплатный совет",
|
||||
"Ro'yxatdan o'tish": "Зарегистрироваться",
|
||||
"Ismingiz": "Ваше имя",
|
||||
"Telefon raqamingiz": "Ваш номер телефона",
|
||||
"Quyosh uskunalarini ulgurji narxlarda sotib oling!": "Купите солнечное оборудование по оптовым ценам!",
|
||||
"Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling": "Купите всё для солнечной электростанции у нас – по лучшим ценам.",
|
||||
"Batafsil": "Подробно",
|
||||
"Quyosh panellari": "Солнечные панели",
|
||||
"Hammasini ko'rish": "Просмотреть все",
|
||||
"Hamkorlarimiz": "Наши партнеры",
|
||||
"profit_1_desc": "Возьмите прибыль от энергии, производимой каждый день солнечными батареями. Вы можете подключиться к «зелёному» тарифу и продавать излишки солнечной энергии в сеть. Обычно оборудовано солнечными батареями, сетевым инвертором, установкой для солнечных батарей, защитой и переключением оборудования.",
|
||||
"profit_2_desc": "Всегда будет свет - когда вы являетесь владельцем автономной или резервной солнечной электростанции. Даже если отключена внешняя сеть, вы продолжаете обеспечивать электроэнергией свой дом. Такие станции при необходимости можно дополнительно оснастить аккумуляторами.",
|
||||
"profit_3_desc": "Вы можете купить все необходимые компоненты для солнечной электростанции в нашем интернет -магазине. Мы устанавливаем оборудование, которое предлагаем. Мы продаём только такое оборудование, которое приносит пользователю реальную экономию.",
|
||||
"Katalog": "Каталог",
|
||||
"Xizmatlar": "Услуги",
|
||||
"Hamkorlik": "Сотрудничество",
|
||||
"Foydali": "Полезный",
|
||||
"Viloyat": "Область",
|
||||
"Tuman": "Район",
|
||||
"Ariza yuborish": "Отправить заявление",
|
||||
"Sotib olish": "Покупка",
|
||||
"Jismoniy shaxs": "Индивидуальный",
|
||||
"Yuridik shaxs": "Юридическое лицо",
|
||||
"Buyurtma muvaffaqiyatli yaratildi!": "Порядок был успешно создан!",
|
||||
"Siz bilan tez orada bog'lanamiz": "Мы скоро свяжемся с вами.",
|
||||
"Buyurtma yaratishda xatolik!": "Ошибка в создании порядка!",
|
||||
"Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring": "Убедитесь, что данные введены и попробуйте еще раз.",
|
||||
"Kompaniya nomi": "Название компании",
|
||||
"Direktor": "Директор Ф.И.О.",
|
||||
"Yuridik manzil": "Юридический адрес",
|
||||
"Bank nomi": "Банк название",
|
||||
"Hisob raqam": "Номер счета",
|
||||
"Yetkazib berish": "Доставка",
|
||||
"Viloyatni tanlang": "Выберите регион",
|
||||
"Tuman/shahar": "Район / Город",
|
||||
"Manzil": "Адрес",
|
||||
"Uy raqami": "Домашний номер",
|
||||
"Mo'ljal": "Цель",
|
||||
"O‘rnatish xizmati kerakmi?": "Нужна служба установки?",
|
||||
"Ha": "Да",
|
||||
"Yo‘q": "Нет",
|
||||
"Yetkazib berish kerakmi": "Вам нужно доставить",
|
||||
"Yo‘q o'zim olib ketaman": "Нет, я возьму себя",
|
||||
"Yuborish": "Отправка",
|
||||
"full_name": "Ф.И.О.",
|
||||
"Passport seriya va raqami": "Серия паспорта и номер",
|
||||
"Yetkazib berish kerakmi?": "Стоит ли вам доставить?",
|
||||
"so'm": "Сум",
|
||||
"QQS bilan": "С НДС",
|
||||
"chegirma": "скидка",
|
||||
"Boshqa mahsulotlar": "Другие продукты",
|
||||
"Mening arizalarim": "Мои приложения",
|
||||
"Sizning arizalaringiz va ularning holati haqida ma'lumotlar": "Информация о ваших приложениях и их статусе",
|
||||
"Ariza": "Приложение",
|
||||
"Yaratilish vaqti": "Время создать",
|
||||
"Ariza tafsilotlari": "Детали приложения",
|
||||
"Ariza turi": "Тип приложения",
|
||||
"Ariza raqami": "Номер приложения",
|
||||
"Ariza holati": "Статус приложения",
|
||||
"Xizmat tafsilotlari": "Служба детали",
|
||||
"Quvvat": "Власть",
|
||||
"Telefon raqami": "Номер телефона",
|
||||
"Izoh": "Комментарий",
|
||||
"Bog'lanish": "Связаться с нами",
|
||||
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin": "Вы можете использовать следующие способы связаться с нами",
|
||||
"Call Center": "Колл-центр",
|
||||
"Xatolik yuz berdi": "Произошла ошибка",
|
||||
"Profil ma'lumotlari": "Информация о профиле",
|
||||
"Sizning profil ma'lumotlaringiz va ularni o'zgartirish": "Информация о вашем профиле и их изменение",
|
||||
"Familiyangiz": "Ваша фамилия",
|
||||
"Sharif": "Шариф",
|
||||
"Saqlanmoqda": "Сохранение...",
|
||||
"Saqlash": "Хранилище",
|
||||
"Mening buyurtmalarim": "Мои заказы",
|
||||
"Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar": "Информация о ваших заказах и их статусе",
|
||||
"Buyurtma": "Заказ",
|
||||
"Buyurtma tafsilotlari": "Подробная информация о заказе",
|
||||
"Buyurtma raqami": "Номер заказа",
|
||||
"Buyurtma holati": "Статус заказа",
|
||||
"To'lov holati": "Статус оплаты",
|
||||
"Xaridlar ro'yxati": "Платный список",
|
||||
"Mijoz ma'lumotlari": "Информация о клиенте",
|
||||
"Mijoz turi": "Тип клиента",
|
||||
"Mijoz telefon": "Номер телефона клиента",
|
||||
"Yetkazib berish turi": "Тип доставки",
|
||||
"Yetkazib berish manzili": "Адрес доставки",
|
||||
"Yetkazib berish narxi": "Стоимость доставки",
|
||||
"To'lash": "Платить",
|
||||
"Foydali ma'lumotlar ro'yxati": "Полезный список информации",
|
||||
"Download PDF": "Загрузите файл",
|
||||
"Yopish": "Закрытие",
|
||||
"Bo'limlar": "Разделы",
|
||||
"Buyurtmalarim": "Мои заказы",
|
||||
"Arizalarim": "Мои приложения",
|
||||
"Offerta va foydalanish shartlari": "Офферта и Условия использования",
|
||||
"GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan": "© GETGREEN ENERGY TRADE. Все права защищены.",
|
||||
"Asosiy": "Главная",
|
||||
"Market": "Магазин",
|
||||
"Kirish": "Войти",
|
||||
"Bepul maslahat uchun ma'lumotlaringizni kiriting": "Введите свои данные, чтобы получить бесплатную консультацию",
|
||||
"Yuborilmoqda": "Отправить",
|
||||
"Ma'lumotlarni to'ldiring": "Заполните данные",
|
||||
"Narx": "Цена",
|
||||
"office": "Юнусабадский район, улица Ифтихор, 1, Ташкент, Узбекистан"
|
||||
}
|
||||
130
src/shared/config/i18n/messages/uz.json
Normal file
130
src/shared/config/i18n/messages/uz.json
Normal file
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"Sozlamalar": "Sozlamalar",
|
||||
"Ma'lumotlarni yangilash": "Ma'lumotlarni yangilash",
|
||||
"Sahifa topilmadi, Iltimos qayta urinib ko`ring": "Sahifa topilmadi, Iltimos qayta urinib ko`ring",
|
||||
"Orga qaytish": "Orga qaytish",
|
||||
"Login": "Login",
|
||||
"Hisobingizga kirish uchun telefon raqamingizni kiriting": "Hisobingizga kirish uchun telefon raqamingizni kiriting",
|
||||
"Telefon": "Telefon",
|
||||
"terms_of_use": "<tag>{tag}</tag> bilan tanishdim",
|
||||
"Tasdiqlash kodini kiriting": "Tasdiqlash kodini kiriting",
|
||||
"Tasdiqlash": "Tasdiqlash",
|
||||
"Offer va shartlar": "Offer va shartlar",
|
||||
"Offerta shartlari": "Offerta shartlari",
|
||||
"Bu yerda offerta shartlarini o'qib chiqishingiz mumkin": "Bu yerda offerta shartlarini o'qib chiqishingiz mumkin.",
|
||||
"Telefon raqam": "Telefon raqam",
|
||||
"Email": "Email",
|
||||
"Adress": "Adress",
|
||||
"Ish vaqtlari": "Ish vaqtlari",
|
||||
"Biz haqimizda": "Biz haqimizda",
|
||||
"about_us_subtitle": "Bizning onlayn-do'konimiz sizga quyosh uskunalari uchun mahsulotlarning to'liq assortimenti va mos modelni sotib olishning noyob imkoniyatini beradi. Bu yerda siz so'nggi yangilanishlar va chegirmalarni hisobga olgan holda o'zingiz va uyingiz uchun kerak bo'lgan hamma narsani topasiz. Shunday qilib, siz har doim eng yangi mahsulotni eng maqbul narxlarda olasiz.",
|
||||
"about_us_desc": "Bizning kataloglarimiz doimiy ravishda yangi brendlar va ularning mahsulotlari bilan to'ldirilib boriladi, biz bilan birga bo'lib, siz doimo tarmoq invertorlari, quyosh panellari va muqobil energiya bilan bog'liq xizmatlarning barcha spektri sohasidagi so'nggi ishlanmalardan xabardor bo'lasiz.",
|
||||
"Kategoriyalar": "Kategoriyalar",
|
||||
"Kontaktlar": "Kontaktlar",
|
||||
"Ilovamizni yuklab oling": "Ilovamizni yuklab oling",
|
||||
"download_our_app_desc": "Bizning onlayn-do'konimiz sizga quyosh uskunalari uchun mahsulotlarning to'liq assortimenti va mos modelni sotib olishning noyob imkoniyatini beradi. Bu yerda siz so'nggi yangilanishlar va chegirmalarni hisobga olgan holda o'zingiz va uyingiz uchun kerak bo'lgan hamma narsani topasiz. Shunday qilib, siz har doim eng yangi mahsulotni eng maqbul narxlarda olasiz.",
|
||||
"Bepul maslahat uchun ro'yxatdan o'ting": "Bepul maslahat uchun ro'yxatdan o'ting",
|
||||
"Ro'yxatdan o'tish": "Ro'yxatdan o'tish",
|
||||
"Ismingiz": "Ismingiz",
|
||||
"Telefon raqamingiz": "Telefon raqamingiz",
|
||||
"Quyosh uskunalarini ulgurji narxlarda sotib oling!": "Quyosh uskunalarini ulgurji narxlarda sotib oling!",
|
||||
"Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling": "Quyosh elektr stansiyasi uchun hamma narsani bir joyda va eng yaxshi narxda sotib oling.",
|
||||
"Batafsil": "Batafsil",
|
||||
"Quyosh panellari": "Quyosh panellari",
|
||||
"Hammasini ko'rish": "Hammasini ko'rish",
|
||||
"Hamkorlarimiz": "Hamkorlarimiz",
|
||||
"profit_1_desc": "Quyosh panellari tomonidan har kuni ishlab chiqariladigan energiyadan foyda oling. Bu 'yashil tarif' bo'yicha tarmoq quyosh elektr stansiyasi tomonidan ta'minlanadi. Odatda quyosh panellari, tarmoq inverteri, quyosh panellari uchun o'rnatish to'plami, himoya va kommutatsiya uskunalari bilan jihozlangan.",
|
||||
"profit_2_desc": "Har doim yorug'lik bo'ladi - avtonom yoki zaxira quyosh elektr stantsiyasining egasi bo'lganingizda. Tarmoq o'chirilgan bo'lsa ham o'zingizni va uyingizni elektr energiyasi bilan ta'minlaysiz. Bunday quyosh stantsiyalari qo'shimcha ravishda qayta zaryadlanuvchi batareyalar bilan jihozlangan.",
|
||||
"profit_3_desc": "Quyosh elektr stansiyasi uchun barcha kerakli komponentlarni bizning onlayn do'konimizda xarid qilishingiz mumkin. Biz o'zimiz taklif qilayotgan uskunani o'rnatamiz. Va biz faqat foydalanuvchiga haqiqiy foyda keltiradigan narsalarni sotamiz.",
|
||||
"Katalog": "Katalog",
|
||||
"Xizmatlar": "Xizmatlar",
|
||||
"Hamkorlik": "Hamkorlik",
|
||||
"Foydali": "Foydali",
|
||||
"Viloyat": "Viloyat",
|
||||
"Tuman": "Tuman",
|
||||
"Ariza yuborish": "Ariza yuborish",
|
||||
"Sotib olish": "Sotib olish",
|
||||
"Jismoniy shaxs": "Jismoniy shaxs",
|
||||
"Yuridik shaxs": "Yuridik shaxs",
|
||||
"Buyurtma muvaffaqiyatli yaratildi!": "Buyurtma muvaffaqiyatli yaratildi!",
|
||||
"Siz bilan tez orada bog'lanamiz": "Siz bilan tez orada bog'lanamiz.",
|
||||
"Buyurtma yaratishda xatolik!": "Buyurtma yaratishda xatolik!",
|
||||
"Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring": "Ma'lumotlar to'liq kiritilganligiga ishonch hosil qiling yoki qaytadan urinib ko'ring.",
|
||||
"Kompaniya nomi": "Kompaniya nomi",
|
||||
"Direktor": "Direktor F.I.Sh.",
|
||||
"Yuridik manzil": "Yuridik manzil",
|
||||
"Bank nomi": "Bank nomi",
|
||||
"Hisob raqam": "Hisob raqam",
|
||||
"Yetkazib berish": "Yetkazib berish",
|
||||
"Viloyatni tanlang": "Viloyatni tanlang",
|
||||
"Tuman/shahar": "Tuman/shahar",
|
||||
"Manzil": "Manzil",
|
||||
"Uy raqami": "Uy raqami",
|
||||
"Mo'ljal": "Mo'ljal",
|
||||
"O‘rnatish xizmati kerakmi?": "O‘rnatish xizmati kerakmi?",
|
||||
"Ha": "Ha",
|
||||
"Yo‘q": "Yo‘q",
|
||||
"Yetkazib berish kerakmi": "Yetkazib berish kerakmi",
|
||||
"Yo‘q o'zim olib ketaman": "Yo‘q o'zim olib ketaman",
|
||||
"Yuborish": "Yuborish",
|
||||
"full_name": "F.I.Sh.",
|
||||
"Passport seriya va raqami": "Passport seriya va raqami",
|
||||
"Yetkazib berish kerakmi?": "Yetkazib berish kerakmi?",
|
||||
"so'm": "so'm",
|
||||
"QQS bilan": "QQS bilan",
|
||||
"chegirma": "chegirma",
|
||||
"Boshqa mahsulotlar": "Boshqa mahsulotlar",
|
||||
"Mening arizalarim": "Mening arizalarim",
|
||||
"Sizning arizalaringiz va ularning holati haqida ma'lumotlar": "Sizning arizalaringiz va ularning holati haqida ma'lumotlar",
|
||||
"Ariza": "Ariza",
|
||||
"Yaratilish vaqti": "Yaratilish vaqti",
|
||||
"Ariza tafsilotlari": "Ariza tafsilotlari",
|
||||
"Ariza turi": "Ariza turi",
|
||||
"Ariza raqami": "Ariza raqami",
|
||||
"Ariza holati": "Ariza holati",
|
||||
"Xizmat tafsilotlari": "Xizmat tafsilotlari",
|
||||
"Quvvat": "Quvvat",
|
||||
"Telefon raqami": "Telefon raqami",
|
||||
"Izoh": "Izoh",
|
||||
"Bog'lanish": "Biz bilan bog'lanish",
|
||||
"Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin": "Biz bilan bog'lanish uchun quyidagi usullardan foydalanishingiz mumkin",
|
||||
"Call Center": "Call Center",
|
||||
"Xatolik yuz berdi": "Xatolik yuz berdi",
|
||||
"Profil ma'lumotlari": "Profil ma'lumotlari",
|
||||
"Sizning profil ma'lumotlaringiz va ularni o'zgartirish": "Sizning profil ma'lumotlaringiz va ularni o'zgartirish",
|
||||
"Familiyangiz": "Familiyangiz",
|
||||
"Sharif": "Sharif",
|
||||
"Saqlanmoqda": "Saqlanmoqda...",
|
||||
"Saqlash": "Saqlash",
|
||||
"Mening buyurtmalarim": "Mening buyurtmalarim",
|
||||
"Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar": "Sizning buyurtmalaringiz va ularning holati haqida ma'lumotlar",
|
||||
"Buyurtma": "Buyurtma",
|
||||
"Buyurtma tafsilotlari": "Buyurtma tafsilotlari",
|
||||
"Buyurtma raqami": "Buyurtma raqami",
|
||||
"Buyurtma holati": "Buyurtma holati",
|
||||
"To'lov holati": "To'lov holati",
|
||||
"Xaridlar ro'yxati": "Xaridlar ro'yxati",
|
||||
"Mijoz ma'lumotlari": "Mijoz ma'lumotlari",
|
||||
"Mijoz turi": "Mijoz turi",
|
||||
"Mijoz telefon": "Mijoz telefon raqami",
|
||||
"Yetkazib berish turi": "Yetkazib berish turi",
|
||||
"Yetkazib berish manzili": "Yetkazib berish manzili",
|
||||
"Yetkazib berish narxi": "Yetkazib berish narxi",
|
||||
"To'lash": "To'lash",
|
||||
"Foydali ma'lumotlar ro'yxati": "Foydali ma'lumotlar ro'yxati",
|
||||
"Download PDF": "Faylni yuklab olish",
|
||||
"Yopish": "Yopish",
|
||||
"Bo'limlar": "Bo'limlar",
|
||||
"Buyurtmalarim": "Buyurtmalarim",
|
||||
"Arizalarim": "Arizalarim",
|
||||
"Offerta va foydalanish shartlari": "Offerta va foydalanish shartlari",
|
||||
"GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan": "GET-GREEN ENERGY TRADE - Barcha huquqlar himoyalangan",
|
||||
"Asosiy": "Asosiy",
|
||||
"Market": "Market",
|
||||
"Kirish": "Kirish",
|
||||
"Bepul maslahat uchun ma'lumotlaringizni kiriting": "Bepul maslahat uchun ma'lumotlaringizni kiriting",
|
||||
"Yuborilmoqda": "Yuborilmoqda...",
|
||||
"Ma'lumotlarni to'ldiring": "Ma'lumotlarni to'ldiring",
|
||||
"Narx": "Narx",
|
||||
"office": "Yunusobod tumani, Iftixor ko‘chasi, 1-uy, Toshkent, O‘zbekiston"
|
||||
}
|
||||
7
src/shared/config/i18n/navigation.ts
Normal file
7
src/shared/config/i18n/navigation.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createNavigation } from "next-intl/navigation";
|
||||
import { routing } from "./routing";
|
||||
|
||||
// Lightweight wrappers around Next.js' navigation
|
||||
// APIs that consider the routing configuration
|
||||
export const { Link, redirect, usePathname, useRouter, getPathname } =
|
||||
createNavigation(routing);
|
||||
16
src/shared/config/i18n/request.ts
Normal file
16
src/shared/config/i18n/request.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { getRequestConfig } from "next-intl/server";
|
||||
import { hasLocale } from "next-intl";
|
||||
import { routing } from "./routing";
|
||||
|
||||
export default getRequestConfig(async ({ requestLocale }) => {
|
||||
// Typically corresponds to the `[locale]` segment
|
||||
const requested = await requestLocale;
|
||||
const locale = hasLocale(routing.locales, requested)
|
||||
? requested
|
||||
: routing.defaultLocale;
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`./messages/${locale}.json`)).default,
|
||||
};
|
||||
});
|
||||
11
src/shared/config/i18n/routing.ts
Normal file
11
src/shared/config/i18n/routing.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineRouting } from "next-intl/routing";
|
||||
import { LanguageRoutes } from "./types";
|
||||
|
||||
export const routing = defineRouting({
|
||||
// A list of all locales that are supported
|
||||
locales: [LanguageRoutes.UZ, LanguageRoutes.RU],
|
||||
|
||||
// Used when no locale matches
|
||||
defaultLocale: LanguageRoutes.UZ,
|
||||
localeDetection: false,
|
||||
});
|
||||
4
src/shared/config/i18n/types.ts
Normal file
4
src/shared/config/i18n/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum LanguageRoutes {
|
||||
UZ = "uz", // o'zbekcha
|
||||
RU = "ru", // ruscha
|
||||
}
|
||||
7
src/shared/config/index.ts
Normal file
7
src/shared/config/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {golosText} from "@/shared/config/fonts";
|
||||
import {routing} from "@/shared/config/i18n/routing";
|
||||
|
||||
export {
|
||||
golosText,
|
||||
routing
|
||||
}
|
||||
11
src/shared/config/theme-provider.tsx
Normal file
11
src/shared/config/theme-provider.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
||||
}
|
||||
19
src/shared/constants/apiEndpoints.ts
Normal file
19
src/shared/constants/apiEndpoints.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const API_URL = process.env.API_URL
|
||||
|
||||
export const CATEGORIES = "/categories"
|
||||
export const BRANDS = "/brands"
|
||||
export const COMPILATIONS = "/compilations"
|
||||
export const PARTNERS = "/partners"
|
||||
export const PRODUCTS = "/products"
|
||||
export const SERVICES = "/services"
|
||||
export const USEFUL_INFORMATION = "/useful-information"
|
||||
export const OAUTH = '/oauth/'
|
||||
export const OAUTH_VERIFY = '/oauth/verify'
|
||||
export const SUPPORT = '/support'
|
||||
export const USER_ORDERS = '/user/orders'
|
||||
export const USER_REQUESTS = '/user/requests'
|
||||
export const FEEDBACK = '/feedback'
|
||||
export const PAGE_POLICY= "page/policy"
|
||||
export const USER_ME= "/user/me"
|
||||
export const REGIONS = "/regions"
|
||||
export const CHECKOUT = "/checkout"
|
||||
26
src/shared/constants/data.ts
Normal file
26
src/shared/constants/data.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
const PRODUCT_INFO = {
|
||||
name: "Get Green",
|
||||
description: "Generated by create next app",
|
||||
logo: "/getgreen.png",
|
||||
favicon: "/favicon.png",
|
||||
url: "https://getgreen.uz",
|
||||
socials: {
|
||||
telegram: "https://t.me/usmanov_dev",
|
||||
instagram: "https://t.me/usmanov_dev",
|
||||
youtube: "https://t.me/usmanov_dev",
|
||||
linkedin: "https://www.linkedin.com/in/usmonov-azizbek/",
|
||||
},
|
||||
contact: {
|
||||
phone: "+998555067788",
|
||||
email: "contact@fias.uz",
|
||||
location: "Yunusabadskiy rayon, ulisa Iftixor, 1"
|
||||
},
|
||||
terms_of_use: "",
|
||||
creator: "Get Green",
|
||||
app: {
|
||||
ios: "/",
|
||||
android: "/"
|
||||
}
|
||||
}
|
||||
|
||||
export {PRODUCT_INFO}
|
||||
7
src/shared/constants/index.ts
Normal file
7
src/shared/constants/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {PRODUCT_INFO} from "@/shared/constants/data";
|
||||
import {profileSidebarMenu} from "@/shared/constants/profileSidebar";
|
||||
export * from "./apiEndpoints"
|
||||
export {
|
||||
PRODUCT_INFO,
|
||||
profileSidebarMenu
|
||||
}
|
||||
39
src/shared/constants/profileSidebar.ts
Normal file
39
src/shared/constants/profileSidebar.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {Calendar, Home, Inbox, Search, Settings, PhoneCall, Newspaper} from "lucide-react";
|
||||
|
||||
export const profileSidebarMenu = [
|
||||
{
|
||||
label: "Bo'limlar",
|
||||
menus: [
|
||||
{
|
||||
title: "Profil ma'lumotlari",
|
||||
url: "/profile",
|
||||
icon: Home,
|
||||
},
|
||||
{
|
||||
title: "Buyurtmalarim",
|
||||
url: "/profile/orders",
|
||||
icon: Inbox,
|
||||
},
|
||||
{
|
||||
title: "Arizalarim",
|
||||
url: "/profile/applications",
|
||||
icon: Calendar,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: "Sozlamalar",
|
||||
menus: [
|
||||
{
|
||||
title: "Offerta va foydalanish shartlari",
|
||||
url: "/profile/terms",
|
||||
icon: Newspaper,
|
||||
},
|
||||
{
|
||||
title: "Bog'lanish",
|
||||
url: "/profile/contact",
|
||||
icon: PhoneCall,
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
14
src/shared/hooks/use-locale.ts
Normal file
14
src/shared/hooks/use-locale.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const useLocale = ()=>{
|
||||
const locale = window.location.pathname.split('/')[1]
|
||||
const setLocale = (newLocale: string) => {
|
||||
const segments = window.location.pathname.split('/');
|
||||
segments[1] = newLocale;
|
||||
const newPath = segments.join('/');
|
||||
window.history.pushState({}, '', newPath);
|
||||
window.location.reload();
|
||||
}
|
||||
return {
|
||||
locale,
|
||||
setLocale
|
||||
}
|
||||
}
|
||||
19
src/shared/hooks/use-mobile.ts
Normal file
19
src/shared/hooks/use-mobile.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
const MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
}
|
||||
mql.addEventListener("change", onChange)
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
|
||||
return () => mql.removeEventListener("change", onChange)
|
||||
}, [])
|
||||
|
||||
return !!isMobile
|
||||
}
|
||||
27
src/shared/hooks/use-query-string.ts
Normal file
27
src/shared/hooks/use-query-string.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export function useQueryString() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const createQueryString = useCallback(
|
||||
(name: string, value: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(name, value);
|
||||
|
||||
return params.toString();
|
||||
},
|
||||
[searchParams]
|
||||
);
|
||||
|
||||
return {
|
||||
searchParams,
|
||||
router,
|
||||
pathname,
|
||||
createQueryString,
|
||||
};
|
||||
}
|
||||
5
src/shared/lib/formatNumberWithSpace.ts
Normal file
5
src/shared/lib/formatNumberWithSpace.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const formatNumberWithSpaces = (number: number | string = "") => {
|
||||
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
}
|
||||
|
||||
export default formatNumberWithSpaces
|
||||
38
src/shared/lib/formatPhone.ts
Normal file
38
src/shared/lib/formatPhone.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Format the number (+998 00 111-22-33)
|
||||
* @param value Number to be formatted
|
||||
* @returns string +998 00 111-22-33
|
||||
*/
|
||||
const formatPhone = (value: string) => {
|
||||
// Keep only numbers
|
||||
const digits = value.replace(/\D/g, '');
|
||||
|
||||
// Return empty string if data is not available
|
||||
if (digits.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix = digits.startsWith('998') ? '+998 ' : '+998 ';
|
||||
|
||||
let formattedNumber = prefix;
|
||||
|
||||
if (digits.length > 3) {
|
||||
formattedNumber += digits.slice(3, 5);
|
||||
}
|
||||
|
||||
if (digits.length > 5) {
|
||||
formattedNumber += ' ' + digits.slice(5, 8);
|
||||
}
|
||||
|
||||
if (digits.length > 8) {
|
||||
formattedNumber += '-' + digits.slice(8, 10);
|
||||
}
|
||||
|
||||
if (digits.length > 10) {
|
||||
formattedNumber += '-' + digits.slice(10, 12);
|
||||
}
|
||||
|
||||
return formattedNumber.trim();
|
||||
};
|
||||
|
||||
export default formatPhone;
|
||||
9
src/shared/lib/getCurrentLocale.ts
Normal file
9
src/shared/lib/getCurrentLocale.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const getCurrentLocale = (): string | undefined => {
|
||||
if (typeof window !== "undefined" && typeof document !== "undefined") {
|
||||
const match = document.cookie
|
||||
.split("; ")
|
||||
.find(row => row.startsWith("NEXT_LOCALE="));
|
||||
return match?.split("=")[1];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
6
src/shared/lib/utils.ts
Normal file
6
src/shared/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
24
src/shared/providers/PrivateRouteProvider.tsx
Normal file
24
src/shared/providers/PrivateRouteProvider.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
import React, {useEffect} from 'react';
|
||||
import {useRouter} from 'next/navigation';
|
||||
import {useAuthStore} from '@/shared/store/authStore';
|
||||
import {isBrowser} from "motion/react";
|
||||
|
||||
const PrivateRoute = ({children}: { children: React.ReactNode }) => {
|
||||
const {user, isAuthenticated} = useAuthStore();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (isBrowser && (!user || !isAuthenticated)) {
|
||||
router.push('/auth/login');
|
||||
}
|
||||
}, [user, isAuthenticated, router]);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default PrivateRoute;
|
||||
21
src/shared/providers/QueryProvider.tsx
Normal file
21
src/shared/providers/QueryProvider.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
"use client"
|
||||
import {ReactNode, useState} from "react";
|
||||
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
|
||||
|
||||
const QueryProvider = ({children}: {children: ReactNode})=> {
|
||||
const [queryClient] = useState(() => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
refetchInterval: 1000 * 60 * 5,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
}
|
||||
|
||||
export default QueryProvider;
|
||||
2
src/shared/providers/index.ts
Normal file
2
src/shared/providers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./PrivateRouteProvider"
|
||||
export * from "./QueryProvider"
|
||||
41
src/shared/store/authStore.ts
Normal file
41
src/shared/store/authStore.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import {create} from 'zustand';
|
||||
import {persist} from 'zustand/middleware';
|
||||
import {isBrowser} from "motion/react";
|
||||
|
||||
interface AuthData {
|
||||
id: number;
|
||||
phone: string;
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
user: AuthData | null;
|
||||
login: (data: AuthData) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
user: isBrowser && typeof window !== "undefined" ? JSON.parse(localStorage.getItem('auth-store') || 'null') : null,
|
||||
login: (data) => {
|
||||
set({user: data});
|
||||
if (isBrowser && typeof window !== "undefined") {
|
||||
localStorage.setItem('auth-store', JSON.stringify(data));
|
||||
}
|
||||
},
|
||||
logout: () => {
|
||||
set({user: null});
|
||||
if (isBrowser && typeof window !== "undefined") {
|
||||
localStorage.removeItem('auth-store');
|
||||
}
|
||||
},
|
||||
isAuthenticated: isBrowser && typeof window !== "undefined" && localStorage.getItem("auth-store") !== null,
|
||||
}),
|
||||
{
|
||||
name: 'auth-store',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
23
src/shared/style/custom-utils.css
Normal file
23
src/shared/style/custom-utils.css
Normal file
@@ -0,0 +1,23 @@
|
||||
@layer utilities {
|
||||
.my-container {
|
||||
@apply container mx-auto transition-all duration-300;
|
||||
}
|
||||
.section-title{
|
||||
@apply text-2xl max-sm:text-[28px] font-bold mb-4 text-[2.8rem] leading-[1.17];
|
||||
}
|
||||
.section-subtitle{
|
||||
@apply text-base leading-[1.6];
|
||||
}
|
||||
.section-wrapper{
|
||||
@apply py-24
|
||||
}
|
||||
.profile-section-wrapper{
|
||||
@apply bg-white rounded-xl w-full p-4 h-fit
|
||||
}
|
||||
.profile-section-title{
|
||||
@apply text-xl font-semibold
|
||||
}
|
||||
.profile-section-subtitle{
|
||||
@apply text-sm font-semibold text-gray-500
|
||||
}
|
||||
}
|
||||
41
src/shared/types/brands.d.ts
vendored
Normal file
41
src/shared/types/brands.d.ts
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
export interface Brands {
|
||||
pagination: Pagination
|
||||
data: BrandsResult[]
|
||||
}
|
||||
|
||||
export interface BrandsResult {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
slug: string
|
||||
}
|
||||
|
||||
// Brand products
|
||||
export interface BrandProductsType {
|
||||
pagination: Pagination
|
||||
data: BrandProductsResultType[]
|
||||
}
|
||||
|
||||
export interface BrandProductsResultType {
|
||||
id: number
|
||||
name: string
|
||||
price: number
|
||||
price_discount: number
|
||||
discount_percent: number
|
||||
is_leader_of_sales: boolean
|
||||
poster: string
|
||||
poster_thumb: string
|
||||
is_favorite: boolean
|
||||
is_cart: boolean
|
||||
count: number
|
||||
power: number
|
||||
}
|
||||
|
||||
interface Pagination {
|
||||
current: number
|
||||
previous: any
|
||||
next: number
|
||||
total: number
|
||||
perPage: number
|
||||
totalItems: number
|
||||
}
|
||||
9
src/shared/types/compilations.d.ts
vendored
Normal file
9
src/shared/types/compilations.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface GetCompilationResponse {
|
||||
data: Compilation[]
|
||||
}
|
||||
|
||||
export interface Compilation {
|
||||
id: number
|
||||
title: string
|
||||
products: Product[]
|
||||
}
|
||||
0
src/shared/types/feedback.d.ts
vendored
Normal file
0
src/shared/types/feedback.d.ts
vendored
Normal file
1
src/shared/types/locale.ts
Normal file
1
src/shared/types/locale.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Locale = "ru" | "uz"
|
||||
10
src/shared/types/pagination.d.ts
vendored
Normal file
10
src/shared/types/pagination.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Pagination {
|
||||
pagination: {
|
||||
current: 1,
|
||||
previous: null,
|
||||
next: 2,
|
||||
total: 3,
|
||||
perPage: 20,
|
||||
totalItems: 41
|
||||
},
|
||||
}
|
||||
24
src/shared/types/partners.d.ts
vendored
Normal file
24
src/shared/types/partners.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface Partners {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
status: Status
|
||||
is_price: boolean
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
slug: string
|
||||
translation: string
|
||||
font_color: string
|
||||
bg_color: string
|
||||
}
|
||||
|
||||
|
||||
export interface GetPartnersResponse {
|
||||
data: Partners[]
|
||||
pagination: {
|
||||
total: number
|
||||
page: number
|
||||
limit: number
|
||||
}
|
||||
}
|
||||
23
src/shared/types/product.d.ts
vendored
Normal file
23
src/shared/types/product.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import {Pagination} from "@/shared/types/pagination";
|
||||
|
||||
export interface Product {
|
||||
id: number
|
||||
name: string
|
||||
price: number
|
||||
price_usd: number
|
||||
price_discount: number
|
||||
discount_percent: number
|
||||
is_leader_of_sales: boolean
|
||||
poster: string
|
||||
poster_thumb: string
|
||||
is_favorite: boolean
|
||||
is_cart: boolean
|
||||
count: number
|
||||
power: number
|
||||
short_description?: string | TrustedHTML
|
||||
description?: string | TrustedHTML
|
||||
}
|
||||
|
||||
export interface GetProductsResponse extends Pagination{
|
||||
data: Product[]
|
||||
}
|
||||
23
src/shared/types/productCategory.d.ts
vendored
Normal file
23
src/shared/types/productCategory.d.ts
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface ProductCategory {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
parent_id: any
|
||||
parents: Child[]
|
||||
}
|
||||
|
||||
export interface Child {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
parent_id: number
|
||||
parents: SubChild[]
|
||||
}
|
||||
|
||||
export interface SubChild {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
parent_id: number
|
||||
parents: any[]
|
||||
}
|
||||
14
src/shared/types/region.d.ts
vendored
Normal file
14
src/shared/types/region.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface GetRegionsResponse {
|
||||
data: Region[]
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
id: number
|
||||
name: string
|
||||
cities: City[]
|
||||
}
|
||||
|
||||
export interface City {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
32
src/shared/types/services.d.ts
vendored
Normal file
32
src/shared/types/services.d.ts
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface Service {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
type: string
|
||||
status: Status
|
||||
is_power: boolean
|
||||
with_problem: boolean
|
||||
created_at: string
|
||||
problems: Problem[]
|
||||
}
|
||||
|
||||
export interface Status {
|
||||
slug: string
|
||||
translation: string
|
||||
font_color: string
|
||||
bg_color: string
|
||||
}
|
||||
|
||||
export interface Problem {
|
||||
id: number
|
||||
title: string
|
||||
}
|
||||
|
||||
|
||||
export interface GetServicesResponse {
|
||||
data: Service[]
|
||||
}
|
||||
|
||||
export interface GetServiceByIdResponse {
|
||||
data: Service
|
||||
}
|
||||
10
src/shared/types/useful.d.ts
vendored
Normal file
10
src/shared/types/useful.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface UsefulItem {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
position: number
|
||||
}
|
||||
|
||||
export interface GetUsefulResponse {
|
||||
data: UsefulItem[]
|
||||
}
|
||||
11
src/shared/types/user.d.ts
vendored
Normal file
11
src/shared/types/user.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface GetUserMeResponse {
|
||||
data: {
|
||||
id: number
|
||||
first_name: string
|
||||
last_name: string
|
||||
middle_name: string
|
||||
language: "uz" | "ru"
|
||||
phone: string
|
||||
gender: boolean
|
||||
}
|
||||
}
|
||||
81
src/shared/types/userOrders.d.ts
vendored
Normal file
81
src/shared/types/userOrders.d.ts
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
import {Pagination} from "@/shared/types/pagination";
|
||||
|
||||
export interface GetUserOrdersResponse extends Pagination{
|
||||
data: UserOrderItem[]
|
||||
}
|
||||
|
||||
export interface UserOrderItem {
|
||||
id: number
|
||||
status: PaymentStatus
|
||||
created_at: string
|
||||
total_amount: number
|
||||
}
|
||||
|
||||
|
||||
|
||||
//by id
|
||||
export interface GetUserOrderByIdResponse {
|
||||
data: UserOrderByIdItem
|
||||
}
|
||||
|
||||
export interface UserOrderByIdItem {
|
||||
id: number
|
||||
pay_url: string
|
||||
status: Status
|
||||
payment_type: string
|
||||
payment_status: PaymentStatus
|
||||
created_at: string
|
||||
delivery_type: string
|
||||
client_type: string
|
||||
total_amount: number
|
||||
products: Product[]
|
||||
address: Address
|
||||
legal_information: any
|
||||
client_information: ClientInformation
|
||||
branch: any
|
||||
with_installation: boolean
|
||||
with_didox: boolean
|
||||
price_products: number
|
||||
price_delivery: number
|
||||
price_master: number
|
||||
contract: any
|
||||
}
|
||||
|
||||
|
||||
export interface PaymentStatus {
|
||||
slug: string
|
||||
translation: string
|
||||
font_color: string
|
||||
bg_color: string
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: number
|
||||
name: string
|
||||
count: number
|
||||
price: number
|
||||
total_price: number
|
||||
}
|
||||
|
||||
export interface Address {
|
||||
id: number
|
||||
city: City
|
||||
home: string
|
||||
landmark: string
|
||||
}
|
||||
|
||||
export interface City {
|
||||
id: number
|
||||
name: string
|
||||
region: Region
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ClientInformation {
|
||||
full_name: string
|
||||
phone: number
|
||||
}
|
||||
49
src/shared/types/userRequests.d.ts
vendored
Normal file
49
src/shared/types/userRequests.d.ts
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import {Pagination} from "@/shared/types/pagination";
|
||||
import {City, PaymentStatus} from "@/shared/types/userOrders";
|
||||
|
||||
export interface GetUserRequestsResponse extends Pagination{
|
||||
data: UserRequestItem[]
|
||||
}
|
||||
|
||||
export interface UserRequestItem {
|
||||
id: number
|
||||
status: Status
|
||||
created_at: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
//by id
|
||||
export interface GetUserRequestByIdResponse {
|
||||
data: UserRequestByIdItem
|
||||
}
|
||||
|
||||
export interface UserRequestByIdItem {
|
||||
id: number
|
||||
service: Service
|
||||
power: Power
|
||||
city: City
|
||||
comment: string
|
||||
phone: number
|
||||
full_name: string
|
||||
status: PaymentStatus
|
||||
problem: any
|
||||
}
|
||||
|
||||
export interface Power {
|
||||
id: number
|
||||
name: string
|
||||
power: number
|
||||
}
|
||||
|
||||
export interface Service {
|
||||
id: number
|
||||
name: string
|
||||
image: string
|
||||
type: string
|
||||
status: Status
|
||||
is_power: boolean
|
||||
with_problem: boolean
|
||||
created_at: string
|
||||
problems: any[]
|
||||
}
|
||||
72
src/shared/ui/accordion.tsx
Normal file
72
src/shared/ui/accordion.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import {ChevronDownIcon} from "lucide-react"
|
||||
|
||||
import {cn} from "@/shared/lib/utils"
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface AccordionTriggerProps extends React.ComponentProps<typeof AccordionPrimitive.Trigger> {
|
||||
showArrowIcon?: boolean;
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
showArrowIcon,
|
||||
children,
|
||||
...props
|
||||
}: AccordionTriggerProps) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showArrowIcon && <ChevronDownIcon
|
||||
className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200"/>}
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export {Accordion, AccordionItem, AccordionTrigger, AccordionContent}
|
||||
53
src/shared/ui/avatar.tsx
Normal file
53
src/shared/ui/avatar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
46
src/shared/ui/badge.tsx
Normal file
46
src/shared/ui/badge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
59
src/shared/ui/button.tsx
Normal file
59
src/shared/ui/button.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex cursor-pointer items-center justify-center gap-2 whitespace-nowrap rounded-sm text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary font-bold text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-7 text-[1rem] py-7 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
92
src/shared/ui/card.tsx
Normal file
92
src/shared/ui/card.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
18
src/shared/ui/category-card.tsx
Normal file
18
src/shared/ui/category-card.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import {ProductCategory} from "@/shared/types/productCategory";
|
||||
import {Link} from "@/shared/config/i18n/navigation";
|
||||
|
||||
interface CategoryCardProps{
|
||||
category: ProductCategory
|
||||
}
|
||||
|
||||
export const CategoryCard = ({category}: CategoryCardProps) => {
|
||||
return (
|
||||
<Link className={"bg-white p-10 flex flex-col justify-center items-center"} href={`/category/${category.id}`}>
|
||||
<Image className={"mx-auto mb-5 filter hue-rotate-[-40deg] saturate-200 brightness-75"} src={category.image}
|
||||
alt={"category"} width={50} height={50}/>
|
||||
<h2 className="text-xl font-bold text-center">{category.name}</h2>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
32
src/shared/ui/checkbox.tsx
Normal file
32
src/shared/ui/checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<CheckIcon className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Checkbox }
|
||||
135
src/shared/ui/dialog.tsx
Normal file
135
src/shared/ui/dialog.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
257
src/shared/ui/dropdown-menu.tsx
Normal file
257
src/shared/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean
|
||||
variant?: "default" | "destructive"
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
}
|
||||
77
src/shared/ui/input-otp.tsx
Normal file
77
src/shared/ui/input-otp.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { OTPInput, OTPInputContext } from "input-otp"
|
||||
import { MinusIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function InputOTP({
|
||||
className,
|
||||
containerClassName,
|
||||
...props
|
||||
}: React.ComponentProps<typeof OTPInput> & {
|
||||
containerClassName?: string
|
||||
}) {
|
||||
return (
|
||||
<OTPInput
|
||||
data-slot="input-otp"
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-disabled:opacity-50",
|
||||
containerClassName
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex gap-4 mx-auto items-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
index,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
index: number
|
||||
}) {
|
||||
const inputOTPContext = React.useContext(OTPInputContext)
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="input-otp-slot"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
" data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-12 w-12 items-center justify-center border text-xl shadow-xs transition-all outline-none rounded-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div data-slot="input-otp-separator" role="separator" {...props}>
|
||||
<MinusIcon />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator }
|
||||
21
src/shared/ui/input.tsx
Normal file
21
src/shared/ui/input.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-10 w-full min-w-0 rounded-sm border bg-transparent px-3 py-3 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Input }
|
||||
24
src/shared/ui/label.tsx
Normal file
24
src/shared/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
37
src/shared/ui/loader.tsx
Normal file
37
src/shared/ui/loader.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import {cn} from "@/shared/lib/utils";
|
||||
|
||||
interface LoaderProps {
|
||||
height?: string
|
||||
}
|
||||
|
||||
const Loader = ({height}: LoaderProps) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
"w-full h-full flex justify-center items-center",
|
||||
height ?? "h-[calc(100vh-80px)]"
|
||||
)}>
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-3 h-5 w-5 text-primary"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 1 1 16 0A8 8 0 0 1 4 12z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Loader
|
||||
63
src/shared/ui/my-pagionation.tsx
Normal file
63
src/shared/ui/my-pagionation.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
"use client"
|
||||
import React from 'react';
|
||||
import {Pagination, PaginationContent, PaginationItem} from "@/shared/ui/pagination";
|
||||
import {Link, useRouter} from "@/shared/config/i18n/navigation";
|
||||
import {Button} from "@/shared/ui/button";
|
||||
import {useSearchParams} from "next/navigation";
|
||||
|
||||
interface MyPagionationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const MyPagionation = ({currentPage, totalPages}: MyPagionationProps) => {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const navigateToPage = (page: number) => {
|
||||
const currentParams = new URLSearchParams(searchParams.toString());
|
||||
currentParams.set("page", String(page));
|
||||
|
||||
router.push(`?${currentParams.toString()}`);
|
||||
};
|
||||
|
||||
const renderPaginationItems = () => {
|
||||
return Array.from({length: totalPages}, (_, index) => {
|
||||
const page = index + 1;
|
||||
const isActive = currentPage === page;
|
||||
return (
|
||||
<Button
|
||||
key={page}
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className={isActive ? "bg-primary/10" : ""}
|
||||
onClick={() => navigateToPage(page)}
|
||||
disabled={isActive}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
{currentPage > 1 && (
|
||||
<Button size="lg" onClick={() => navigateToPage(currentPage - 1)}>
|
||||
Oldingi
|
||||
</Button>
|
||||
)}
|
||||
{renderPaginationItems()}
|
||||
{currentPage < totalPages && (
|
||||
<Button size="lg" onClick={() => navigateToPage(currentPage + 1)}>
|
||||
Keyingi
|
||||
</Button>
|
||||
)}
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyPagionation;
|
||||
168
src/shared/ui/navigation-menu.tsx
Normal file
168
src/shared/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import * as React from "react"
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||
import { cva } from "class-variance-authority"
|
||||
import { ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn(
|
||||
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center gap-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item
|
||||
data-slot="navigation-menu-item"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
)
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-full left-0 isolate z-50 flex justify-center"
|
||||
)}
|
||||
>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuLink({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
}
|
||||
127
src/shared/ui/pagination.tsx
Normal file
127
src/shared/ui/pagination.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
import { Button, buttonVariants } from "@/shared/ui/button"
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
50
src/shared/ui/product-card.tsx
Normal file
50
src/shared/ui/product-card.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import React from "react";
|
||||
import { Product } from "@/shared/types/product";
|
||||
import { Link } from "@/shared/config/i18n/navigation";
|
||||
import formatNumberWithSpaces from "@/shared/lib/formatNumberWithSpace";
|
||||
import { useIsMobile } from "@/shared/hooks/use-mobile";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface ProductCardProps {
|
||||
product: Product;
|
||||
}
|
||||
|
||||
const ProductCard = ({ product }: ProductCardProps) => {
|
||||
const isMobile = useIsMobile();
|
||||
const t = useTranslations("");
|
||||
return (
|
||||
<div className={"bg-white p-5 flex flex-col justify-between"}>
|
||||
<div>
|
||||
<div className={"relative h-[300px]"}>
|
||||
<Image
|
||||
className={"mx-auto mb-5"}
|
||||
src={isMobile ? product.poster_thumb : product.poster}
|
||||
alt={"category"}
|
||||
fill
|
||||
objectFit={"cover"}
|
||||
/>
|
||||
</div>
|
||||
<h3 className="font-bold text-xl my-5">{product.name}</h3>
|
||||
<p>
|
||||
<b>{t("Narx")}</b>:{" "}
|
||||
{product.price_discount ? (
|
||||
<span className={"text-destructive line-through"}>
|
||||
{formatNumberWithSpaces(product.price_discount)}
|
||||
</span>
|
||||
) : null}{" "}
|
||||
{formatNumberWithSpaces(product.price)} {t("so'm")}
|
||||
</p>
|
||||
</div>
|
||||
<div className={"text-center"}>
|
||||
<Button className={"mt-8 px-16"} asChild>
|
||||
<Link href={`/product/${product.id}`}>Batafsil</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductCard;
|
||||
16
src/shared/ui/progressbar.tsx
Normal file
16
src/shared/ui/progressbar.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import React from 'react'
|
||||
import {AppProgressBar} from "next-nprogress-bar";
|
||||
|
||||
const ProgressBar = () => {
|
||||
return (
|
||||
<AppProgressBar
|
||||
height="4px"
|
||||
color="#fffd00"
|
||||
options={{ showSpinner: false }}
|
||||
shallowRouting
|
||||
/>
|
||||
)
|
||||
}
|
||||
export default ProgressBar
|
||||
45
src/shared/ui/radio-group.tsx
Normal file
45
src/shared/ui/radio-group.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
||||
import { CircleIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root
|
||||
data-slot="radio-group"
|
||||
className={cn("grid gap-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
data-slot="radio-group-item"
|
||||
className={cn(
|
||||
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator
|
||||
data-slot="radio-group-indicator"
|
||||
className="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
</RadioGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem }
|
||||
185
src/shared/ui/select.tsx
Normal file
185
src/shared/ui/select.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-10 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="size-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon className="size-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
28
src/shared/ui/separator.tsx
Normal file
28
src/shared/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator-root"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Separator }
|
||||
139
src/shared/ui/sheet.tsx
Normal file
139
src/shared/ui/sheet.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />
|
||||
}
|
||||
|
||||
function SheetTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
|
||||
}
|
||||
|
||||
function SheetClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Close>) {
|
||||
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
|
||||
}
|
||||
|
||||
function SheetPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />
|
||||
}
|
||||
|
||||
function SheetOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Description>) {
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
726
src/shared/ui/sidebar.tsx
Normal file
726
src/shared/ui/sidebar.tsx
Normal file
@@ -0,0 +1,726 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { VariantProps, cva } from "class-variance-authority"
|
||||
import { PanelLeftIcon } from "lucide-react"
|
||||
|
||||
import { useIsMobile } from "@/shared/hooks/use-mobile"
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
import { Button } from "@/shared/ui/button"
|
||||
import { Input } from "@/shared/ui/input"
|
||||
import { Separator } from "@/shared/ui/separator"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/shared/ui/sheet"
|
||||
import { Skeleton } from "@/shared/ui/skeleton"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/shared/ui/tooltip"
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
const SIDEBAR_WIDTH = "16rem"
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed"
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
openMobile: boolean
|
||||
setOpenMobile: (open: boolean) => void
|
||||
isMobile: boolean
|
||||
toggleSidebar: () => void
|
||||
}
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null)
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext)
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.")
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
function SidebarProvider({
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) {
|
||||
const isMobile = useIsMobile()
|
||||
const [openMobile, setOpenMobile] = React.useState(false)
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen)
|
||||
const open = openProp ?? _open
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState)
|
||||
} else {
|
||||
_setOpen(openState)
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
|
||||
},
|
||||
[setOpenProp, open]
|
||||
)
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open)
|
||||
}, [isMobile, setOpen, setOpenMobile])
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault()
|
||||
toggleSidebar()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [toggleSidebar])
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed"
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
|
||||
)
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Sidebar({
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right"
|
||||
variant?: "sidebar" | "floating" | "inset"
|
||||
collapsible?: "offcanvas" | "icon" | "none"
|
||||
}) {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground hidden md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
"relative w-(--sidebar-width) bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon)"
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:shadow-sm"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarTrigger({
|
||||
className,
|
||||
onClick,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<Button
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event)
|
||||
toggleSidebar()
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeftIcon />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
const { toggleSidebar } = useSidebar()
|
||||
|
||||
return (
|
||||
<button
|
||||
data-sidebar="rail"
|
||||
data-slot="sidebar-rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
return (
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Input>) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-header"
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-footer"
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Separator>) {
|
||||
return (
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group"
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupLabel({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupAction({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroupContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-item"
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function SidebarMenuButton({
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
isActive?: boolean
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const { isMobile, state } = useSidebar()
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-button"
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!tooltip) {
|
||||
return button
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuAction({
|
||||
className,
|
||||
asChild = false,
|
||||
showOnHover = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & {
|
||||
asChild?: boolean
|
||||
showOnHover?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuBadge({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSkeleton({
|
||||
className,
|
||||
showIcon = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean
|
||||
}) {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-(--skeleton-width) flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="sidebar-menu-sub-item"
|
||||
data-sidebar="menu-sub-item"
|
||||
className={cn("group/menu-sub-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarMenuSubButton({
|
||||
asChild = false,
|
||||
size = "md",
|
||||
isActive = false,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean
|
||||
size?: "sm" | "md"
|
||||
isActive?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-sub-button"
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
}
|
||||
13
src/shared/ui/skeleton.tsx
Normal file
13
src/shared/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
25
src/shared/ui/sonner.tsx
Normal file
25
src/shared/ui/sonner.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "light" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
66
src/shared/ui/tabs.tsx
Normal file
66
src/shared/ui/tabs.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
data-slot="tabs"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
data-slot="tabs-list"
|
||||
className={cn(
|
||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
data-slot="tabs-trigger"
|
||||
className={cn(
|
||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TabsContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
data-slot="tabs-content"
|
||||
className={cn("flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
18
src/shared/ui/textarea.tsx
Normal file
18
src/shared/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
39
src/shared/ui/theme-toggle.tsx
Normal file
39
src/shared/ui/theme-toggle.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Laptop, Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { ToggleGroup, ToggleGroupItem } from "./toggle-group";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<ToggleGroup type="single" className="border">
|
||||
<ToggleGroupItem
|
||||
value="system"
|
||||
className="border-r"
|
||||
aria-label="Toggle system"
|
||||
onClick={() => setTheme("system")}
|
||||
>
|
||||
<Laptop className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="light"
|
||||
aria-label="Toggle light"
|
||||
onClick={() => setTheme("light")}
|
||||
>
|
||||
<Sun className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="dark"
|
||||
className="border-l"
|
||||
aria-label="Toggle dark"
|
||||
onClick={() => setTheme("dark")}
|
||||
>
|
||||
<Moon className="h-4 w-4" />
|
||||
</ToggleGroupItem>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</ToggleGroup>
|
||||
);
|
||||
}
|
||||
73
src/shared/ui/toggle-group.tsx
Normal file
73
src/shared/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
|
||||
import { type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
import { toggleVariants } from "@/shared/ui/toggle"
|
||||
|
||||
const ToggleGroupContext = React.createContext<
|
||||
VariantProps<typeof toggleVariants>
|
||||
>({
|
||||
size: "default",
|
||||
variant: "default",
|
||||
})
|
||||
|
||||
function ToggleGroup({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<ToggleGroupPrimitive.Root
|
||||
data-slot="toggle-group"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={{ variant, size }}>
|
||||
{children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ToggleGroupItem({
|
||||
className,
|
||||
children,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
const context = React.useContext(ToggleGroupContext)
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
data-slot="toggle-group-item"
|
||||
data-variant={context.variant || variant}
|
||||
data-size={context.size || size}
|
||||
className={cn(
|
||||
toggleVariants({
|
||||
variant: context.variant || variant,
|
||||
size: context.size || size,
|
||||
}),
|
||||
"min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem }
|
||||
47
src/shared/ui/toggle.tsx
Normal file
47
src/shared/ui/toggle.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-2 min-w-9",
|
||||
sm: "h-8 px-1.5 min-w-8",
|
||||
lg: "h-10 px-2.5 min-w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Toggle({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
|
||||
VariantProps<typeof toggleVariants>) {
|
||||
return (
|
||||
<TogglePrimitive.Root
|
||||
data-slot="toggle"
|
||||
className={cn(toggleVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toggle, toggleVariants }
|
||||
61
src/shared/ui/tooltip.tsx
Normal file
61
src/shared/ui/tooltip.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/shared/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 0,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Reference in New Issue
Block a user