init create-fias

This commit is contained in:
Samandar Turgunboyev
2025-10-15 17:45:07 +05:00
commit edf364b389
54 changed files with 6664 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
const BASE_URL =
import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com';
const ENDP_POSTS = '/posts/';
export { BASE_URL, ENDP_POSTS };

View File

@@ -0,0 +1,35 @@
import axios from 'axios';
import { BASE_URL } from './URLs';
import i18n from '@/shared/config/i18n';
const httpClient = axios.create({
baseURL: BASE_URL,
timeout: 10000,
});
httpClient.interceptors.request.use(
async (config) => {
console.log(`API REQUEST to ${config.url}`, config);
// Language configs
const language = i18n.language;
config.headers['Accept-Language'] = language;
// const accessToken = localStorage.getItem('accessToken');
// if (accessToken) {
// config.headers['Authorization'] = `Bearer ${accessToken}`;
// }
return config;
},
(error) => Promise.reject(error),
);
httpClient.interceptors.response.use(
(response) => response,
(error) => {
console.error('API error:', error);
return Promise.reject(error);
},
);
export default httpClient;

View File

@@ -0,0 +1,6 @@
export interface TestApiType {
userId: number;
id: number;
title: string;
body: string;
}

View File

@@ -0,0 +1,14 @@
import httpClient from '@/shared/config/api/httpClient';
import type { TestApiType } from '@/shared/config/api/test/test.model';
import type { ReqWithPagination } from '@/shared/config/api/types';
import { ENDP_POSTS } from '@/shared/config/api/URLs';
import type { AxiosResponse } from 'axios';
const getPosts = async (
pagination?: ReqWithPagination,
): Promise<AxiosResponse<TestApiType>> => {
const response = await httpClient.get(ENDP_POSTS, { params: pagination });
return response;
};
export { getPosts };

View File

@@ -0,0 +1,20 @@
export interface ResWithPagination<T> {
success: boolean;
message: string;
links: Links;
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
data: T[];
}
interface Links {
next: number | null;
previous: number | null;
}
export interface ReqWithPagination {
_start?: number;
_limit?: number;
}

View File

@@ -0,0 +1,28 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import uz from './locales/uz/translation.json';
import ki from './locales/ki/translation.json';
import ru from './locales/ru/translation.json';
i18n
.use(LanguageDetector)
.use(initReactI18next)
.init({
resources: {
uz: { translation: uz },
ru: { translation: ru },
ki: { translation: ki },
},
fallbackLng: 'uz',
interpolation: {
escapeValue: false,
},
detection: {
order: ['localStorage', 'navigator'],
caches: ['localStorage'],
},
});
export default i18n;

View File

@@ -0,0 +1,4 @@
{
"welcome": "Kiril. Bizning saytga xush kelibsiz",
"language": "Til"
}

View File

@@ -0,0 +1,4 @@
{
"welcome": "Rus. Bizning saytga xush kelibsiz",
"language": "Til"
}

View File

@@ -0,0 +1,4 @@
{
"welcome": "Uzbek. Bizning saytga xush kelibsiz",
"language": "Til"
}

View File

@@ -0,0 +1,5 @@
export enum LanguageRoutes {
UZ = 'uz', // o'zbekcha
RU = 'ru', // ruscha
KI = 'ki', // kirilcha
}

View File

@@ -0,0 +1,21 @@
const PRODUCT_INFO = {
name: 'FIAS App',
description: 'Generated by create next app',
logo: '/favicon.png',
favicon: '/favicon.svg',
url: 'https://www.shadcnblocks.com',
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: '+998901234567',
email: 'contact@fias.uz',
},
terms_of_use: '',
creator: 'FIAS App',
};
export { PRODUCT_INFO };

View File

@@ -0,0 +1,39 @@
import React, { useEffect } from 'react';
/**
* Hook for closing some items when they are unnecessary to the user
* @param ref For an item that needs to be closed when the outer part is pressed
* @param closeFunction Closing function
* @param scrollClose If it shouldn't close when scrolling, false will be sent. Default true
*/
const useCloser = (
ref: React.RefObject<HTMLElement>,
closeFunction: () => void,
scrollClose: boolean = true,
) => {
useEffect(() => {
// call function when click outside is ref element
function handleClickOutside(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) {
closeFunction();
}
}
// call function when page is scrolling
function handleScroll() {
if (scrollClose) {
closeFunction();
}
}
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('scroll', handleScroll);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('scroll', handleScroll);
};
}, [ref, closeFunction]);
};
export default useCloser;

