create hamburger menu and responsive header

This commit is contained in:
zahravaziri
2025-06-02 18:36:20 +03:30
parent 919364a4dd
commit a5a287f11f
15 changed files with 501 additions and 241 deletions
-142
View File
@@ -2203,15 +2203,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true "dev": true
}, },
"node_modules/cross-fetch": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz",
"integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==",
"license": "MIT",
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3420,63 +3411,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
"dependencies": {
"void-elements": "3.1.0"
}
},
"node_modules/i18next": {
"version": "25.2.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.2.1.tgz",
"integrity": "sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==",
"funding": [
{
"type": "individual",
"url": "https://locize.com"
},
{
"type": "individual",
"url": "https://locize.com/i18next.html"
},
{
"type": "individual",
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.27.1"
},
"peerDependencies": {
"typescript": "^5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/i18next-browser-languagedetector": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.1.0.tgz",
"integrity": "sha512-mHZxNx1Lq09xt5kCauZ/4bsXOEA2pfpwSoU11/QTJB+pD94iONFwp+ohqi///PwiFvjFOxe1akYCdHyFo1ng5Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}
},
"node_modules/i18next-http-backend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz",
"integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==",
"license": "MIT",
"dependencies": {
"cross-fetch": "4.0.0"
}
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.3.2", "version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4529,26 +4463,6 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -4991,32 +4905,6 @@
"react": "^19.1.0" "react": "^19.1.0"
} }
}, },
"node_modules/react-i18next": {
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz",
"integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",
"html-parse-stringify": "^3.0.1"
},
"peerDependencies": {
"i18next": ">= 23.2.3",
"react": ">= 16.8.0",
"typescript": "^5"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": { "node_modules/react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -5704,12 +5592,6 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -5903,30 +5785,6 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/void-elements": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+3
View File
@@ -1,6 +1,9 @@
export default [ export default [
{ {
title: 'instagram', title: 'instagram',
link: 'https://www.instagram.com/yourprofile', link: 'https://www.instagram.com/yourprofile',
icon: 'instagram',
}, },
]; ];
+2 -6
View File
@@ -1,5 +1,6 @@
import { appConstants } from '@/assets/constants'; import { appConstants } from '@/assets/constants';
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import { Icon, IconName } from '@/components/uikit/icons';
import { t } from '@/locales/translates'; import { t } from '@/locales/translates';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
@@ -31,12 +32,7 @@ export default function FooterInfo({
key={social.title} key={social.title}
className="text-white lg:flex-1" className="text-white lg:flex-1"
> >
{/* <Image <Icon name={social.icon as IconName} className="text-primary" />
alt={social.title}
src={images[social.title]}
className="h-6 w-6 object-contain"
/> */}
{social.title}
</Link> </Link>
))} ))}
</div> </div>
@@ -0,0 +1,91 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import images from '@/assets/images/images';
import { usePathname } from 'next/navigation';
import { Icon } from '@/components/uikit/icons';
import { t } from '@/locales/translates';
import { useEffect, useRef } from 'react';
import FooterInfo from '../footer/FooterInfo';
import routeFactory from '@/assets/constants/routeFactory';
interface Props {
isOpen: boolean;
onClose: () => void;
}
const links = [routeFactory.home, routeFactory.about, routeFactory.contact];
export default function HamburgerMenu({ isOpen, onClose }: Props) {
const pathname = usePathname();
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
onClose();
}
}
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.getElementsByTagName('main')[0].style.overflow = 'hidden';
} else {
document.getElementsByTagName('main')[0].style.overflow = 'auto';
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="fixed inset-x-0 top-0 z-50 flex h-screen">
<div className="absolute inset-0 bg-black/60" />
<div
ref={menuRef}
className="relative z-10 flex h-full w-80 max-w-full flex-col bg-black p-5 text-white"
>
<div className="mb-14 flex items-center justify-between">
<Link href="/" className="h-12 w-auto">
<Image
alt="logo"
src={images.logo}
className="h-full w-auto object-contain"
/>
</Link>
<button onClick={onClose}>
<Icon name="close" className="h-6 w-6 text-white" />
</button>
</div>
<nav className="flex flex-col gap-6">
{links.map(({ route: link, title }) => {
const isActive = pathname === link();
return (
<Link
key={link()}
href={link()}
onClick={onClose}
className={`rounded-lg px-4 py-3 text-center text-sm font-medium ${
isActive
? 'bg-primary text-white'
: 'border border-white/10 text-white hover:bg-white/10'
}`}
>
{title as string}
</Link>
);
})}
</nav>
<div className="mt-auto flex flex-col items-center pt-10 text-xs text-gray-400">
<FooterInfo className="text-center" />
</div>
</div>
</div>
);
}
+25 -19
View File
@@ -1,47 +1,53 @@
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import Navbar from '../navbar'; import Navbar from '../navbar';
import Button from '@/components/uikit/button'; import Button, { IconButton } from '@/components/uikit/button';
import { t } from '@/locales/translates'; import { t } from '@/locales/translates';
import { Icon } from '@/components/uikit/icons';
export default function Header() { export default function Header() {
const bannerFooter = [
t('com_home_Banner_link1') as string,
t('com_home_Banner_link2') as string,
t('com_home_Banner_link3') as string,
];
return ( return (
<header <header
className=" bg-cover text-white h-screen relative bg-no-repeat bg-right w-full" className="relative w-full bg-cover bg-right bg-no-repeat text-white md:h-screen"
style={{ backgroundImage: `url(${images.homeBanner.src})` }} style={{ backgroundImage: `url(${images.homeBanner.src})` }}
> >
<div className="absolute inset-0 bg-black opacity-50 z-0 blur-[100px]" /> <div className="absolute inset-0 z-0 bg-black opacity-50 blur-[100px]" />
<Navbar /> <Navbar />
<div className="container mx-auto bg-cover bg-center text-white flex justify-between items-center flex-col relative z-10 h-[80%] py-20"> <div className="relative z-10 container mx-auto flex h-[80%] flex-col items-center bg-cover bg-center pb-6 text-white md:justify-between md:py-20">
<div className=" w-full flex flex-col justify-start "> <div className="flex w-full flex-col justify-start">
<h1 className="text-7xl md:text-6xl font-light"> <h1 className="text-xl font-light md:text-5xl lg:text-7xl">
{t('com_home_banner_title') as string} {t('com_home_banner_title') as string}
</h1> </h1>
<h1 className="text-7xl md:text-6xl font-bold"> <h1 className="text-xl font-bold md:text-5xl lg:text-7xl">
{t('com_home_banner_title_bold') as string} {t('com_home_banner_title_bold') as string}
</h1> </h1>
<p <p
className="mb-8 mt-5 text-base font-normal" className="mt-2 mb-8 text-xs font-normal md:mt-5 md:text-sm lg:text-base"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: t('com_home_banner_description') as string, __html: t('com_home_banner_description') as string,
}} }}
/> />
<div> <div>
<Button variant="solid" endIcon={'x'}> <Button endIcon={'x'}>
{t('com_home_banner_button') as string} {t('com_home_banner_button') as string}
</Button> </Button>
</div> </div>
</div> </div>
<div className="w-full flex justify-center gap-8 text-sm border-t-[1px] border-white/10 pt-12 "> <div className="text-nase flex w-full flex-col justify-center gap-8 border-t-0 border-white/10 pt-6 md:flex-row md:border-t-[1px] md:pt-12">
<a href="#" className="hover:underline"> {bannerFooter.map((item, index) => (
{t('com_home_Banner_link1') as string} <div
</a> key={index}
<a href="#" className="hover:underline"> className="flex items-center gap-2 hover:underline"
{t('com_home_Banner_link2') as string} >
</a> <Icon name="achievement" className="text-primary" />
<a href="#" className="hover:underline"> {item}
{t('com_home_Banner_link3') as string} </div>
</a> ))}
</div> </div>
</div> </div>
</header> </header>
+112 -30
View File
@@ -1,11 +1,99 @@
// 'use client';
// import images from '@/assets/images/images';
// import { Icon } from '@/components/uikit/icons';
// import { t } from '@/locales/translates';
// import Image from 'next/image';
// import Link from 'next/link';
// import { usePathname } from 'next/navigation';
// import { useState } from 'react';
// export default function Navbar() {
// const [open, setOpen] = useState(false);
// const pathname = usePathname();
// const links = [
// { href: '/', label: t('pages_home_title') as string },
// { href: '/about', label: t('pages_about_title') as string },
// { href: '/blog', label: t('pages_contact_title') as string },
// { href: '/contact', label: t('pages_contact_title') as string },
// { href: '/product', label: t('pages_product_title') as string },
// { href: '/project', label: t('pages_project_title') as string },
// ];
// const linkClass = (href: string) =>
// pathname === href ? ' text-red-500 font-semibold' : 'text-white';
// return (
// <nav className="relative z-10 container mx-auto shrink-0 bg-transparent pt-7 pb-3 md:py-11">
// <div className="flex items-center justify-between border-b-0 border-white/10 pb-6 md:border-b-[1px]">
// <Link href="/" className="h-12 w-auto">
// <Image
// alt="logo"
// src={images.logo}
// className="h-full w-auto object-contain"
// />
// </Link>
// <ul className="hidden space-x-6 text-sm md:flex lg:text-base">
// {links.map(({ href, label }) => (
// <li key={href}>
// <Link
// href={href}
// className={`${linkClass(href)} hover:text-primary-light`}
// >
// {label}
// </Link>
// </li>
// ))}
// <li>
// <a className="flex items-center gap-2" href="tel:+098123456789">
// <Icon name="phone" className="text-primary" />
// <p>+098 123456789</p>
// </a>
// </li>
// </ul>
// <button
// className="text-gray-700 md:hidden"
// onClick={() => setOpen(!open)}
// >
// {open ? (
// <Icon name="close" className="text-white" />
// ) : (
// <Icon name="menu" className="text-white" />
// )}
// </button>
// </div>
// {open && (
// <ul className="mt-2 space-y-2 px-2 md:hidden">
// {links.map(({ href, label }) => (
// <li key={href}>
// <Link
// href={href}
// className={linkClass(href)}
// onClick={() => setOpen(false)}
// >
// {label}
// </Link>
// </li>
// ))}
// </ul>
// )}
// </nav>
// );
// }
'use client'; 'use client';
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import { t } from '@/locales/translates'; import { Icon } from '@/components/uikit/icons';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import HamburgerMenu from '../hamburgerMenu';
import { t } from '@/locales/translates';
export default function Navbar() { export default function Navbar() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -19,58 +107,52 @@ export default function Navbar() {
{ href: '/product', label: t('pages_product_title') as string }, { href: '/product', label: t('pages_product_title') as string },
{ href: '/project', label: t('pages_project_title') as string }, { href: '/project', label: t('pages_project_title') as string },
]; ];
const linkClass = (href: string) => const linkClass = (href: string) =>
pathname === href ? ' text-red-500 font-semibold' : 'text-white'; pathname === href ? 'text-primary font-semibold' : 'text-white';
return ( return (
<nav className="bg-transparent container mx-auto py-11 shrink-0 relative z-10 "> <>
<div className=" flex items-center justify-between"> <nav className="relative z-50 container mx-auto bg-transparent pt-7 pb-3 md:py-11">
<Link href="/" className="text-xl font-bold w-auto h-12 "> <div className="flex items-center justify-between border-b-0 border-white/10 pb-6 md:border-b">
<Image alt="logo" src={images.logo} className="w-auto h-full object-contain" /> <Link href="/" className="h-12 w-auto">
<Image
alt="logo"
src={images.logo}
className="h-full w-auto object-contain"
/>
</Link> </Link>
<ul className="hidden md:flex space-x-6">
<ul className="hidden space-x-6 text-sm md:flex lg:text-base">
{links.map(({ href, label }) => ( {links.map(({ href, label }) => (
<li key={href}> <li key={href}>
<Link <Link
href={href} href={href}
className={`${linkClass(href)} hover:text-primary-light `} className={`${linkClass(href)} hover:text-primary-light`}
> >
{label} {label}
</Link> </Link>
</li> </li>
))} ))}
<li> <li>
<a className="flex gap-2 items-center" href="tel:+098123456789"> <a className="flex items-center gap-2" href="tel:+098123456789">
<span>xx</span> <Icon name="phone" className="text-primary" />
<p>+098 123456789</p> <p>+098 123456789</p>
</a> </a>
</li> </li>
</ul> </ul>
{!open && (
<button <button
className="md:hidden text-gray-700" className="text-white md:hidden"
onClick={() => setOpen(!open)} onClick={() => setOpen(true)}
> >
{open ? <p>x</p> : <p>menu</p>} <Icon name="menu" className="h-6 w-6" />
</button> </button>
</div>
{open && (
<ul className="md:hidden mt-2 space-y-2 px-2">
{links.map(({ href, label }) => (
<li key={href}>
<Link
href={href}
className={linkClass(href)}
onClick={() => setOpen(false)}
>
{label}
</Link>
</li>
))}
</ul>
)} )}
</div>
</nav> </nav>
<HamburgerMenu isOpen={open} onClose={() => setOpen(false)} />
</>
); );
} }
+14
View File
@@ -0,0 +1,14 @@
export interface IBaseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
loading?: boolean;
disabled?: boolean;
className?: string
}
export interface ITextButtonProps extends IBaseButtonProps{
endIcon?: ReactNode;
}
export interface IIconButtonProps extends IBaseButtonProps{
color?: 'primary' | 'secondary'
size?: 'small' | 'medium' | 'large'
}
+60 -22
View File
@@ -1,36 +1,24 @@
'use client'; 'use client';
import React, { ButtonHTMLAttributes, ReactNode } from 'react'; import React from 'react';
import { IIconButtonProps, ITextButtonProps } from './button';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { const Button: React.FC<ITextButtonProps> = ({
children?: ReactNode;
loading?: boolean;
disabled?: boolean;
endIcon?: ReactNode;
size?: 'small' | 'medium' | 'large';
variant?: 'solid' | 'text';
}
const Button: React.FC<ButtonProps> = ({
children, children,
loading = false, loading = false,
disabled = false, disabled = false,
endIcon, endIcon,
size = 'medium',
variant = 'solid',
className, className,
...props ...props
}) => { }) => {
return ( return (
<button <button
className={`group relative bg-[#FA003F] rounded-[0.5625rem] h-12 p-px flex ${className || ''}`} className={`group relative flex h-12 cursor-pointer rounded-[0.5625rem] bg-[#FA003F] p-px ${className || ''}`}
disabled={disabled || loading} disabled={disabled || loading}
{...props} {...props}
> >
{loading ? ( {loading ? (
<svg <svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-primary" className="text-primary mr-3 -ml-1 h-5 w-5 animate-spin"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -51,18 +39,68 @@ const Button: React.FC<ButtonProps> = ({
</svg> </svg>
) : null} ) : null}
<div className={`rounded-lg bg-white text-[#222222] flex items-center justify-center px-3 `}> <div
className={`flex items-center justify-center rounded-lg bg-white px-3 text-[#222222]`}
>
{children} {children}
</div> </div>
{endIcon && !loading ? ( {endIcon && !loading ? (
<span className=" flex items-center justify-center p-3"> <span className="flex items-center justify-center p-3">{endIcon}</span>
{endIcon}
</span>
) : null} ) : null}
</button> </button>
); );
}; };
export default Button; export default Button;
export const IconButton: React.FC<IIconButtonProps> = ({
children,
loading = false,
disabled = false,
size = 'medium',
color = 'primary',
className,
...props
}) => {
const colorClass =
color === 'primary' ? 'bg-primary text-white' : 'bg-gray-300 text-gray-500';
const sizeClass =
size === 'large' ? 'w-12 h-12' : size === 'small' ? 'w-8 h-8' : 'w-10 h-10';
return (
<button
className={`group relative flex cursor-pointer items-center justify-center rounded-[0.5625rem] ${colorClass} ${sizeClass} ${className || ''} `}
disabled={disabled || loading}
{...props}
>
{loading ? (
<svg
className="text-primary mr-3 -ml-1 h-5 w-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l2-2.647z"
></path>
</svg>
) : null}
<div className={`flex items-center justify-center text-center`}>
{children}
</div>
</button>
);
};
@@ -0,0 +1,22 @@
export const AchievementIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10.7499 2.45007C11.4399 1.86007 12.5699 1.86007 13.2699 2.45007L14.8499 3.81007C15.1499 4.07007 15.7099 4.28007 16.1099 4.28007H17.8099C18.8699 4.28007 19.7399 5.15007 19.7399 6.21007V7.91007C19.7399 8.30007 19.9499 8.87007 20.2099 9.17007L21.5699 10.7501C22.1599 11.4401 22.1599 12.5701 21.5699 13.2701L20.2099 14.8501C19.9499 15.1501 19.7399 15.7101 19.7399 16.1101V17.8101C19.7399 18.8701 18.8699 19.7401 17.8099 19.7401H16.1099C15.7199 19.7401 15.1499 19.9501 14.8499 20.2101L13.2699 21.5701C12.5799 22.1601 11.4499 22.1601 10.7499 21.5701L9.16988 20.2101C8.86988 19.9501 8.30988 19.7401 7.90988 19.7401H6.17988C5.11988 19.7401 4.24988 18.8701 4.24988 17.8101V16.1001C4.24988 15.7101 4.03988 15.1501 3.78988 14.8501L2.43988 13.2601C1.85988 12.5701 1.85988 11.4501 2.43988 10.7601L3.78988 9.17007C4.03988 8.87007 4.24988 8.31007 4.24988 7.92007V6.20007C4.24988 5.14007 5.11988 4.27007 6.17988 4.27007H7.90988C8.29988 4.27007 8.86988 4.06007 9.16988 3.80007L10.7499 2.45007Z"
fill="currentColor"
/>
<path
d="M8.37988 12.0001L10.7899 14.4201L15.6199 9.58008"
stroke="white"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
+15
View File
@@ -0,0 +1,15 @@
export const CloseIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M2.4 24L0 21.6L9.6 12L0 2.4L2.4 0L12 9.6L21.6 0L24 2.4L14.4 12L24 21.6L21.6 24L12 14.4L2.4 24Z"
fill="currentColor"
/>
</svg>
);
+28
View File
@@ -0,0 +1,28 @@
import React from 'react';
import { CloseIcon } from './closeIcon';
import { MenuIcon } from './menuIcon';
import { SettingIcon } from './settingIcon';
import { AchievementIcon } from './achievementIcon';
import { PhoneIcon } from './phoneIcon';
import { InstagramIcon } from './instagramIcon';
export const icons = {
close: CloseIcon,
menu: MenuIcon,
setting: SettingIcon,
achievement: AchievementIcon,
phone: PhoneIcon,
instagram: InstagramIcon,
};
export type IconName = keyof typeof icons;
interface IconProps extends React.SVGProps<SVGSVGElement> {
name: IconName;
}
export const Icon = ({ name, ...props }: IconProps) => {
const Component = icons[name];
if (!Component) return null;
return <Component {...props} />;
};
File diff suppressed because one or more lines are too long
+15
View File
@@ -0,0 +1,15 @@
export const MenuIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M0 24H24V20H0V24ZM0 14H24V10H0V14ZM0 0V4H24V0H0Z"
fill="currentColor"
/>
</svg>
);
+38
View File
@@ -0,0 +1,38 @@
export const PhoneIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_4180_257)">
<path
d="M21.97 18.33C21.97 18.69 21.89 19.06 21.72 19.42C21.55 19.78 21.33 20.12 21.04 20.44C20.55 20.98 20.01 21.37 19.4 21.62C18.8 21.87 18.15 22 17.45 22C16.43 22 15.34 21.76 14.19 21.27C13.04 20.78 11.89 20.12 10.75 19.29C9.6 18.45 8.51 17.52 7.47 16.49C6.44 15.45 5.51 14.36 4.68 13.22C3.86 12.08 3.2 10.94 2.72 9.81C2.24 8.67 2 7.58 2 6.54C2 5.86 2.12 5.21 2.36 4.61C2.6 4 2.98 3.44 3.51 2.94C4.15 2.31 4.85 2 5.59 2C5.87 2 6.15 2.06 6.4 2.18C6.66 2.3 6.89 2.48 7.07 2.74L9.39 6.01C9.57 6.26 9.7 6.49 9.79 6.71C9.88 6.92 9.93 7.13 9.93 7.32C9.93 7.56 9.86 7.8 9.72 8.03C9.59 8.26 9.4 8.5 9.16 8.74L8.4 9.53C8.29 9.64 8.24 9.77 8.24 9.93C8.24 10.01 8.25 10.08 8.27 10.16C8.3 10.24 8.33 10.3 8.35 10.36C8.53 10.69 8.84 11.12 9.28 11.64C9.73 12.16 10.21 12.69 10.73 13.22C11.27 13.75 11.79 14.24 12.32 14.69C12.84 15.13 13.27 15.43 13.61 15.61C13.66 15.63 13.72 15.66 13.79 15.69C13.87 15.72 13.95 15.73 14.04 15.73C14.21 15.73 14.34 15.67 14.45 15.56L15.21 14.81C15.46 14.56 15.7 14.37 15.93 14.25C16.16 14.11 16.39 14.04 16.64 14.04C16.83 14.04 17.03 14.08 17.25 14.17C17.47 14.26 17.7 14.39 17.95 14.56L21.26 16.91C21.52 17.09 21.7 17.3 21.81 17.55C21.91 17.8 21.97 18.05 21.97 18.33Z"
stroke="currentColor"
stroke-width="1.5"
stroke-miterlimit="10"
/>
<path
d="M18.5 9C18.5 8.4 18.03 7.48 17.33 6.73C16.69 6.04 15.84 5.5 15 5.5"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M22 9C22 5.13 18.87 2 15 2"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<defs>
<clipPath id="clip0_4180_257">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
@@ -0,0 +1,27 @@
export const SettingIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
{...props}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z"
stroke="currentColor"
stroke-width="1.5"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M2 12.8799V11.1199C2 10.0799 2.85 9.21994 3.9 9.21994C5.71 9.21994 6.45 7.93994 5.54 6.36994C5.02 5.46994 5.33 4.29994 6.24 3.77994L7.97 2.78994C8.76 2.31994 9.78 2.59994 10.25 3.38994L10.36 3.57994C11.26 5.14994 12.74 5.14994 13.65 3.57994L13.76 3.38994C14.23 2.59994 15.25 2.31994 16.04 2.78994L17.77 3.77994C18.68 4.29994 18.99 5.46994 18.47 6.36994C17.56 7.93994 18.3 9.21994 20.11 9.21994C21.15 9.21994 22.01 10.0699 22.01 11.1199V12.8799C22.01 13.9199 21.16 14.7799 20.11 14.7799C18.3 14.7799 17.56 16.0599 18.47 17.6299C18.99 18.5399 18.68 19.6999 17.77 20.2199L16.04 21.2099C15.25 21.6799 14.23 21.3999 13.76 20.6099L13.65 20.4199C12.75 18.8499 11.27 18.8499 10.36 20.4199L10.25 20.6099C9.78 21.3999 8.76 21.6799 7.97 21.2099L6.24 20.2199C5.33 19.6999 5.02 18.5299 5.54 17.6299C6.45 16.0599 5.71 14.7799 3.9 14.7799C2.85 14.7799 2 13.9199 2 12.8799Z"
stroke="currentColor"
stroke-width="1.5"
stroke-miterlimit="10"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);