This commit is contained in:
2026-04-15 11:19:45 +00:00
commit acb79b2db7
183 changed files with 22067 additions and 0 deletions

View 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
View 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;
};

View 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;
}

View 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;
}

View 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;
};

View 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
View File

@@ -0,0 +1,5 @@
import apiClient from "@/shared/api/apiClient";
export * from "./authSvc"
export {
apiClient
}

View 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;
}

View 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
}

View 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;
}

View 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;
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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 };

View 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": "Цель",
"Ornatish xizmati kerakmi?": "Нужна служба установки?",
"Ha": "Да",
"Yoq": "Нет",
"Yetkazib berish kerakmi": "Вам нужно доставить",
"Yoq 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, Ташкент, Узбекистан"
}

View 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",
"Ornatish xizmati kerakmi?": "Ornatish xizmati kerakmi?",
"Ha": "Ha",
"Yoq": "Yoq",
"Yetkazib berish kerakmi": "Yetkazib berish kerakmi",
"Yoq o'zim olib ketaman": "Yoq 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 kochasi, 1-uy, Toshkent, Ozbekiston"
}

View 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);

View 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,
};
});

View 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,
});

View File

@@ -0,0 +1,4 @@
export enum LanguageRoutes {
UZ = "uz", // o'zbekcha
RU = "ru", // ruscha
}

View File

@@ -0,0 +1,7 @@
import {golosText} from "@/shared/config/fonts";
import {routing} from "@/shared/config/i18n/routing";
export {
golosText,
routing
}

View 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>
}

View 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"

View 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}

View 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
}

View 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,
},
]
}
]

View 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
}
}

View 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
}

View 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,
};
}

View File

@@ -0,0 +1,5 @@
const formatNumberWithSpaces = (number: number | string = "") => {
return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
}
export default formatNumberWithSpaces

View 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;

View 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
View 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))
}

View 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;

View 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;

View File

@@ -0,0 +1,2 @@
export * from "./PrivateRouteProvider"
export * from "./QueryProvider"

View 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',
}
)
);

View 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
View 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
View 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
View File

View File

@@ -0,0 +1 @@
export type Locale = "ru" | "uz"

10
src/shared/types/pagination.d.ts vendored Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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[]
}

View 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
View 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
View 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
View 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
View 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,
}

View 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>
)
}

View 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
View 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,
}

View 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,
}

View 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
View 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
View 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
View 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

View 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;

View 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,
}

View 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,
}

View 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;

View 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

View 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
View 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,
}

View 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
View 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
View 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,
}

View 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
View 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
View 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 }

View 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 }

View 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>
);
}

View 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
View 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
View 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 }