View File

@@ -0,0 +1,27 @@
import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
/**
* Determine if it's on the current mobile screen (768px)
* @returns boolean
*/
const 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;
};
export default useIsMobile;

View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react';
interface ISize {
width: number | undefined;
height: number | undefined;
}
/**
* Screen size determination
* @returns number
*/
const useWindowSize = () => {
const [size, setSize] = useState<ISize>({
width: undefined,
height: undefined,
});
useEffect(() => {
const getScreenSize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
getScreenSize();
window.addEventListener('resize', getScreenSize);
return () => {
window.removeEventListener('resize', getScreenSize);
};
}, []);
return size;
};
export default useWindowSize;

View File

@@ -0,0 +1,10 @@
/**
* Add base url to url
* @param url Current url
* @returns string
*/
const addBaseUrl = (url: string) => {
return import.meta.env.VITE_API_URL + url;
};
export default addBaseUrl;

View File

@@ -0,0 +1,70 @@
import dayjs from 'dayjs';
import 'dayjs/locale/uz-latn';
import 'dayjs/locale/uz';
import 'dayjs/locale/ru';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import i18n from '@/shared/config/i18n';
// Install Dayjs plugins
dayjs.extend(localizedFormat);
dayjs.extend(relativeTime);
const formatDate = {
/**
* Show date in specified format
* @param time Date object or string or number
* @param format type
* @param locale Language (optional)
* @returns string
*/
to: (
time: Date | string | number,
format: string,
locale?: string,
): string => {
const currentLocale = locale || i18n.language;
return dayjs(time).locale(currentLocale).format(format);
},
/**
* Sync date in specified format (for client-side)
* @param time Date object or string or number
* @param format type
* @param locale Language (optional, standard Uzbek)
* @returns string
*/
format: (
time: Date | string | number,
format: string,
locale: string = 'uz',
): string => {
return dayjs(time).locale(locale).format(format);
},
/**
* Show date in relative time format (today, yesterday, 2 days ago,...)
* @param time Date object or string or number
* @param locale Language (optional, standard Uzbek)
* @returns string
*/
relative: (time: Date | string | number, locale?: string): string => {
const currentLocale = locale || i18n.language;
return dayjs(time).locale(currentLocale).fromNow();
},
/**
* Show relative time synchronously (for client-side)
* @param time Date object or string or number
* @param locale Language (optional, standard Uzbek)
* @returns string
*/
relativeFormat: (
time: Date | string | number,
locale: string = 'uz',
): string => {
return dayjs(time).locale(locale).fromNow();
},
};
export default formatDate;

View File

@@ -0,0 +1,38 @@
/**
* Format the number (+998 00 111-22-33)
* @param value Number to be formatted (XXXYYZZZAABB)
* @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,32 @@
import i18n from '@/shared/config/i18n';
import { LanguageRoutes } from '@/shared/config/i18n/type';
/**
* Format price. With label.
* @param amount Price
* @param withLabel Show label. Default false
* @returns string. Ex. X XXX XXX sum
*/
const formatPrice = (amount: number | string, withLabel?: boolean) => {
const locale = i18n.language;
const label = withLabel
? locale == LanguageRoutes.RU
? ' сум'
: locale == LanguageRoutes.KI
? ' сўм'
: ' som'
: '';
const parts = String(amount).split('.');
const dollars = parts[0];
const cents = parts.length > 1 ? parts[1] : '00';
const formattedDollars = dollars.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
if (String(amount).length == 0) {
return formattedDollars + '.' + cents + label;
} else {
return formattedDollars + label;
}
};
export default formatPrice;

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

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 items-center justify-center gap-2 whitespace-nowrap rounded-md 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 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-4 py-2 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 };

View File

@@ -0,0 +1,255 @@
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,
};