diff --git a/index.html b/index.html index ad17c37..74aa100 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ = 14.18" }, @@ -4417,6 +4424,7 @@ "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4555,6 +4563,7 @@ "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4565,6 +4574,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4626,6 +4636,7 @@ "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.4", "@typescript-eslint/types": "8.46.4", @@ -4885,6 +4896,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4951,6 +4963,7 @@ "integrity": "sha512-sVEPw+J0Gp0IHQabKu8cfdsxlfME0e36Wid7RIaPclGM2OUt+O7O4+6mfAmTUYhy5bDk8cNHzEhPfVtLCIXEJA==", "dev": true, "license": "SEE LICENSE IN LICENSE", + "peer": true, "dependencies": { "@svgdotjs/svg.draggable.js": "^3.0.4", "@svgdotjs/svg.filter.js": "^3.0.8", @@ -5204,6 +5217,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -5939,6 +5953,7 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6784,6 +6799,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4" }, @@ -8217,6 +8233,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8240,6 +8257,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8656,6 +8674,7 @@ "integrity": "sha512-Dqh7SiYcaFtdv5Wvku6QgS5IGPm281L+ZtVD1U2FJa7Q0EFRlq8Z3sjYtz6gYObsYThUOz9ArwFqPZx+1azILQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -9110,6 +9129,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9253,6 +9273,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9410,6 +9431,7 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -9518,6 +9540,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -9783,6 +9806,7 @@ "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..44f7265 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/routes/AppRoutes.tsx b/src/app/routes/AppRoutes.tsx index 2c07a59..d5955e0 100644 --- a/src/app/routes/AppRoutes.tsx +++ b/src/app/routes/AppRoutes.tsx @@ -12,7 +12,7 @@ import { ContactsPage } from '../../features/contacts'; import { RightsPage } from '../../features/rights'; import { CheckoutPage } from '../../features/checkout'; import { ProfilePage } from '../../features/profile'; - +import { CreatorsPage } from '../../features/creators'; import { ROUTES } from '../../shared/config/routes'; export const AppRoutes = () => ( @@ -31,11 +31,12 @@ export const AppRoutes = () => ( } /> } /> + } /> } /> } /> } /> - + } /> }> diff --git a/src/features/auth/AuthSlider/AuthSlider.tsx b/src/features/auth/AuthSlider/AuthSlider.tsx index a43346d..ab16c57 100644 --- a/src/features/auth/AuthSlider/AuthSlider.tsx +++ b/src/features/auth/AuthSlider/AuthSlider.tsx @@ -7,12 +7,14 @@ import './AuthSlider.scss'; import 'swiper/css'; import 'swiper/css/navigation'; import 'swiper/css/pagination'; +import { useTranslation } from 'react-i18next'; type Props = { slides: string[]; }; export const AuthSlider: React.FC = ({ slides }) => { + const { t } = useTranslation('auth') return (
= ({ slides }) => { {`Slide @@ -34,7 +36,7 @@ export const AuthSlider: React.FC = ({ slides }) => { - Back to website + {t("backToWebsite")}
diff --git a/src/features/auth/LoginPage/LoginPage.tsx b/src/features/auth/LoginPage/LoginPage.tsx index 4cbfb39..fd0e6fb 100644 --- a/src/features/auth/LoginPage/LoginPage.tsx +++ b/src/features/auth/LoginPage/LoginPage.tsx @@ -7,9 +7,11 @@ import GoogleIcon from '../../../shared/assets/icons/google.svg?react'; import { LanguageContext } from '../../../shared/context/language'; import { ROUTES } from '../../../shared/config/routes'; import '../form.scss'; +import { useTranslation } from 'react-i18next'; export function LoginPage() { const { language: lng } = useContext(LanguageContext)!; + const { t } = useTranslation('auth'); const navigate = useNavigate(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); @@ -50,12 +52,12 @@ export function LoginPage() { return ( -

Log in your account

+

{t("loginTitle")}

- Don't have an account?{' '} + {t("noAccountPrompt")}{' '} - Create account + {t('createAccountAction')}

@@ -64,7 +66,7 @@ export function LoginPage() { setEmail(e.target.value)} required @@ -72,7 +74,7 @@ export function LoginPage() {
- Please provide a valid email + {t('emailValidationFailed')}
@@ -82,14 +84,14 @@ export function LoginPage() { setPassword(e.target.value)} - placeholder="Password..." + placeholder={t('passwordPlaceholder')} name="password" required />
- Please provide a valid password + {t('passwordValidationFailed')}
@@ -103,16 +105,16 @@ export function LoginPage() { onChange={(e) => setAgreement(e.target.checked)} /> @@ -120,7 +122,7 @@ export function LoginPage() {

- Or login with + {t('orLoginWith')}

@@ -131,7 +133,7 @@ export function LoginPage() { disabled={loading} > - {loading ? 'Logging in...' : 'Google'} + {loading ? t('loggingInStatus') : t('googleLoginOption')}
); diff --git a/src/features/auth/SignupPage/SignupPage.tsx b/src/features/auth/SignupPage/SignupPage.tsx index d05c739..6f48d12 100644 --- a/src/features/auth/SignupPage/SignupPage.tsx +++ b/src/features/auth/SignupPage/SignupPage.tsx @@ -7,9 +7,11 @@ import GoogleIcon from '../../../shared/assets/icons/google.svg?react'; import { LanguageContext } from '../../../shared/context/language'; import { ROUTES } from '../../../shared/config/routes'; import '../form.scss'; +import { useTranslation } from 'react-i18next'; export function SignupPage() { const { language: lng } = useContext(LanguageContext)!; + const { t } = useTranslation('auth'); const navigate = useNavigate(); const [email, setEmail] = useState(''); @@ -51,12 +53,12 @@ export function SignupPage() { return ( -

Create an account

+

{t('createAccountTitle')}

- Already have an account?{' '} + {t('alreadyHaveAccountPrompt')}{' '} - Log in + {t('loginButton')}

@@ -66,7 +68,7 @@ export function SignupPage() { setFirstName(e.target.value)} required @@ -74,7 +76,7 @@ export function SignupPage() {
- Please provide your first name + {t('firstNameValidationFailed')}
@@ -84,7 +86,7 @@ export function SignupPage() { setLastName(e.target.value)} required @@ -92,7 +94,7 @@ export function SignupPage() {
- Please provide your last name + {t('lastNameValidationFailed')}
@@ -103,7 +105,7 @@ export function SignupPage() { setEmail(e.target.value)} required @@ -111,7 +113,7 @@ export function SignupPage() {
- Please provide a valid email + {t('emailValidationFailed')}
@@ -121,14 +123,14 @@ export function SignupPage() { setPassword(e.target.value)} - placeholder="Password..." + placeholder={t('passwordPlaceholder')} name="password" required />
- Please provide a valid password + {t('passwordValidationFailed')}
@@ -142,16 +144,16 @@ export function SignupPage() { onChange={(e) => setAgreement(e.target.checked)} /> @@ -159,7 +161,7 @@ export function SignupPage() {

- Or register with + {t('orRegisterWith')}

@@ -170,7 +172,7 @@ export function SignupPage() { disabled={loading} > - {loading ? 'Logging in...' : 'Google'} + {loading ? t('loggingInStatus') : t('googleLoginOption')}
); diff --git a/src/features/cart/CartPage.tsx b/src/features/cart/CartPage.tsx index 5677d39..4152182 100644 --- a/src/features/cart/CartPage.tsx +++ b/src/features/cart/CartPage.tsx @@ -12,11 +12,13 @@ import { CartInfo } from './components/CartInfo'; import { LanguageContext } from '../../shared/context/language'; import { ROUTES } from '../../shared/config/routes'; import './CartPage.scss'; +import { useTranslation } from 'react-i18next'; const CartPage: React.FC = () => { const navigate = useNavigate(); const { language: lng } = useContext(LanguageContext)!; const { user, loading: authLoading } = useAuth(); + const { t } = useTranslation('cartPage'); const [, setCart] = useState(null); const [cartItems, setCartItems] = useState([]); @@ -74,12 +76,12 @@ const CartPage: React.FC = () => { /> -

Your Cart

+

{t('cartTitle')}

{cartItems.length === 0 ? ( -

Your cart is empty.

+

{t('cartEmptyMessage')}

) : (
    {cartItems.map((item) => ( diff --git a/src/features/cart/components/CartInfo/CartInfo.tsx b/src/features/cart/components/CartInfo/CartInfo.tsx index 3301182..f87b2cc 100644 --- a/src/features/cart/components/CartInfo/CartInfo.tsx +++ b/src/features/cart/components/CartInfo/CartInfo.tsx @@ -9,6 +9,7 @@ import './CartInfo.scss'; import { useCurrency } from '../../../../shared/context/currency'; import { convertPrice } from '../../../../shared/utils'; import type { Currency } from '../../../../widgets/CurrencyButton'; +import { useTranslation } from 'react-i18next'; type Props = { total: number; @@ -19,6 +20,8 @@ export const CartInfo: React.FC = ({ total, itemsCount }) => { const navigate = useNavigate(); const { language: lng } = useContext(LanguageContext)!; const { rates, currentCurrency } = useCurrency(); + const { t } = useTranslation('cartPage'); + const totalFormatted = convertPrice( total, rates, @@ -32,13 +35,15 @@ export const CartInfo: React.FC = ({ total, itemsCount }) => {
    {totalFormatted}
    - Total for {itemsCount} items + {t('totalItemsLabel', { + count: itemsCount, + })}
    - +
    ); }; diff --git a/src/features/cart/components/CartItem/CartItem.tsx b/src/features/cart/components/CartItem/CartItem.tsx index 230ccc9..0d66981 100644 --- a/src/features/cart/components/CartItem/CartItem.tsx +++ b/src/features/cart/components/CartItem/CartItem.tsx @@ -11,6 +11,7 @@ import { useCurrency } from '../../../../shared/context/currency'; import { convertPrice } from '../../../../shared/utils'; import type { Currency } from '../../../../widgets/CurrencyButton'; import { useCartCount } from '../../../../shared/context/cart'; +import { useTranslation } from 'react-i18next'; export type Props = { item: CartItemType; @@ -27,6 +28,7 @@ export const CartItem: React.FC = ({ const { language: lng } = useContext(LanguageContext)!; const { rates, currentCurrency } = useCurrency(); const { decrease: decreaseCart } = useCartCount(); + const { t } = useTranslation('cartPage'); const totalPrice = convertPrice( (item.product?.priceDiscount ?? item.product?.priceRegular ?? 0) * quantity, @@ -101,7 +103,7 @@ export const CartItem: React.FC = ({
    {item.product?.name
    diff --git a/src/features/catalog/CatalogPage.tsx b/src/features/catalog/CatalogPage.tsx index 05b2edf..89f5c89 100644 --- a/src/features/catalog/CatalogPage.tsx +++ b/src/features/catalog/CatalogPage.tsx @@ -7,10 +7,12 @@ import { CategoryLabels } from './types'; import { CatalogList } from './components/CatalogList'; import { PaginationList } from './components/PaginationList'; import './CatalogPage.scss'; +import { useTranslation } from 'react-i18next'; const CatalogPage: React.FC = () => { const { category } = useParams<{ category: Category }>(); const [searchParams, setSearchParams] = useSearchParams(); + const { t } = useTranslation('catalog'); const { products, isLoading, error, page, perPage, total } = useCatalogProducts(category); @@ -28,22 +30,22 @@ const CatalogPage: React.FC = () => {
    -

    {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')}

    = ({ onBackToCatalog }) => {
    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) => ( + +
    + {`Image +
    +
    + ))} +
    {/* 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')}

        404 - +
        ); }; 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 (