-
{CategoryLabels[category || Category.PHONES]}
+
{t(CategoryLabels[category || Category.PHONES])}
{total}
- models
+ {t('models')}
- {isLoading &&
Loading products...
}
+ {isLoading &&
{t('loading')}
}
- {!isLoading && error &&
{error}
}
+ {!isLoading && error &&
{t('error')}
}
{!isLoading && !error && (
<>
diff --git a/src/features/catalog/components/CatalogFilter/CatalogFilter.tsx b/src/features/catalog/components/CatalogFilter/CatalogFilter.tsx
index 1c458fc..e5ac271 100644
--- a/src/features/catalog/components/CatalogFilter/CatalogFilter.tsx
+++ b/src/features/catalog/components/CatalogFilter/CatalogFilter.tsx
@@ -3,14 +3,18 @@ import './CatalogFilter.scss';
import { Dropdown } from '../../../../shared/ui/Dropdown';
import { ProductItemsPerPage, ProductSortTypes } from './type';
import { useSearchParams } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
type DropdownItem = number | string;
export const CatalogFilter: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
+ const { t, i18n } = useTranslation('catalog');
const defaultProductLabel =
- searchParams.get('sort') ?? Object.values(ProductSortTypes)[0];
+ searchParams.get('sort') ?? ProductSortTypes.NAME_ASC;
+ const defaultSortLabel = t(`sort.${defaultProductLabel}`);
+
const defaultItemsPerPage = ProductItemsPerPage.THIRTY;
const onItemsPerPageSelect = (item: DropdownItem) => {
@@ -36,14 +40,16 @@ export const CatalogFilter: React.FC = () => {
return (
t(`sort.${sort}`))}
onSelect={onSortTypeSelect}
- defaultValue={defaultProductLabel}
+ defaultValue={defaultSortLabel}
+ key={i18n.language}
/>
= {
- [Category.PHONES]: 'Mobile phones',
- [Category.TABLETS]: 'Tablets',
- [Category.Accessory]: 'Accessrories',
+ [Category.PHONES]: 'categories.phones',
+ [Category.TABLETS]: 'categories.tablets',
+ [Category.Accessory]: 'categories.accessories'
};
diff --git a/src/features/checkout/CheckoutPage.tsx b/src/features/checkout/CheckoutPage.tsx
index 4bd2133..85a9f57 100644
--- a/src/features/checkout/CheckoutPage.tsx
+++ b/src/features/checkout/CheckoutPage.tsx
@@ -94,6 +94,16 @@ export const CheckoutPage: React.FC = () => {
}
};
+ const handleRemoveItem = (id: string) => {
+ setCartItems(prev => {
+ const updated = prev.filter(item => item.id !== id);
+ setCartCount(updated.length); // ← ось цього не вистачало
+ return updated;
+ });
+ };
+
+
+
// Redirect if user is not logged in
useEffect(() => {
if (!authLoading && !user) navigate('/auth');
@@ -128,6 +138,7 @@ export const CheckoutPage: React.FC = () => {
cartItems={cartItems}
noProducts={!cartItems.length || !!error}
isLoading={cartLoading}
+ onRemoveItem={handleRemoveItem}
onQuantityChange={handleQuantityChange}
/>
diff --git a/src/features/checkout/components/CheckoutForm/CheckoutForm.tsx b/src/features/checkout/components/CheckoutForm/CheckoutForm.tsx
index e50daac..1590b2a 100644
--- a/src/features/checkout/components/CheckoutForm/CheckoutForm.tsx
+++ b/src/features/checkout/components/CheckoutForm/CheckoutForm.tsx
@@ -6,12 +6,8 @@ import './CheckoutForm.scss';
import { PaymentMethod } from '../../../../services/order';
import RadioGroupComponent from '../../../../shared/ui/RadioGroup/RadioGroup';
import type { User } from 'firebase/auth';
+import { useTranslation } from 'react-i18next';
-const paymentMethodOptions = [
- { label: 'Online Payment', value: PaymentMethod.ONLINE_PAYMENT },
- { label: 'Cash on Delivery', value: PaymentMethod.CASH_ON_DELIVERY },
- { label: 'POS on Delivery', value: PaymentMethod.POS_ON_DELIVERY },
-];
type Props = {
user: User | null;
@@ -35,6 +31,13 @@ export const CheckoutForm = forwardRef
(
const [email, setEmail] = useState(user?.email || '');
const [mobile, setMobile] = useState(user?.phoneNumber || '');
const [isScheduled, setIsScheduled] = useState(true);
+ const { t } = useTranslation('checkoutForm');
+
+ const paymentMethodOptions = [
+ { label: t('payment.online') , value: PaymentMethod.ONLINE_PAYMENT },
+ { label: t('payment.cash'), value: PaymentMethod.CASH_ON_DELIVERY },
+ { label: t('payment.pos'), value: PaymentMethod.POS_ON_DELIVERY },
+ ];
return (
@@ -42,16 +45,16 @@ export const CheckoutForm = forwardRef(
setFirstName(event.target.value)}
/>
setLastName(event.target.value)}
/>
@@ -60,17 +63,17 @@ export const CheckoutForm = forwardRef(
setEmail(event.target.value)}
/>
(
-
+
-
-
+
+
@@ -114,13 +131,13 @@ export const CheckoutForm = forwardRef(
@@ -128,8 +145,8 @@ export const CheckoutForm = forwardRef(
(
- Payment Method
+ {t('payment.method')}
= ({
}) => {
const [usePromo, setUsePromo] = useState(false);
const { rates, currentCurrency } = useCurrency();
+ const { t } = useTranslation('checkoutInfo');
const formattedTotal = convertPrice(
totalAmount,
@@ -68,22 +70,22 @@ export const CheckoutInfo: React.FC = ({
return (
- Order Summary
+ {t('order.summary')}
-
- Subtotal
+ - {t('order.subtotal')}
- {formattedTotal}
-
- Shipping
+ - {t('order.shipping')}
- --
{formattedDiscount && (
-
- Discount
+ - {t('order.discount')}
- {formattedDiscount}
)}
@@ -91,14 +93,14 @@ export const CheckoutInfo: React.FC = ({
-
- Total (USD)
+ - {t('order.total')} (USD)
- {formattedFinalTotal}
@@ -110,12 +112,12 @@ export const CheckoutInfo: React.FC
= ({
@@ -125,7 +127,7 @@ export const CheckoutInfo: React.FC = ({
{paymentMethod === PaymentMethod.ONLINE_PAYMENT ? (
) : (
-
+
)}
);
diff --git a/src/features/checkout/components/CheckoutList/CheckoutList.tsx b/src/features/checkout/components/CheckoutList/CheckoutList.tsx
index 0793201..d4ea83c 100644
--- a/src/features/checkout/components/CheckoutList/CheckoutList.tsx
+++ b/src/features/checkout/components/CheckoutList/CheckoutList.tsx
@@ -4,11 +4,13 @@ import { CartItem } from '../../../cart/components/CartItem';
import './CheckoutList.scss';
import { Spinner } from '../../../../shared/ui/Spinner';
import { CurrencyRatesChart } from '../CurrencyRatesChart';
+import { useTranslation } from 'react-i18next';
type Props = {
cartItems: CartItemType[];
noProducts?: boolean;
isLoading: boolean;
+ onRemoveItem?: (id: string) => void;
onQuantityChange?: (id: string, qty: number) => void;
};
@@ -17,7 +19,9 @@ export const CheckoutList: React.FC = ({
isLoading,
noProducts = false,
onQuantityChange,
+ onRemoveItem,
}) => {
+ const { t } = useTranslation('checkoutList');
return (
{isLoading &&
}
@@ -26,7 +30,11 @@ export const CheckoutList: React.FC
= ({
{cartItems.map((item) => (
-
-
+ onRemoveItem?.(item.id)}
+ />
))}
@@ -34,7 +42,7 @@ export const CheckoutList: React.FC = ({
{!isLoading && noProducts && (
- It seems like you have no products in your cart yet
+ {t('noProducts')}
)}
diff --git a/src/features/checkout/components/CurrencyRatesChart/CurrencyRatesChart.tsx b/src/features/checkout/components/CurrencyRatesChart/CurrencyRatesChart.tsx
index d7e0213..a3eb3ad 100644
--- a/src/features/checkout/components/CurrencyRatesChart/CurrencyRatesChart.tsx
+++ b/src/features/checkout/components/CurrencyRatesChart/CurrencyRatesChart.tsx
@@ -2,9 +2,11 @@ import React from 'react';
import ReactApexChart from 'react-apexcharts';
import { useCurrency } from '../../../../shared/context/currency';
import './CurrencyRatesChart.scss';
+import { useTranslation } from 'react-i18next';
export const CurrencyRatesChart: React.FC = () => {
const { rates, loading, error } = useCurrency();
+ const { t } = useTranslation('currencyRatesChart');
if (loading) return Loading currency rates...
;
if (error) return Error: {error}
;
@@ -42,7 +44,7 @@ export const CurrencyRatesChart: React.FC = () => {
return (
-
Currency Exchange Rates
+
{t('currencyRates')}
void;
};
export const SuccessAnimation: React.FC = ({ onBackToCatalog }) => {
+ const { t } = useTranslation('successAnimation');
+
useEffect(() => {
startCatAnimation();
}, []);
return (
-
Thanks for you purchase
+
{t('gratitude')}
diff --git a/src/features/contacts/ContactsPage.tsx b/src/features/contacts/ContactsPage.tsx
index 88f0449..600d686 100644
--- a/src/features/contacts/ContactsPage.tsx
+++ b/src/features/contacts/ContactsPage.tsx
@@ -4,8 +4,11 @@ import * as Form from '@radix-ui/react-form';
import { Button } from '../../shared/ui/Button';
import { FormInput } from '../../shared/ui/FormInput';
import emailjs from 'emailjs-com';
+import { useTranslation } from 'react-i18next';
export const ContactsPage: React.FC = () => {
+ const { t } = useTranslation('contactsPage');
+
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
@@ -51,21 +54,21 @@ export const ContactsPage: React.FC = () => {
return (
-
Get in Touch
-
You can reach us anytime
+
{t('getInTouch')}
+
{t('reachUs')}
{
{
-
+
- By contacting us, you agree to our
+ {t('agreeWarning')}
- Terms of service
+ {t('termsOfService')}
- and
+ {t('and')}
- Privacy Policy
+ {t('privacyPolicy')}
.
diff --git a/src/features/creators/CreatorsPage.scss b/src/features/creators/CreatorsPage.scss
new file mode 100644
index 0000000..d5aea67
--- /dev/null
+++ b/src/features/creators/CreatorsPage.scss
@@ -0,0 +1,16 @@
+@use 'sass:map';
+@use '../../shared/styles/vars' as *;
+@use '../../shared/styles/mixins' as *;
+
+.creators {
+ display: flex;
+ flex-direction: column;
+ gap: map.get($spacing, 'xl');
+ padding-block: map.get($spacing, 'xl');
+
+ &__list {
+ display: flex;
+ flex-direction: column;
+ gap: map.get($spacing, '4xl');
+ }
+}
diff --git a/src/features/creators/CreatorsPage.tsx b/src/features/creators/CreatorsPage.tsx
new file mode 100644
index 0000000..ec76fe5
--- /dev/null
+++ b/src/features/creators/CreatorsPage.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import './CreatorsPage.scss';
+import { creators } from './data';
+import { CreatorCard } from './components/CreatorCard';
+import type { CreatorInfo } from './types';
+
+const shuffleArray = (array: CreatorInfo[]) => {
+ const shuffledArray = [...array];
+
+ for (let i = shuffledArray.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
+ }
+ return shuffledArray;
+};
+
+export const CreatorsPage: React.FC = () => {
+ const shuffledCreators = shuffleArray(creators);
+
+ return (
+
+
+ {shuffledCreators.map((creator) => (
+ -
+
+
+ ))}
+
+
+ );
+};
diff --git a/src/features/creators/components/CreatorCard/CreatorCard.scss b/src/features/creators/components/CreatorCard/CreatorCard.scss
new file mode 100644
index 0000000..2984d01
--- /dev/null
+++ b/src/features/creators/components/CreatorCard/CreatorCard.scss
@@ -0,0 +1,51 @@
+@use 'sass:map';
+@use '../../../../shared/styles/vars' as *;
+@use '../../../../shared/styles/mixins' as *;
+
+.creator-info {
+ display: flex;
+ flex-direction: column;
+ gap: map.get($spacing, '2xl');
+
+ @include on-desktop {
+ flex-direction: row;
+ align-items: center;
+ }
+
+ &__info {
+ display: flex;
+ flex-direction: column;
+ gap: map.get($spacing, 'md');
+ }
+
+ &__images {
+ max-width: 100%;
+
+ @include on-desktop {
+ max-width: 350px;
+ }
+ }
+
+ &__role {
+ color: map.get($colors, 'red');
+ }
+
+ &__name,
+ &__role {
+ text-transform: capitalize;
+ }
+
+ &__social-network-list {
+ display: flex;
+ align-items: center;
+ gap: map.get($spacing, 'md');
+
+ svg {
+ width: 25px;
+
+ path {
+ fill: var(--text-color);
+ }
+ }
+ }
+}
diff --git a/src/features/creators/components/CreatorCard/CreatorCard.tsx b/src/features/creators/components/CreatorCard/CreatorCard.tsx
new file mode 100644
index 0000000..080bd07
--- /dev/null
+++ b/src/features/creators/components/CreatorCard/CreatorCard.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { SocialNetworkIcons, type CreatorInfo } from '../../types';
+import './CreatorCard.scss';
+import { CreatorImageSlider } from '../CreatorImageSlider';
+import { useTranslation } from 'react-i18next';
+
+type Props = {
+ creator: CreatorInfo;
+};
+
+export const CreatorCard: React.FC = ({ creator }) => {
+ const { t } = useTranslation('creators');
+ return (
+
+
+
+
+
+
+
{t(`${creator.key}.name`)}
+
{t(`${creator.key}.role`)}
+
+ {t(`${creator.key}.description`)}
+
+
+
+
+
+ );
+};
diff --git a/src/features/creators/components/CreatorCard/index.ts b/src/features/creators/components/CreatorCard/index.ts
new file mode 100644
index 0000000..12ae38d
--- /dev/null
+++ b/src/features/creators/components/CreatorCard/index.ts
@@ -0,0 +1 @@
+export * from './CreatorCard';
diff --git a/src/features/creators/components/CreatorImageSlider/CreatorImageSlider.scss b/src/features/creators/components/CreatorImageSlider/CreatorImageSlider.scss
new file mode 100644
index 0000000..2ca717d
--- /dev/null
+++ b/src/features/creators/components/CreatorImageSlider/CreatorImageSlider.scss
@@ -0,0 +1,54 @@
+@use 'sass:map';
+@use '../../../../shared/styles/vars' as *;
+@use '../../../../shared/styles/mixins' as *;
+
+.creator-slider {
+ &__image-wrapper {
+ position: relative;
+ border-radius: 6px;
+ overflow: hidden;
+ width: 100%;
+ height: 700px;
+
+ @include on-desktop {
+ max-width: 350px;
+ max-height: 400px;
+ height: 100%;
+ }
+
+ img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center;
+ }
+ }
+
+ .swiper-slide {
+ height: 100%;
+
+ @include on-desktop {
+ height: 400px;
+ }
+ }
+
+ .swiper-pagination {
+ bottom: 12px;
+
+ &-bullet {
+ width: 10px;
+ height: 10px;
+ background: map.get($colors, 'white');
+ opacity: 1;
+ border-radius: 4px;
+ margin: 0 6px;
+ transition: 0.3s;
+
+ &-active {
+ background: map.get($colors, 'red');
+ width: 24px;
+ border-radius: 4px;
+ }
+ }
+ }
+}
diff --git a/src/features/creators/components/CreatorImageSlider/CreatorImageSlider.tsx b/src/features/creators/components/CreatorImageSlider/CreatorImageSlider.tsx
new file mode 100644
index 0000000..3e6f56f
--- /dev/null
+++ b/src/features/creators/components/CreatorImageSlider/CreatorImageSlider.tsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import type { CreatorInfo } from '../../types';
+import { Swiper, SwiperSlide } from 'swiper/react';
+import { Pagination, Autoplay } from 'swiper/modules';
+import 'swiper/css';
+import 'swiper/css/navigation';
+import 'swiper/css/pagination';
+import './CreatorImageSlider.scss';
+
+type Props = {
+ images: CreatorInfo['images'];
+};
+
+export const CreatorImageSlider: React.FC = ({ images }) => {
+ return (
+
+
+ {images.map((image, index) => (
+
+
+

+
+
+ ))}
+ {/* Bullet pagination */}
+
+
+ );
+};
diff --git a/src/features/creators/components/CreatorImageSlider/index.ts b/src/features/creators/components/CreatorImageSlider/index.ts
new file mode 100644
index 0000000..b10d020
--- /dev/null
+++ b/src/features/creators/components/CreatorImageSlider/index.ts
@@ -0,0 +1 @@
+export * from './CreatorImageSlider';
diff --git a/src/features/creators/data.ts b/src/features/creators/data.ts
new file mode 100644
index 0000000..7959c12
--- /dev/null
+++ b/src/features/creators/data.ts
@@ -0,0 +1,38 @@
+import { SocialNetworkEnum, type CreatorInfo } from './types';
+import nhr_1 from '../../shared/assets/img/nhr_1.webp';
+import nhr_2 from '../../shared/assets/img/nhr_2.webp';
+
+export const creators: CreatorInfo[] = [
+ {
+ key: 'nhr',
+ images: [nhr_1, nhr_2],
+ socialNetworks: [
+ {
+ link: 'https://www.linkedin.com/in/nazariy-holovach-a6a30738a/',
+ icon: SocialNetworkEnum.LINKEDIN,
+ },
+ {
+ link: 'https://github.com/Aiiyuu',
+ icon: SocialNetworkEnum.GITHUB,
+ },
+ {
+ link: 'https://t.me/nazariyholovach',
+ icon: SocialNetworkEnum.TELEGRAM,
+ },
+ ],
+ },
+ {
+ key: 'nhr2',
+ images: [nhr_2, nhr_1],
+ socialNetworks: [
+ {
+ link: '',
+ icon: SocialNetworkEnum.LINKEDIN,
+ },
+ {
+ link: '',
+ icon: SocialNetworkEnum.GITHUB,
+ },
+ ],
+ },
+];
diff --git a/src/features/creators/index.ts b/src/features/creators/index.ts
new file mode 100644
index 0000000..87be7e0
--- /dev/null
+++ b/src/features/creators/index.ts
@@ -0,0 +1,2 @@
+export * from './CreatorsPage';
+export * from './types';
diff --git a/src/features/creators/types.tsx b/src/features/creators/types.tsx
new file mode 100644
index 0000000..787d04b
--- /dev/null
+++ b/src/features/creators/types.tsx
@@ -0,0 +1,35 @@
+import Linkedin from '../../shared/assets/icons/linkedin.svg?react';
+import Telegram from '../../shared/assets/icons/telegram.svg?react';
+import Github from '../../shared/assets/icons/github.svg?react';
+import Instagram from '../../shared/assets/icons/instagram.svg?react';
+import Facebook from '../../shared/assets/icons/facebook.svg?react';
+import Spotify from '../../shared/assets/icons/spotify.svg?react';
+
+export interface CreatorInfo {
+ key: string;
+ images: string[];
+ socialNetworks: SocialNetwork[];
+}
+
+export interface SocialNetwork {
+ link: string;
+ icon: SocialNetworkEnum;
+}
+
+export enum SocialNetworkEnum {
+ LINKEDIN = 'linkedin',
+ TELEGRAM = 'telegram',
+ GITHUB = 'github',
+ INSTAGRAM = 'instagram',
+ FACEBOOK = 'facebook',
+ SPOTIFY = 'spotify',
+}
+
+export const SocialNetworkIcons: Record = {
+ [SocialNetworkEnum.LINKEDIN]: ,
+ [SocialNetworkEnum.TELEGRAM]: ,
+ [SocialNetworkEnum.GITHUB]: ,
+ [SocialNetworkEnum.INSTAGRAM]: ,
+ [SocialNetworkEnum.FACEBOOK]: ,
+ [SocialNetworkEnum.SPOTIFY]: ,
+};
diff --git a/src/features/favorite/FavoritePage.tsx b/src/features/favorite/FavoritePage.tsx
index d0521a4..c2549e6 100644
--- a/src/features/favorite/FavoritePage.tsx
+++ b/src/features/favorite/FavoritePage.tsx
@@ -8,11 +8,13 @@ import './FavoritePage.scss';
import { ProductCard } from '../../widgets/ProductCard';
import { LanguageContext } from '../../shared/context/language';
import { ROUTES } from '../../shared/config/routes';
+import { useTranslation } from 'react-i18next';
const FavoritePage: React.FC = () => {
const navigate = useNavigate();
const { language: lng } = useContext(LanguageContext)!;
const { user, loading: authLoading } = useAuth();
+ const { t } = useTranslation('favoritePage');
const [favorites, setFavorites] = useState([]);
const [isLoading, setIsLoading] = useState(false);
@@ -51,22 +53,24 @@ const FavoritePage: React.FC = () => {
-
Favorites
+
{t('favoritesTitle')}
- {isLoading &&
Loading favorites...
}
- {error &&
{error}
}
+ {isLoading &&
{t('loadingFavorites')}
}
+ {error &&
{t('error', { error })}
}
- {!isLoading && favorites.length === 0 && (
-
You have no favorite items yet.
- )}
+ {!isLoading && favorites.length === 0 &&
{t('noFavorites')}
}
{!isLoading && favorites.length > 0 && (
<>
-
{favorites.length} items
+
+ {t('itemCount', { count: favorites.length })}
+
{favorites.map((fav) => (
-
diff --git a/src/features/home/HomePage.tsx b/src/features/home/HomePage.tsx
index 14856cc..1dd239f 100644
--- a/src/features/home/HomePage.tsx
+++ b/src/features/home/HomePage.tsx
@@ -10,6 +10,8 @@ import CategoryList from './components/CategoryList/CategoryList';
import './HomePage.scss';
import ImageSlider from './components/ImageSlider/ImageSlider';
import { useAllProducts } from '../../shared/hooks';
+import { useTranslation } from 'react-i18next';
+import { SlideIn } from '../../shared/animation/SlideIn';
const HomePage: React.FC = () => {
const { products, isLoading } = useAllProducts();
@@ -18,36 +20,51 @@ const HomePage: React.FC = () => {
const premium = getPremiumProducts(products);
const popular = getPopularProducts(products);
const random = getRandomProducts(products);
+ const { t } = useTranslation('homePage');
return (
-
Welcome to Nice Gadgets store!
-
-
-
-
-
-
-
-
-
-
+
{t('welcomeMessage')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
diff --git a/src/features/home/components/CategoryItem/CategoryItem.tsx b/src/features/home/components/CategoryItem/CategoryItem.tsx
index 61854ac..48ce512 100644
--- a/src/features/home/components/CategoryItem/CategoryItem.tsx
+++ b/src/features/home/components/CategoryItem/CategoryItem.tsx
@@ -1,5 +1,6 @@
import React from 'react';
import './CategoryItem.scss';
+import { useTranslation } from 'react-i18next';
type Props = {
categoryImg: string;
@@ -12,6 +13,8 @@ const CategoryItem: React.FC = ({
categoryQuantity,
categoryName,
}) => {
+ const { t } = useTranslation('homePage');
+
return (
@@ -22,7 +25,7 @@ const CategoryItem: React.FC
= ({
/>
- {categoryName}
+ {t(categoryName)}
{categoryQuantity} models
diff --git a/src/features/home/components/CategoryList/CategoryList.tsx b/src/features/home/components/CategoryList/CategoryList.tsx
index 850bad8..cc7a8bd 100644
--- a/src/features/home/components/CategoryList/CategoryList.tsx
+++ b/src/features/home/components/CategoryList/CategoryList.tsx
@@ -11,7 +11,11 @@ import type { Category } from '../../../../services/product';
import { Link } from 'react-router-dom';
import { useLanguage } from '../../../../shared/context/language';
-const CategoryList: React.FC = () => {
+interface CategoryListProps {
+ sectionTitle: string;
+}
+
+const CategoryList: React.FC = ({ sectionTitle }) => {
const [categories, setCategories] = useState<
{ category: string; numberOfModels: number }[]
>([]);
@@ -46,7 +50,7 @@ const CategoryList: React.FC = () => {
return (
-
Shop by category
+
{sectionTitle}
{categories.map((category) => (
-
diff --git a/src/features/not-found/NotFound.tsx b/src/features/not-found/NotFound.tsx
index dfcb968..e23a71c 100644
--- a/src/features/not-found/NotFound.tsx
+++ b/src/features/not-found/NotFound.tsx
@@ -3,19 +3,21 @@ import { Button } from '../../shared/ui/Button';
import { useNavigate } from 'react-router-dom';
import GIF from '../../shared/assets/img/404.gif';
import './NotFound.scss';
+import { useTranslation } from 'react-i18next';
const NotFound: React.FC = () => {
const navigate = useNavigate();
+ const { t } = useTranslation('notFound');
return (
404
- Looks like you're lost
- The page you are looking for not available
+ {t('404_title')}
+ {t('404_message')}
-
+
);
};
diff --git a/src/services/currency/currency.service.ts b/src/services/currency/currency.service.ts
index eb0845d..9705f9f 100644
--- a/src/services/currency/currency.service.ts
+++ b/src/services/currency/currency.service.ts
@@ -1,7 +1,7 @@
import { Currency } from '../../widgets/CurrencyButton';
import type { Rate } from './currency.types';
-const API_KEY = 'fca_live_ehEwkhH6bDrcA31n5iwVMQDqTwFjMZ6NpGHxVtwL';
+const API_KEY = 'fca_live_wJ0Ifv6oTAfdDMi1xbwTF6iVBtXUxvtRK21B2XAg';
const BASE_URL = `https://api.freecurrencyapi.com/v1/latest?apikey=${API_KEY}`;
async function getCashRates(
diff --git a/src/shared/animation/SlideIn/SlideIn.tsx b/src/shared/animation/SlideIn/SlideIn.tsx
new file mode 100644
index 0000000..940072c
--- /dev/null
+++ b/src/shared/animation/SlideIn/SlideIn.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { motion } from 'framer-motion';
+
+type Props = {
+ children: React.ReactNode;
+ beforeAnimationState?: {
+ opacity?: number;
+ y?: number;
+ x?: number;
+ delay?: number;
+ duration?: number;
+ };
+};
+
+export const SlideIn: React.FC = ({
+ children,
+ beforeAnimationState,
+}) => {
+ const {
+ x = 0,
+ y = 30,
+ opacity = 0.7,
+ delay = 0,
+ duration = 0.3,
+ } = beforeAnimationState || {};
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/shared/animation/SlideIn/index.ts b/src/shared/animation/SlideIn/index.ts
new file mode 100644
index 0000000..c296c68
--- /dev/null
+++ b/src/shared/animation/SlideIn/index.ts
@@ -0,0 +1 @@
+export * from './SlideIn';
diff --git a/src/shared/assets/icons/facebook.svg b/src/shared/assets/icons/facebook.svg
new file mode 100644
index 0000000..7e383c1
--- /dev/null
+++ b/src/shared/assets/icons/facebook.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/github.svg b/src/shared/assets/icons/github.svg
new file mode 100644
index 0000000..9e7f627
--- /dev/null
+++ b/src/shared/assets/icons/github.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/shared/assets/icons/instagram.svg b/src/shared/assets/icons/instagram.svg
new file mode 100644
index 0000000..edb86ef
--- /dev/null
+++ b/src/shared/assets/icons/instagram.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/shared/assets/icons/linkedin.svg b/src/shared/assets/icons/linkedin.svg
new file mode 100644
index 0000000..e391bd7
--- /dev/null
+++ b/src/shared/assets/icons/linkedin.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/shared/assets/icons/spotify.svg b/src/shared/assets/icons/spotify.svg
new file mode 100644
index 0000000..60781bd
--- /dev/null
+++ b/src/shared/assets/icons/spotify.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/shared/assets/icons/telegram.svg b/src/shared/assets/icons/telegram.svg
new file mode 100644
index 0000000..9a4b4d5
--- /dev/null
+++ b/src/shared/assets/icons/telegram.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/shared/assets/img/nhr_1.webp b/src/shared/assets/img/nhr_1.webp
new file mode 100644
index 0000000..c743b58
Binary files /dev/null and b/src/shared/assets/img/nhr_1.webp differ
diff --git a/src/shared/assets/img/nhr_2.webp b/src/shared/assets/img/nhr_2.webp
new file mode 100644
index 0000000..cb83c40
Binary files /dev/null and b/src/shared/assets/img/nhr_2.webp differ
diff --git a/src/shared/config/routes/routes.ts b/src/shared/config/routes/routes.ts
index 9d3bb06..9c2ae04 100644
--- a/src/shared/config/routes/routes.ts
+++ b/src/shared/config/routes/routes.ts
@@ -9,6 +9,7 @@ export const ROUTES = {
cart: 'cart',
favorite: 'favorite',
contacts: 'contacts',
+ creators: 'creators',
rights: 'rights',
checkout: 'checkout',
profile: 'profile',
diff --git a/src/shared/i18n/languages/en/auth.json b/src/shared/i18n/languages/en/auth.json
new file mode 100644
index 0000000..909b0fc
--- /dev/null
+++ b/src/shared/i18n/languages/en/auth.json
@@ -0,0 +1,24 @@
+{
+ "loginTitle": "Log in your account",
+ "noAccountPrompt": "Don't have an account?",
+ "createAccountAction": "Create account",
+ "emailPlaceholder": "Email...",
+ "emailValidationFailed": "Please provide a valid email",
+ "passwordPlaceholder": "Password...",
+ "passwordValidationFailed": "Please provide a valid password",
+ "iAgreeCheckbox": "I agree",
+ "termsAndConditions": "Terms & Conditions",
+ "loginButton": "Log in",
+ "orLoginWith": "Or login with",
+ "orRegisterWith": "Or register with",
+ "loggingInStatus": "Logging in...",
+ "googleLoginOption": "Google",
+ "slideIndex": "Slide",
+ "backToWebsite": "Back to website",
+ "createAccountTitle": "Create an account",
+ "alreadyHaveAccountPrompt": "Already have an account?",
+ "firstNamePlaceholder": "First name...",
+ "firstNameValidationFailed": "Please provide your first name",
+ "lastNamePlaceholder": "Last name...",
+ "lastNameValidationFailed": "Please provide your last name"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/cartPage.json b/src/shared/i18n/languages/en/cartPage.json
new file mode 100644
index 0000000..e9873d9
--- /dev/null
+++ b/src/shared/i18n/languages/en/cartPage.json
@@ -0,0 +1,8 @@
+{
+ "cartEmptyMessage": "Your cart is empty..",
+ "totalItemsLabel_one": "Total for {{count}} item",
+ "totalItemsLabel_other": "Total for {{count}} items",
+ "checkoutAction": "Checkout",
+ "productImageAlt": "Product image",
+ "cartTitle": "Your Cart"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/catalog.json b/src/shared/i18n/languages/en/catalog.json
new file mode 100644
index 0000000..94ee4e8
--- /dev/null
+++ b/src/shared/i18n/languages/en/catalog.json
@@ -0,0 +1,25 @@
+{
+ "categories": {
+ "phones": "Mobile phones",
+ "tablets": "Tablets",
+ "accessories": "Accessories"
+ },
+
+ "models": "models",
+ "loading": "Loading products...",
+ "error": "Something went wrong",
+
+ "filters": {
+ "sortBy": "Sort by",
+ "itemsPerPage": "Items on page"
+ },
+
+ "sort": {
+ "name_asc": "Name (A - Z)",
+ "name_desc": "Name (Z - A)",
+ "price_asc": "Price (Low - High)",
+ "price_desc": "Price (High - Low)",
+ "discount_asc": "Discount (Low - High)",
+ "discount_desc": "Discount (High - Low)"
+ }
+}
diff --git a/src/shared/i18n/languages/en/checkoutForm.json b/src/shared/i18n/languages/en/checkoutForm.json
new file mode 100644
index 0000000..b32114f
--- /dev/null
+++ b/src/shared/i18n/languages/en/checkoutForm.json
@@ -0,0 +1,28 @@
+{
+ "payment": {
+ "method": "Payment Method",
+ "online": "Online Payment",
+ "cash": "Cash on Delivery",
+ "pos": "POS on Delivery"
+ },
+
+ "checkoutForm": {
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "email": "Email",
+ "mobile": "Mobile Number",
+ "country": "Country",
+ "city": "City",
+ "address": "Address",
+ "zip": "ZIP",
+ "state": "State"
+ },
+
+ "schedule": {
+ "delivery": "Schedule Delivery",
+ "date": "Delivery Date",
+ "time": "Delivery Time",
+ "notes": "Delivery Notes",
+ "notesForCourier": "Notes for the courier"
+ }
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/checkoutInfo.json b/src/shared/i18n/languages/en/checkoutInfo.json
new file mode 100644
index 0000000..9b18faf
--- /dev/null
+++ b/src/shared/i18n/languages/en/checkoutInfo.json
@@ -0,0 +1,17 @@
+{
+ "order": {
+ "summary": "Order Summary",
+ "subtotal": "Subtotal",
+ "shipping": "Shipping",
+ "discount": "Discount",
+ "total": "Total"
+ },
+
+ "promo": {
+ "promocode": "Promocode",
+ "usePromo": "Use Promocode",
+ "apply": "Apply Promocode"
+ },
+
+ "confirmOrder": "Confirm Order"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/checkoutList.json b/src/shared/i18n/languages/en/checkoutList.json
new file mode 100644
index 0000000..40e8c98
--- /dev/null
+++ b/src/shared/i18n/languages/en/checkoutList.json
@@ -0,0 +1,3 @@
+{
+ "noProducts": "It seems like you have no products in your cart yet"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/contactsPage.json b/src/shared/i18n/languages/en/contactsPage.json
new file mode 100644
index 0000000..a70b13c
--- /dev/null
+++ b/src/shared/i18n/languages/en/contactsPage.json
@@ -0,0 +1,21 @@
+{
+ "getInTouch": "Get in Touch",
+ "reachUs": "You can reach us anytime",
+
+ "placeholder": {
+ "firstName": "First name",
+ "lastName": "Last name",
+ "email": "Your email",
+ "subject": "Subject",
+ "message": "Tell us what we can help you with..."
+ },
+
+ "submit": "Submit",
+ "agreeWarning": "By contacting us, you agree to our",
+ "termsOfService": "Terms of service",
+ "and": "and",
+ "privacyPolicy": "Privacy Policy",
+
+ "isRequired": "is required",
+ "validEmail": "Please enter a valid email"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/creators.json b/src/shared/i18n/languages/en/creators.json
new file mode 100644
index 0000000..c8e3af7
--- /dev/null
+++ b/src/shared/i18n/languages/en/creators.json
@@ -0,0 +1,7 @@
+{
+ "nhr": {
+ "name": "Holovach Nazariy",
+ "role": "Project-Manager",
+ "description": "idk so far"
+ }
+}
diff --git a/src/shared/i18n/languages/en/currencyRatesChart.json b/src/shared/i18n/languages/en/currencyRatesChart.json
new file mode 100644
index 0000000..df9a2aa
--- /dev/null
+++ b/src/shared/i18n/languages/en/currencyRatesChart.json
@@ -0,0 +1,5 @@
+{
+ "currencyRates": "Currency Exchange Rates",
+ "loadingCurrencyRates": "Loading currency rates...",
+ "error": "Error: "
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/favoritePage.json b/src/shared/i18n/languages/en/favoritePage.json
new file mode 100644
index 0000000..688d640
--- /dev/null
+++ b/src/shared/i18n/languages/en/favoritePage.json
@@ -0,0 +1,8 @@
+{
+ "favoritesTitle": "Favorites",
+ "loadingFavorites": "Loading favorites...",
+ "noFavorites": "You have no favorite items yet.",
+ "itemCount_one": "{{count}} item",
+ "itemCount_other": "{{count}} items",
+ "error": "Failed to load favorites: {{error}}"
+}
diff --git a/src/shared/i18n/languages/en/footer.json b/src/shared/i18n/languages/en/footer.json
new file mode 100644
index 0000000..2e1ff92
--- /dev/null
+++ b/src/shared/i18n/languages/en/footer.json
@@ -0,0 +1,8 @@
+{
+ "footerLinks": {
+ "contacts": "CONTACTS",
+ "rights": "RIGHTS"
+ },
+
+ "topButton": "Back to top"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/homePage.json b/src/shared/i18n/languages/en/homePage.json
new file mode 100644
index 0000000..3203617
--- /dev/null
+++ b/src/shared/i18n/languages/en/homePage.json
@@ -0,0 +1,11 @@
+{
+ "welcomeMessage": "Welcome to Nice Gadgets store!",
+ "brandNewModels": "Brand new models",
+ "shopByCategory": "Shop by category",
+ "hotPrices": "Hot prices",
+ "premiumDevices": "Premium devices",
+ "popularDevices": "Popular devices",
+ "categories": {
+ "phones": "asdfsdf"
+ }
+}
diff --git a/src/shared/i18n/languages/en/notFound.json b/src/shared/i18n/languages/en/notFound.json
new file mode 100644
index 0000000..50235f2
--- /dev/null
+++ b/src/shared/i18n/languages/en/notFound.json
@@ -0,0 +1,5 @@
+{
+ "404_title": "Looks like you're lost",
+ "404_message": "The page you are looking for not available",
+ "404_goHomeAction": "Go to home"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/productCard.json b/src/shared/i18n/languages/en/productCard.json
new file mode 100644
index 0000000..bf7db66
--- /dev/null
+++ b/src/shared/i18n/languages/en/productCard.json
@@ -0,0 +1,8 @@
+{
+ "addedConfirmation": "Added",
+ "addToCartAction": "Add to cart",
+ "specificationScreen": "Screen",
+ "specificationCapacity": "Capacity",
+ "specificationRAM": "RAM",
+ "productImage": "Product image"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/en/successAnimation.json b/src/shared/i18n/languages/en/successAnimation.json
new file mode 100644
index 0000000..d2dd561
--- /dev/null
+++ b/src/shared/i18n/languages/en/successAnimation.json
@@ -0,0 +1,4 @@
+{
+ "gratitude": "Thanks for your purchase",
+ "trackOrder": "Track Order"
+}
diff --git a/src/shared/i18n/languages/ua/auth.json b/src/shared/i18n/languages/ua/auth.json
new file mode 100644
index 0000000..5753d69
--- /dev/null
+++ b/src/shared/i18n/languages/ua/auth.json
@@ -0,0 +1,24 @@
+{
+ "loginTitle": "Увійти до акаунту",
+ "noAccountPrompt": "Не маєте акаунту?",
+ "createAccountAction": "Створити акаунт",
+ "emailPlaceholder": "Електронна пошта...",
+ "emailValidationFailed": "Будь ласка, вкажіть дійсну адресу електронної пошти",
+ "passwordPlaceholder": "Пароль...",
+ "passwordValidationFailed": "Будь ласка, вкажіть дійсний пароль",
+ "iAgreeCheckbox": "Я погоджуюсь",
+ "termsAndConditions": "Умови використання",
+ "loginButton": "Увійти",
+ "orLoginWith": "Або увійти за допомогою",
+ "orRegisterWith": "Або зареєструйтесь за допомогою",
+ "loggingInStatus": "Виконується вхід...",
+ "googleLoginOption": "Google",
+ "slideIndex": "Слайд",
+ "backToWebsite": "Повернутися на сайт",
+ "createAccountTitle": "Створити акаунт",
+ "alreadyHaveAccountPrompt": "Вже маєте акаунт?",
+ "firstNamePlaceholder": "Ім'я...",
+ "firstNameValidationFailed": "Будь ласка, вкажіть ваше ім'я",
+ "lastNamePlaceholder": "Прізвище...",
+ "lastNameValidationFailed": "Будь ласка, вкажіть ваше прізвище"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/ua/cartPage.json b/src/shared/i18n/languages/ua/cartPage.json
new file mode 100644
index 0000000..3f7e083
--- /dev/null
+++ b/src/shared/i18n/languages/ua/cartPage.json
@@ -0,0 +1,8 @@
+{
+ "cartEmptyMessage": "Ваш кошик порожній.",
+ "totalItemsLabel_one": "Загальна сума для {{count}} позиції",
+ "totalItemsLabel_other": "Загальна сума для {{count}} позицій",
+ "checkoutAction": "Оформити замовлення",
+ "productImageAlt": "Зображення продукту",
+ "cartTitle": "Ваш кошик"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/ua/catalog.json b/src/shared/i18n/languages/ua/catalog.json
new file mode 100644
index 0000000..4a00869
--- /dev/null
+++ b/src/shared/i18n/languages/ua/catalog.json
@@ -0,0 +1,25 @@
+{
+ "categories": {
+ "phones": "Мобільні телефони",
+ "tablets": "Планшети",
+ "accessories": "Аксесуари"
+ },
+
+ "models": "модел(-і /-ей)",
+ "loading": "Завантаження товарів...",
+ "error": "Сталася помилка",
+
+ "filters": {
+ "sortBy": "Сортувати за",
+ "itemsPerPage": "Кількість на сторінці"
+ },
+
+ "sort": {
+ "name_asc": "Назвою (А - Я)",
+ "name_desc": "Назвою (Я - А)",
+ "price_asc": "Ціною (зростання)",
+ "price_desc": "Ціною (спадання)",
+ "discount_asc": "Знижкою (менша - більша)",
+ "discount_desc": "Знижкою (більша - менша)"
+ }
+}
diff --git a/src/shared/i18n/languages/ua/checkoutForm.json b/src/shared/i18n/languages/ua/checkoutForm.json
new file mode 100644
index 0000000..fe5144a
--- /dev/null
+++ b/src/shared/i18n/languages/ua/checkoutForm.json
@@ -0,0 +1,28 @@
+{
+ "payment": {
+ "method": "Спосіб оплати",
+ "online": "Оплата онлайн",
+ "cash": "Готівка при отриманні",
+ "pos": "Оплата карткою при отриманні"
+ },
+
+ "checkoutForm": {
+ "firstName": "Ім’я",
+ "lastName": "Прізвище",
+ "email": "Email",
+ "mobile": "Номер телефону",
+ "country": "Країна",
+ "city": "Місто",
+ "address": "Адреса",
+ "zip": "Індекс",
+ "state": "Область"
+ },
+
+ "schedule": {
+ "delivery": "Запланувати доставку",
+ "date": "Дата доставки",
+ "time": "Час доставки",
+ "notes": "Примітки до доставки",
+ "notesForCourier": "Примітки для кур’єра"
+ }
+}
diff --git a/src/shared/i18n/languages/ua/checkoutInfo.json b/src/shared/i18n/languages/ua/checkoutInfo.json
new file mode 100644
index 0000000..b249e03
--- /dev/null
+++ b/src/shared/i18n/languages/ua/checkoutInfo.json
@@ -0,0 +1,17 @@
+{
+ "order": {
+ "summary": "Підсумок замовлення",
+ "subtotal": "Проміжний підсумок",
+ "shipping": "Доставка",
+ "discount": "Знижка",
+ "total": "Разом"
+ },
+
+ "promo": {
+ "promocode": "Промокод",
+ "usePromo": "Використати промокод",
+ "apply": "Застосувати промокод"
+ },
+
+ "confirmOrder": "Підтвердити замовлення"
+}
diff --git a/src/shared/i18n/languages/ua/checkoutList.json b/src/shared/i18n/languages/ua/checkoutList.json
new file mode 100644
index 0000000..a56fd91
--- /dev/null
+++ b/src/shared/i18n/languages/ua/checkoutList.json
@@ -0,0 +1,3 @@
+{
+ "noProducts": "Здається, у вашому кошику ще немає товарів"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/ua/contactsPage.json b/src/shared/i18n/languages/ua/contactsPage.json
new file mode 100644
index 0000000..6683167
--- /dev/null
+++ b/src/shared/i18n/languages/ua/contactsPage.json
@@ -0,0 +1,21 @@
+{
+ "getInTouch": "Зв’яжіться з нами",
+ "reachUs": "Ви можете звертатися до нас у будь-який час",
+
+ "placeholder": {
+ "firstName": "Ім’я",
+ "lastName": "Прізвище",
+ "email": "Ваш email",
+ "subject": "Тема",
+ "message": "Опишіть, чим ми можемо вам допомогти..."
+ },
+
+ "submit": "Надіслати",
+ "agreeWarning": "Звертаючись до нас, ви погоджуєтесь з нашими",
+ "termsOfService": "Умовами користування",
+ "and": "та",
+ "privacyPolicy": "Політикою конфіденційності",
+
+ "isRequired": "- обов’язково",
+ "validEmail": "Будь ласка, введіть коректний email"
+}
diff --git a/src/shared/i18n/languages/ua/creators.json b/src/shared/i18n/languages/ua/creators.json
new file mode 100644
index 0000000..2a59c59
--- /dev/null
+++ b/src/shared/i18n/languages/ua/creators.json
@@ -0,0 +1,7 @@
+{
+ "nhr": {
+ "name": "Головач Назар",
+ "role": "Проджек-Менеджер",
+ "description": "idk so far"
+ }
+}
diff --git a/src/shared/i18n/languages/ua/currencyRatesChart.json b/src/shared/i18n/languages/ua/currencyRatesChart.json
new file mode 100644
index 0000000..d05f6bf
--- /dev/null
+++ b/src/shared/i18n/languages/ua/currencyRatesChart.json
@@ -0,0 +1,5 @@
+{
+ "currencyRates": "Курси валют",
+ "loadingCurrencyRates": "Завантаження курсів валют...",
+ "error": "Помилка: "
+}
diff --git a/src/shared/i18n/languages/ua/favoritePage.json b/src/shared/i18n/languages/ua/favoritePage.json
new file mode 100644
index 0000000..b648724
--- /dev/null
+++ b/src/shared/i18n/languages/ua/favoritePage.json
@@ -0,0 +1,9 @@
+{
+ "favoritesTitle": "Вибрані товари",
+ "loadingFavorites": "Завантаження улюблених товарів...",
+ "noFavorites": "У вас ще немає улюблених товарів.",
+ "itemCount_one": "{{count}} товар",
+ "itemCount_few": "{{count}} товари",
+ "itemCount_many": "{{count}} товарів",
+ "error": "Не вдалося завантажити улюблені товари: {{error}}"
+}
diff --git a/src/shared/i18n/languages/ua/footer.json b/src/shared/i18n/languages/ua/footer.json
new file mode 100644
index 0000000..044d07a
--- /dev/null
+++ b/src/shared/i18n/languages/ua/footer.json
@@ -0,0 +1,8 @@
+{
+ "footerLinks": {
+ "contacts": "КОНТАКТИ",
+ "rights": "ПРАВА"
+ },
+
+ "topButton": "Повернутись нагору"
+}
diff --git a/src/shared/i18n/languages/ua/homePage.json b/src/shared/i18n/languages/ua/homePage.json
new file mode 100644
index 0000000..bcda512
--- /dev/null
+++ b/src/shared/i18n/languages/ua/homePage.json
@@ -0,0 +1,8 @@
+{
+ "welcomeMessage": "Ласкаво просимо до магазину Nice Gadgets!",
+ "brandNewModels": "Нові моделі",
+ "shopByCategory": "Купуй за категоріями",
+ "hotPrices": "Гарячі ціни",
+ "premiumDevices": "Преміум пристрої",
+ "popularDevices": "Популярні пристрої"
+}
diff --git a/src/shared/i18n/languages/ua/notFound.json b/src/shared/i18n/languages/ua/notFound.json
new file mode 100644
index 0000000..beed5b5
--- /dev/null
+++ b/src/shared/i18n/languages/ua/notFound.json
@@ -0,0 +1,5 @@
+{
+ "404_title": "Схоже, ви заблукали",
+ "404_message": "Сторінка, яку ви шукаєте, недоступна",
+ "404_goHomeAction": "Перейти на головну"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/ua/productCard.json b/src/shared/i18n/languages/ua/productCard.json
new file mode 100644
index 0000000..553c953
--- /dev/null
+++ b/src/shared/i18n/languages/ua/productCard.json
@@ -0,0 +1,8 @@
+{
+ "addedConfirmation": "Додано",
+ "addToCartAction": "Додати в кошик",
+ "specificationScreen": "Екран",
+ "specificationCapacity": "Ємність",
+ "specificationRAM": "Оперативна пам'ять",
+ "productImage": "Зображення продукту"
+}
\ No newline at end of file
diff --git a/src/shared/i18n/languages/ua/successAnimation.json b/src/shared/i18n/languages/ua/successAnimation.json
new file mode 100644
index 0000000..2ef92a0
--- /dev/null
+++ b/src/shared/i18n/languages/ua/successAnimation.json
@@ -0,0 +1,4 @@
+{
+ "gratitude": "Дякуємо за вашу покупку",
+ "trackOrder": "Відстежити замовлення"
+}
diff --git a/src/shared/ui/Dropdown/Dropdown.tsx b/src/shared/ui/Dropdown/Dropdown.tsx
index d6e850d..0739442 100644
--- a/src/shared/ui/Dropdown/Dropdown.tsx
+++ b/src/shared/ui/Dropdown/Dropdown.tsx
@@ -10,6 +10,7 @@ type Item = string | number;
type Props = {
labelValue: string;
dropdownItems: Item[];
+ dropdownItemsLabels?: string[];
defaultValue?: Item;
onSelect: (item: Item) => void;
};
@@ -17,6 +18,7 @@ type Props = {
export const Dropdown: React.FC = ({
labelValue,
dropdownItems,
+ dropdownItemsLabels = dropdownItems,
defaultValue = dropdownItems[0],
onSelect,
}) => {
@@ -24,7 +26,11 @@ export const Dropdown: React.FC = ({
const [selectedItem, setSelectedItem] = useState(defaultValue);
const handleSelection = (item: Item) => {
- setSelectedItem(item);
+ const index = dropdownItems.findIndex(
+ (currentItem) => currentItem === item,
+ );
+ const label = dropdownItemsLabels[index] ?? String(item);
+ setSelectedItem(label);
onSelect(item);
};
@@ -41,14 +47,14 @@ export const Dropdown: React.FC = ({
- {dropdownItems.map((item) => {
+ {dropdownItems.map((item, index) => {
return (
handleSelection(item)}
>
- {item}
+ {dropdownItemsLabels.at(index)}
);
})}
diff --git a/src/shared/ui/FormInput/FormInput.tsx b/src/shared/ui/FormInput/FormInput.tsx
index 663df79..0e91c33 100644
--- a/src/shared/ui/FormInput/FormInput.tsx
+++ b/src/shared/ui/FormInput/FormInput.tsx
@@ -2,6 +2,7 @@ import React from 'react';
import * as Form from '@radix-ui/react-form';
import cn from 'classnames';
import './FormInput.scss';
+import { useTranslation } from 'react-i18next';
interface FormInputProps {
name: string;
@@ -30,6 +31,7 @@ export const FormInput: React.FC = ({
disabled = false,
onChange,
}) => {
+ const { t } = useTranslation('contactsPage');
return (
= ({
- {placeholder} is required
+ {placeholder} {t('isRequired')}
{type === 'email' && (
- Please enter a valid email
+ {t('validEmail')}
)}
diff --git a/src/widgets/CardLoader/CardLoader.scss b/src/widgets/CardLoader/CardLoader.scss
new file mode 100644
index 0000000..c2c0160
--- /dev/null
+++ b/src/widgets/CardLoader/CardLoader.scss
@@ -0,0 +1,126 @@
+@use '../../shared/styles/mixins' as *;
+@use '../../shared/styles/vars' as *;
+@use 'sass:map';
+
+/* ================================
+ Skeleton animation (custom)
+================================ */
+@keyframes skeleton-loading {
+ 0% {
+ background-position: 200% 0;
+ }
+ 100% {
+ background-position: -200% 0;
+ }
+}
+
+
+/* ==========================================================
+ CORE FIX — override Radix Skeleton ONLY inside the loader
+========================================================== */
+.product-card--loader .rt-Skeleton {
+ background: linear-gradient(
+ 90deg,
+ #dcdcdc 25%,
+ #e8e8e8 37%,
+ #dcdcdc 63%
+ ) !important;
+
+ background-size: 400% 100% !important;
+ animation: skeleton-loading 1.4s ease infinite !important;
+ border-radius: 8px !important;
+ display: block;
+}
+
+/* ===============================================
+ BASE WRAPPER
+=============================================== */
+.product-card--loader {
+ width: 100%;
+ padding: 32px;
+ background: var(--bg-color-lighter);
+ border: $border;
+ display: flex;
+ flex-direction: column;
+ gap: map.get($spacing, 'sm');
+}
+
+/* ===============================================
+ IMAGE
+=============================================== */
+.product-card__loader-image {
+ width: 100%;
+ height: 300px;
+ border-radius: 12px;
+
+ @include on-tablet {
+ height: 300px;
+ }
+
+ @include on-desktop {
+ height: 200px;
+ }
+}
+
+/* ===============================================
+ TITLE
+=============================================== */
+.product-card__loader-title {
+ width: 70%;
+ height: 20px;
+ margin: 12px 0;
+ border-radius: 6px;
+}
+
+/* ===============================================
+ PRICE
+=============================================== */
+.product-card__loader-price-current {
+ width: 40%;
+ height: 18px;
+ border-radius: 4px;
+}
+
+.product-card__loader-price-old {
+ width: 25%;
+ height: 16px;
+ margin-left: 10px;
+ border-radius: 4px;
+}
+
+/* ===============================================
+ SPECS
+=============================================== */
+.product-card__loader-spec {
+ width: 60%;
+ height: 14px;
+ margin: 6px 0;
+ border-radius: 4px;
+
+ @include on-tablet {
+ width: 55%;
+ }
+
+ @include on-desktop {
+ width: 50%;
+ }
+}
+
+/* ===============================================
+ BUTTONS
+=============================================== */
+.product-card__loader-button {
+ width: 70%;
+ height: 40px;
+ border-radius: 8px;
+
+ @include on-desktop {
+ width: 65%;
+ }
+}
+
+.product-card__loader-favorite {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+}
diff --git a/src/widgets/CardLoader/CardLoader.tsx b/src/widgets/CardLoader/CardLoader.tsx
new file mode 100644
index 0000000..86733ff
--- /dev/null
+++ b/src/widgets/CardLoader/CardLoader.tsx
@@ -0,0 +1,36 @@
+import { Skeleton } from "@radix-ui/themes";
+import "./CardLoader.scss";
+
+export const CardLoader = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/widgets/CardLoader/index.ts b/src/widgets/CardLoader/index.ts
new file mode 100644
index 0000000..8b69554
--- /dev/null
+++ b/src/widgets/CardLoader/index.ts
@@ -0,0 +1 @@
+export * from './CardLoader';
\ No newline at end of file
diff --git a/src/widgets/Footer/Footer.tsx b/src/widgets/Footer/Footer.tsx
index 83ae9df..b652189 100644
--- a/src/widgets/Footer/Footer.tsx
+++ b/src/widgets/Footer/Footer.tsx
@@ -5,9 +5,11 @@ import Logo from '../../shared/assets/Logo.svg?react';
import { ROUTES } from '../../shared/config/routes';
import { LanguageContext } from '../../shared/context/language';
import { ArrowButton } from '../../shared/ui/ArrowButton';
+import { useTranslation } from 'react-i18next';
export const Footer: React.FC = () => {
const { language: lng } = useContext(LanguageContext)!;
+ const { t } = useTranslation('footer');
return (