Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec0f73f560 | ||
|
|
e5820a4890 | ||
|
|
2a3f542e98 | ||
|
|
fe468885b0 | ||
|
|
3ee54504cc | ||
|
|
4fb64cfb88 | ||
|
|
3b7d973d27 | ||
|
|
f3a3931c51 | ||
|
|
3a551728c8 | ||
|
|
7732ce80e5 | ||
|
|
0cd2bf4c38 | ||
|
|
a21cca3df9 | ||
|
|
79d1865dc0 | ||
|
|
13212f6170 | ||
|
|
598395aebe | ||
|
|
9bc484e1da | ||
|
|
b226c82db6 | ||
|
|
72915f515d | ||
|
|
fe01646473 | ||
|
|
95f86f13ba |
No files matched your search
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "dev",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev"],
|
||||||
|
"port": 5173
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.claude
|
||||||
|
*.log
|
||||||
+1
-3
@@ -1,7 +1,5 @@
|
|||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
|
|
||||||
# Path: /app
|
|
||||||
|
|
||||||
WORKDIR /lk-results/
|
WORKDIR /lk-results/
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -10,4 +8,4 @@ RUN npm install
|
|||||||
RUN npm run build
|
RUN npm run build
|
||||||
RUN npm install -g serve
|
RUN npm install -g serve
|
||||||
|
|
||||||
CMD ["serve", "-s", "build"]
|
CMD ["serve", "-s", "dist"]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
container_name: lk-landing-page
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
networks:
|
||||||
|
- cloudflare
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
cloudflare:
|
||||||
|
external: true
|
||||||
Generated
+45
@@ -11,6 +11,7 @@
|
|||||||
"@emailjs/browser": "^4.3.3",
|
"@emailjs/browser": "^4.3.3",
|
||||||
"@headlessui/react": "^1.7.18",
|
"@headlessui/react": "^1.7.18",
|
||||||
"@heroicons/react": "^2.1.3",
|
"@heroicons/react": "^2.1.3",
|
||||||
|
"framer-motion": "^13.2.0",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"i18next-http-backend": "^2.5.2",
|
"i18next-http-backend": "^2.5.2",
|
||||||
@@ -2595,6 +2596,29 @@
|
|||||||
"url": "https://github.com/sponsors/rawify"
|
"url": "https://github.com/sponsors/rawify"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "13.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-13.2.0.tgz",
|
||||||
|
"integrity": "sha512-9E33ebgMaO33w1nN/jEdW8z3/GO483fMi4rqbMG9rt83XgW9QLKRe4NcmJ8s+fQ3O34++UHrIQwlIWGIWTITjA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^13.2.0",
|
||||||
|
"motion-utils": "^13.0.0",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs.realpath": {
|
"node_modules/fs.realpath": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
@@ -3158,6 +3182,21 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "13.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-13.2.0.tgz",
|
||||||
|
"integrity": "sha512-N6gdSoWRDk0Rh/fVtlqUtLs+fEN3ELFZI3cn3IQE9Mnf3E+Mh8wjO6MstzCOPFh4Yf0L1as5m2eUyYWj8ylVSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^13.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "13.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-13.0.0.tgz",
|
||||||
|
"integrity": "sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
@@ -4238,6 +4277,12 @@
|
|||||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@emailjs/browser": "^4.3.3",
|
"@emailjs/browser": "^4.3.3",
|
||||||
"@headlessui/react": "^1.7.18",
|
"@headlessui/react": "^1.7.18",
|
||||||
"@heroicons/react": "^2.1.3",
|
"@heroicons/react": "^2.1.3",
|
||||||
|
"framer-motion": "^13.2.0",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"i18next-http-backend": "^2.5.2",
|
"i18next-http-backend": "^2.5.2",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"pp": {
|
||||||
|
"lastUpdated": "Last Updated: 06/19/2024",
|
||||||
|
"pageTitle": "Privacy Policy",
|
||||||
|
"introText": "LK Results (\"we\", \"our\", or \"us\") is committed to protecting your privacy. This Privacy Policy explains how we collect, use, and safeguard your information when you visit our website lk-results.com (the \"Site\") and use our contact form.",
|
||||||
|
"pp1": {
|
||||||
|
"title": "1. Information We Collect",
|
||||||
|
"desc": "We may collect personal information that you voluntarily provide when you fill out the contact form on our Site. This includes:\n\nName\nEmail address\nPhone number\nCompany name\nAny other information you choose to provide in your message."
|
||||||
|
},
|
||||||
|
"pp2": {
|
||||||
|
"title": "2. How We Use Your Information",
|
||||||
|
"desc": "We use the information we collect to:\n\nRespond to your inquiries and provide customer support.\nCommunicate with you about your inquiries or services you have requested.\nImprove our Site and the services we offer.\nMonitor and analyze usage and trends to enhance user experience."
|
||||||
|
},
|
||||||
|
"pp3": {
|
||||||
|
"title": "3. Sharing Your Information",
|
||||||
|
"desc": "We do not sell or rent your personal information to third parties. We may share your information only in the following circumstances:\n\nWith Service Providers: We may share your information with third-party service providers who assist us in operating our Site or conducting our business, so long as those parties agree to keep this information confidential.\nFor Legal Purposes: We may disclose your information if required by law or in response to valid requests by public authorities (e.g., a court or government agency)."
|
||||||
|
},
|
||||||
|
"pp4": {
|
||||||
|
"title": "4. Security of Your Information",
|
||||||
|
"desc": "We implement appropriate security measures to protect your personal information from unauthorized access, use, or disclosure. However, please note that no method of transmission over the internet or method of electronic storage is 100% secure, and we cannot guarantee absolute security."
|
||||||
|
},
|
||||||
|
"pp5": {
|
||||||
|
"title": "5. Retention of Your Information",
|
||||||
|
"desc": "We will retain your personal information for as long as necessary to fulfill the purposes for which it was collected or to comply with legal requirements."
|
||||||
|
},
|
||||||
|
"pp6": {
|
||||||
|
"title": "6. Your Rights",
|
||||||
|
"desc": "Depending on your location, you may have the right to:\n\nAccess and receive a copy of the personal information we hold about you.\nRequest correction or deletion of your personal information.\nObject to or request the restriction of processing your personal information.\nWithdraw your consent to the use of your personal information, where consent was the basis for processing."
|
||||||
|
},
|
||||||
|
"pp7": {
|
||||||
|
"title": "7. Changes to This Privacy Policy",
|
||||||
|
"desc": "We may update this Privacy Policy from time to time. Changes will be posted on this page, and the \"Last Updated\" date will be revised. Your continued use of the Site after any changes indicates your acceptance of the new Privacy Policy."
|
||||||
|
},
|
||||||
|
"pp8": {
|
||||||
|
"title": "8. Contact Us",
|
||||||
|
"desc": "If you have any questions about these Privacy Policy, please contact us at:\n\nLK Results\[email protected]\n(+351) 923-543-517"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"pp": {
|
||||||
|
"lastUpdated": "Última Atualização: 06/19/2024",
|
||||||
|
"pageTitle": "Política de Privacidade",
|
||||||
|
"introText": "LK Results (\"nós\", \"nosso\" ou \"nos\") está comprometido com a proteção da sua privacidade. Esta Política de Privacidade explica como recolhemos, utilizamos e protegemos as suas informações quando visita o nosso website lk-results.com (o \"Site\") e utiliza o nosso formulário de contacto.",
|
||||||
|
"pp1": {
|
||||||
|
"title": "1. Informações que Recolhemos",
|
||||||
|
"desc": "Podemos recolher informações pessoais que você fornece voluntariamente ao preencher o formulário de contacto no nosso Site. Isto inclui:\n\nNome\nEndereço de Email\nNúmero de Telefone\nNome da Empresa\nQualquer outra informação que decida fornecer na sua mensagem."
|
||||||
|
},
|
||||||
|
"pp2": {
|
||||||
|
"title": "2. Como Utilizamos as Suas Informações",
|
||||||
|
"desc": "Utilizamos as informações que recolhemos para:\n\nResponder às suas perguntas e fornecer suporte ao cliente.\nComunicar consigo sobre as suas questões ou serviços que solicitou.\nMelhorar o nosso Site e os serviços que oferecemos.\nMonitorizar e analisar o uso e as tendências para melhorar a experiência do utilizador."
|
||||||
|
},
|
||||||
|
"pp3": {
|
||||||
|
"title": "3. Partilha das Suas Informações",
|
||||||
|
"desc": "Não vendemos nem alugamos as suas informações pessoais a terceiros. Podemos partilhar as suas informações apenas nas seguintes circunstâncias:\n\nCom Fornecedores de Serviços: Podemos partilhar as suas informações com fornecedores de serviços terceiros que nos assistem na operação do nosso Site ou na condução do nosso negócio, desde que essas partes concordem em manter essa informação confidencial.\nPara Fins Legais: Podemos divulgar as suas informações se exigido por lei ou em resposta a pedidos válidos de autoridades públicas (por exemplo, um tribunal ou agência governamental)."
|
||||||
|
},
|
||||||
|
"pp4": {
|
||||||
|
"title": "4. Segurança das Suas Informações",
|
||||||
|
"desc": "Implementamos medidas de segurança apropriadas para proteger as suas informações pessoais contra acesso, uso ou divulgação não autorizados. No entanto, note que nenhum método de transmissão pela internet ou método de armazenamento eletrónico é 100% seguro, e não podemos garantir segurança absoluta."
|
||||||
|
},
|
||||||
|
"pp5": {
|
||||||
|
"title": "5. Retenção das Suas Informações",
|
||||||
|
"desc": "Reteremos as suas informações pessoais pelo tempo necessário para cumprir os propósitos para os quais foram recolhidas ou para cumprir os requisitos legais."
|
||||||
|
},
|
||||||
|
"pp6": {
|
||||||
|
"title": "6. Os Seus Direitos",
|
||||||
|
"desc": "Dependendo da sua localização, pode ter o direito de:\n\nAceder e receber uma cópia das informações pessoais que mantemos sobre si.\nSolicitar a correção ou eliminação das suas informações pessoais.\nOpor-se ao processamento ou solicitar a restrição do processamento das suas informações pessoais.\nRetirar o seu consentimento para o uso das suas informações pessoais, quando o consentimento foi a base para o processamento."
|
||||||
|
},
|
||||||
|
"pp7": {
|
||||||
|
"title": "7. Alterações a Esta Política de Privacidade",
|
||||||
|
"desc": "Podemos atualizar esta Política de Privacidade periodicamente. As alterações serão publicadas nesta página, e a data de \"Última Atualização\" será revista. A sua utilização contínua do Site após quaisquer alterações indica a sua aceitação da nova Política de Privacidade."
|
||||||
|
},
|
||||||
|
"pp8": {
|
||||||
|
"title": "8. Contacte-nos",
|
||||||
|
"desc": "Se tiver alguma pergunta sobre esta Política de Privacidade, por favor, contacte-nos em:\n\nLK Results\[email protected]\n(+351) 923-543-517"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"tos": {
|
||||||
|
"lastUpdated": "Last Updated: 06/19/2024",
|
||||||
|
"pageTitle": "Terms and Conditions",
|
||||||
|
"introText": "Welcome to LK Results (\"we\", \"our\", or \"us\"). By accessing or using LK Results (the \"Site\"), you agree to comply with and be bound by these Terms and Conditions (\"Terms\"). Please read these Terms carefully. If you do not agree with any part of these Terms, you must not use the Site.",
|
||||||
|
"tos1": {
|
||||||
|
"title": "1. Use of the Site",
|
||||||
|
"desc": "You agree to use the Site and the contact form for lawful purposes only.\nYou must provide accurate and complete information when filling out the contact form.\nYou agree not to use the form to submit any content that is illegal, harmful, or intended to harass or impersonate others."
|
||||||
|
},
|
||||||
|
"tos2": {
|
||||||
|
"title": "2. User Responsibilities",
|
||||||
|
"desc": "You are responsible for ensuring the security and confidentiality of any personal information you provide.\nYou agree not to use the contact form to send unsolicited commercial messages or spam."
|
||||||
|
},
|
||||||
|
"tos3": {
|
||||||
|
"title": "3. Intellectual Property Rights",
|
||||||
|
"desc": "All content and materials on the Site, including text, graphics, logos, and images, are the property of LK Results or its licensors. You may not use, reproduce, or distribute any content without our prior written permission."
|
||||||
|
},
|
||||||
|
"tos4": {
|
||||||
|
"title": "4. Limitation of Liability",
|
||||||
|
"desc": "To the fullest extent permitted by law, LK Results shall not be liable for any damages resulting from your use of the Site or the contact form, including but not limited to direct, indirect, incidental, or consequential damages."
|
||||||
|
},
|
||||||
|
"tos5": {
|
||||||
|
"title": "5. Changes to Terms",
|
||||||
|
"desc": "We may update these Terms from time to time. Changes will be posted on this page, and the \"Last Updated\" date will be revised. Your continued use of the Site after any changes indicates your acceptance of the new Terms."
|
||||||
|
},
|
||||||
|
"tos6": {
|
||||||
|
"title": "6. Contact Us",
|
||||||
|
"desc": "If you have any questions about these Terms, please contact us at:\n\nLK Results\[email protected]\n(+351) 923-543-517"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"tos": {
|
||||||
|
"lastUpdated": "Última Atualização: 19/06/2024",
|
||||||
|
"pageTitle": "Termos e Condições",
|
||||||
|
"introText": "Bem-vindo(a) ao LK Results (\"nós\", \"nosso\" ou \"nos\"). Ao aceder ou utilizar LK Results (o \"Site\"), você concorda em cumprir e estar vinculado por estes Termos e Condições (\"Termos\"). Por favor, leia estes Termos cuidadosamente. Se não concordar com qualquer parte destes Termos, não deve utilizar o Site.",
|
||||||
|
"tos1": {
|
||||||
|
"title": "1. Utilização do Site",
|
||||||
|
"desc": "Concorda em utilizar o Site e o formulário de contacto apenas para fins legais.\nDeve fornecer informações precisas e completas ao preencher o formulário de contacto.\nConcorda em não utilizar o formulário para enviar qualquer conteúdo que seja ilegal, prejudicial ou destinado a assediar ou fazer-se passar por outras pessoas."
|
||||||
|
},
|
||||||
|
"tos2": {
|
||||||
|
"title": "2. Responsabilidades do Utilizador",
|
||||||
|
"desc": "É responsável por garantir a segurança e a confidencialidade de qualquer informação pessoal que forneça.\nConcorda em não utilizar o formulário de contacto para enviar mensagens comerciais não solicitadas ou spam."
|
||||||
|
},
|
||||||
|
"tos3": {
|
||||||
|
"title": "3. Direitos de Propriedade Intelectual",
|
||||||
|
"desc": "Todo o conteúdo e materiais no Site, incluindo texto, gráficos, logótipos e imagens, são propriedade do LK Results ou dos seus licenciadores. Não pode utilizar, reproduzir ou distribuir qualquer conteúdo sem a nossa permissão prévia por escrito."
|
||||||
|
},
|
||||||
|
"tos4": {
|
||||||
|
"title": "4. Limitação de Responsabilidade",
|
||||||
|
"desc": "Na medida máxima permitida por lei, LK Results não será responsável por quaisquer danos resultantes da sua utilização do Site ou do formulário de contacto, incluindo, mas não limitado a danos diretos, indiretos, incidentais ou consequenciais."
|
||||||
|
},
|
||||||
|
"tos5": {
|
||||||
|
"title": "5. Alterações aos Termos",
|
||||||
|
"desc": "Podemos atualizar estes Termos periodicamente. As alterações serão publicadas nesta página, e a data de \"Última Atualização\" será revista. A sua utilização contínua do Site após quaisquer alterações indica a sua aceitação dos novos Termos."
|
||||||
|
},
|
||||||
|
"tos6": {
|
||||||
|
"title": "6. Contacte-nos",
|
||||||
|
"desc": "Se tiver alguma pergunta sobre estes Termos, por favor, contacte-nos em:\n\nLK Results\[email protected]\n(+351) 923-543-517"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,13 +18,10 @@
|
|||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"part1": "We've Got Your",
|
"part1": "Unlock The Full ",
|
||||||
"part2": "Footprint",
|
"part2": "Potential",
|
||||||
"part3": "Bullsh",
|
"part3": "Of Your Business!"
|
||||||
"part4": "*",
|
|
||||||
"part5": "t Covered!"
|
|
||||||
},
|
},
|
||||||
"subheading": "Unlock the full potential of your business with our services.",
|
|
||||||
"cards": {
|
"cards": {
|
||||||
"marketing": {
|
"marketing": {
|
||||||
"card1": {
|
"card1": {
|
||||||
@@ -154,7 +151,7 @@
|
|||||||
},
|
},
|
||||||
"subheading": {
|
"subheading": {
|
||||||
"part1": "Would you like to know what we could do for you? Fill out the form ad we’ll get",
|
"part1": "Would you like to know what we could do for you? Fill out the form ad we’ll get",
|
||||||
"part2": "back to you No obligation, no annoying tactics. We won’t waste your time."
|
"part2": "back to you. No obligation, no annoying tactics. We won’t waste your time."
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
@@ -170,5 +167,10 @@
|
|||||||
"error": "An error occurred while sending your message. Please try again later.",
|
"error": "An error occurred while sending your message. Please try again later.",
|
||||||
"empty": "Please fill out all the fields before sending the message."
|
"empty": "Please fill out all the fields before sending the message."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"rights": "© 2024 LK Results. All rights reservered.",
|
||||||
|
"tos": "Terms of Service",
|
||||||
|
"privacy": "Privacy Policy"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,24 +7,21 @@
|
|||||||
},
|
},
|
||||||
"hero": {
|
"hero": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"part1": "CRESÇA O",
|
"part1": "CRESCE O",
|
||||||
"part2": "SEU NEGÓCIO"
|
"part2": "TEU NEGÓCIO"
|
||||||
},
|
},
|
||||||
"subheading": {
|
"subheading": {
|
||||||
"part1": "Nós tratamos de toda a sua presença digital,",
|
"part1": "Nós tratamos de toda a tua presença digital,",
|
||||||
"part2": "para que possa concentrar-se no que é importante!"
|
"part2": "para que te possas concentrar no que é importante!"
|
||||||
},
|
},
|
||||||
"button": "Eu quero isto!"
|
"button": "Eu quero isto!"
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"part1": "Nós Cuidamos Da Sua",
|
"part1": "Desbloqueia Todo O ",
|
||||||
"part2": "Presença",
|
"part2": "Potencial",
|
||||||
"part3": "Digital",
|
"part3": "Do Teu Negócio!"
|
||||||
"part4": " ",
|
|
||||||
"part5": "Por Completo!"
|
|
||||||
},
|
},
|
||||||
"subheading": "Desbloqueie todo o potencial do seu negócio com os nossos serviços.",
|
|
||||||
"cards": {
|
"cards": {
|
||||||
"marketing": {
|
"marketing": {
|
||||||
"card1": {
|
"card1": {
|
||||||
@@ -108,7 +105,7 @@
|
|||||||
"cards": {
|
"cards": {
|
||||||
"card1": {
|
"card1": {
|
||||||
"title": "TRATAR DE TUDO SOZINHO?",
|
"title": "TRATAR DE TUDO SOZINHO?",
|
||||||
"description": "Está ótimo se tens muito pouco em mãos.\n\nMas se já estás bastante ocupado... esta não é uma opção viável."
|
"description": "É ótimo se tens muito pouco em mãos.\n\nMas se já estás bastante ocupado... esta não é uma opção viável."
|
||||||
},
|
},
|
||||||
"card2": {
|
"card2": {
|
||||||
"title": "NOVA STAFF?",
|
"title": "NOVA STAFF?",
|
||||||
@@ -116,7 +113,7 @@
|
|||||||
},
|
},
|
||||||
"card3": {
|
"card3": {
|
||||||
"title": "CONTRATAR UMA AGÊNCIA?",
|
"title": "CONTRATAR UMA AGÊNCIA?",
|
||||||
"description": "Não tens um orçamento de dezenas de milhares de euros?\n\n Então a tua conta pode acabar a ser gerida pelo estagiário do assistente do assistente...\n\n Não é o cenário ideal."
|
"description": "Não tens um orçamento de dezenas de milhares de euros?\n\n Então a tua conta pode acabar por ser gerida pelo estagiário do assistente do assistente...\n\n Não é o cenário ideal."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -129,7 +126,7 @@
|
|||||||
"cards": {
|
"cards": {
|
||||||
"card1": {
|
"card1": {
|
||||||
"title": "GARANTIDO",
|
"title": "GARANTIDO",
|
||||||
"description": "Nós só ganhamos se você ganhar!\nEssa é a base para uma boa\nparceria.\nVocê não vai carregar todo o risco,\nnós vamos compartilhá-lo."
|
"description": "Nós só ganhamos se tu ganhares!\nEssa é a base para uma boa\nparceria. Não vais carregar todo o risco,\n vamos compartilhá-lo."
|
||||||
},
|
},
|
||||||
"card2": {
|
"card2": {
|
||||||
"title": "RESULTADOS",
|
"title": "RESULTADOS",
|
||||||
@@ -137,38 +134,43 @@
|
|||||||
},
|
},
|
||||||
"card3": {
|
"card3": {
|
||||||
"title": "LOCAL",
|
"title": "LOCAL",
|
||||||
"description": "Não somos um centro de chamadas anónimo. Somos uma\nempresa local, por isso poderá\ncontactar-nos quando precisar."
|
"description": "Não somos um centro de chamadas anónimo. Somos uma\nempresa local, por isso podes\ncontactar-nos quando precisares."
|
||||||
},
|
},
|
||||||
"card4": {
|
"card4": {
|
||||||
"title": "ESPECIALIZADO",
|
"title": "ESPECIALIZADO",
|
||||||
"description": "Faz-tudo... mestre de nada. A especialização\nfunciona. Trabalhamos com\nindústrias que conhecemos, para que possamos\ngarantir resultados."
|
"description": "Quem faz tudo, nada faz... A especialização\nfunciona. Trabalhamos com\nindústrias que conhecemos, para que possamos\ngarantir resultados."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
"heading": {
|
"heading": {
|
||||||
"part1": "Contacte-nos para uma",
|
"part1": "Contacta-nos para uma",
|
||||||
"part2": "Análise",
|
"part2": "Análise",
|
||||||
"part3": "Gratuita",
|
"part3": "Gratuita",
|
||||||
"part4": ""
|
"part4": ""
|
||||||
},
|
},
|
||||||
"subheading": {
|
"subheading": {
|
||||||
"part1": "Gostaria de saber o que podemos fazer por si? Preencha o formulário e nós entraremos",
|
"part1": "Gostarias de saber o que podemos fazer por ti? Preenche o formulário e nós entraremos",
|
||||||
"part2": "em contacto consigo. Sem obrigações, sem táticas irritantes. Não vamos desperdiçar o seu tempo."
|
"part2": "em contacto contigo. Sem obrigações, sem táticas irritantes. Não vamos desperdiçar o teu tempo."
|
||||||
},
|
},
|
||||||
"form": {
|
"form": {
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"company": "Empresa",
|
"company": "Empresa",
|
||||||
"phone": "Telefone",
|
"phone": "Telefone",
|
||||||
"question": "Qual é a sua pergunta mais importante?",
|
"question": "Qual é a tua pergunta mais importante?",
|
||||||
"message": "Mensagem",
|
"message": "Mensagem",
|
||||||
"button": "Enviar"
|
"button": "Enviar"
|
||||||
},
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"success": "A sua mensagem foi enviada com sucesso!",
|
"success": "A tua mensagem foi enviada com sucesso!",
|
||||||
"error": "Ocorreu um erro ao enviar a sua mensagem. Por favor, tente novamente mais tarde.",
|
"error": "Ocorreu um erro ao enviar a tua mensagem. Por favor, tenta novamente mais tarde.",
|
||||||
"empty": "Por favor, preencha todos os campos antes de enviar a mensagem."
|
"empty": "Por favor, preenche todos os campos antes de enviar a mensagem."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"rights": "©2024 Direitos reservados a LK Results",
|
||||||
|
"tos": "Termos de Serviço",
|
||||||
|
"privacy": "Polítca de Privacidade"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
import { Route, Routes } from "react-router-dom";
|
import { Route, Routes } from "react-router-dom";
|
||||||
import Home from "./routes/Home";
|
import Home from "./routes/Home";
|
||||||
|
import TOS from "./routes/TOS";
|
||||||
|
import PP from "./routes/PP";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<div className="font-baijam min-h-screen text-white">
|
<div className="font-baijam min-h-screen text-white">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="*" element={<Home />} />
|
<Route path="*" element={<Home />} />
|
||||||
|
<Route path="tos" element={<TOS />} />
|
||||||
|
<Route path="privacy" element={<PP />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
+31
-10
@@ -1,28 +1,49 @@
|
|||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
index?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Card = ({ title, description, image }: IProps) => {
|
const Card = ({ title, description, image, index = 0 }: IProps) => {
|
||||||
const defaultImgClasses = `w-full h-20 mt-5`;
|
const prefersReducedMotion = useReducedMotion();
|
||||||
const imgClasses = image ? defaultImgClasses : `${defaultImgClasses} hidden`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-5 xl:mt-0 flex flex-1 bg-gradient-to-b from-gold-light to-gold-dark p-0.5 shadow-lg">
|
<motion.div
|
||||||
<div className="w-full font-medium flex-col md:px-4 py-1 bg-gradient-to-br from-custom-bglight to-custom-bgdark">
|
className={`relative flex flex-1 flex-col gap-5 rounded-2xl glass-panel shadow-glass px-6 py-7 md:px-7 md:py-8 ${
|
||||||
<object type="image/svg+xml" data={image} className={imgClasses} aria-label={title}></object>
|
image ? "text-left items-start" : "text-center items-center"
|
||||||
<h1 className="mt-2 lg:mt-6 text-3xl xl:text-5xl">{title}</h1>
|
}`}
|
||||||
<h2 className="px-2 mb-2 mt-3 italic text-xl xl:text-3xl font-medium text-gray-300">
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 24 }}
|
||||||
|
whileInView={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, margin: "-80px" }}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
damping: 22,
|
||||||
|
stiffness: 140,
|
||||||
|
delay: prefersReducedMotion ? 0 : index * 0.08,
|
||||||
|
}}
|
||||||
|
whileHover={prefersReducedMotion ? {} : { y: -4 }}
|
||||||
|
>
|
||||||
|
<span className="absolute inset-x-6 top-0 h-px bg-gradient-to-r from-transparent via-gold-light/50 to-transparent" />
|
||||||
|
|
||||||
|
{image && <img src={image} alt="" className="h-10 w-10" />}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h3 className="text-xl lg:text-2xl font-semibold text-white tracking-tight leading-snug">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm lg:text-base text-white/55 leading-relaxed">
|
||||||
{description.split("\n").map((line, i) => (
|
{description.split("\n").map((line, i) => (
|
||||||
<span key={i}>
|
<span key={i}>
|
||||||
{line}
|
{line}
|
||||||
<br />
|
<br />
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</h2>
|
</p>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,75 @@
|
|||||||
import { FaFacebook, FaLinkedin, FaInstagram } from "react-icons/fa";
|
import { useTranslation } from "react-i18next";
|
||||||
import { FaXTwitter } from "react-icons/fa6";
|
import { FaFacebook, FaLinkedin } from "react-icons/fa";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
|
||||||
|
// Browsers report region variants (e.g. "en-US", "pt-BR"), not just the
|
||||||
|
// bare "en"/"pt" i18next is configured with - matching on that exactly
|
||||||
|
// would pick the wrong sized text class for most real visitors.
|
||||||
|
const currentLang = i18n.language?.startsWith("pt") ? "pt" : "en";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="flex flex-col space-y-10 justify-center m-10">
|
<footer className="flex flex-col space-y-8 justify-center items-center mt-10 mb-8 px-6 pt-10">
|
||||||
<div className="flex justify-center space-x-5">
|
<div className="w-full max-w-xs h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
|
||||||
<a
|
|
||||||
|
<div className="flex justify-center space-x-6">
|
||||||
|
<motion.a
|
||||||
href="https://www.facebook.com/people/LK-Marketing/61556714037746/"
|
href="https://www.facebook.com/people/LK-Marketing/61556714037746/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
whileHover={{ scale: 1.12, color: "#FACC97" }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
transition={{ type: "spring", stiffness: 400, damping: 22 }}
|
||||||
|
className="text-gold-light"
|
||||||
>
|
>
|
||||||
<FaFacebook className="text-gold-light" size={30} />
|
<FaFacebook size={26} />
|
||||||
</a>
|
</motion.a>
|
||||||
<a
|
<motion.a
|
||||||
href="https://www.linkedin.com/company/lk-results"
|
href="https://www.linkedin.com/company/lk-results"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
whileHover={{ scale: 1.12, color: "#FACC97" }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
transition={{ type: "spring", stiffness: 400, damping: 22 }}
|
||||||
|
className="text-gold-light"
|
||||||
>
|
>
|
||||||
<FaLinkedin className="text-gold-light" size={30} />
|
<FaLinkedin size={26} />
|
||||||
</a>
|
</motion.a>
|
||||||
{/* <a
|
|
||||||
href="https://instagram.com"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<FaInstagram className="text-gold-light" size={30} />
|
|
||||||
</a>
|
|
||||||
<a href="https://twitter.com" target="_blank" rel="noopener noreferrer">
|
|
||||||
<FaXTwitter className="text-gold-light" size={30} />
|
|
||||||
</a> */}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-center text-gray-400 font-medium">
|
|
||||||
© 2024 LK Results. All rights reservered.
|
<div className="flex justify-center space-x-6">
|
||||||
|
<a
|
||||||
|
href="/tos"
|
||||||
|
className={
|
||||||
|
currentLang === "en"
|
||||||
|
? "footer-text hover:text-gold-light transition-colors"
|
||||||
|
: "footer-text-pt hover:text-gold-light transition-colors"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("footer.tos")}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="/privacy"
|
||||||
|
className={
|
||||||
|
currentLang === "en"
|
||||||
|
? "footer-text hover:text-gold-light transition-colors"
|
||||||
|
: "footer-text-pt hover:text-gold-light transition-colors"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("footer.privacy")}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
currentLang === "en"
|
||||||
|
? "footer-text text-center"
|
||||||
|
: "footer-text-pt text-center"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("footer.rights")}
|
||||||
</p>
|
</p>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen bg-custom-bgdark">
|
||||||
|
{prefersReducedMotion ? (
|
||||||
|
<div className="h-16 w-16 rounded-full border-4 border-white/10 border-t-gold-light" />
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
className="h-16 w-16 rounded-full border-4 border-white/10 border-t-gold-light"
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 0.9, repeat: Infinity, ease: "linear" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Loading;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface MenuContentProps {
|
||||||
|
navigation: { title: string; path: string }[];
|
||||||
|
activeSection: string;
|
||||||
|
handleNavigation: (path: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuContent = ({
|
||||||
|
navigation,
|
||||||
|
activeSection,
|
||||||
|
handleNavigation,
|
||||||
|
}: MenuContentProps) => {
|
||||||
|
return (
|
||||||
|
<ul className="flex items-center gap-8">
|
||||||
|
{navigation.map((item, idx) => {
|
||||||
|
const isActive = activeSection === item.path;
|
||||||
|
return (
|
||||||
|
<li key={idx}>
|
||||||
|
<motion.a
|
||||||
|
href={item.path}
|
||||||
|
className="relative block text-sm font-medium tracking-wide"
|
||||||
|
animate={{ color: isActive ? "#ffffff" : "rgba(255,255,255,0.55)" }}
|
||||||
|
whileHover={{ color: "#ffffff" }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleNavigation(item.path);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
{isActive && (
|
||||||
|
<motion.span
|
||||||
|
layoutId="desktop-nav-underline"
|
||||||
|
className="absolute -bottom-1.5 left-0 h-px w-full rounded-full bg-gold-light"
|
||||||
|
transition={{ type: "spring", stiffness: 380, damping: 32 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuContent;
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import {
|
||||||
|
FaLayerGroup,
|
||||||
|
FaGaugeHigh,
|
||||||
|
FaAward,
|
||||||
|
FaEnvelope,
|
||||||
|
} from "react-icons/fa6";
|
||||||
|
import { langFlagUrl } from "./langFlag";
|
||||||
|
|
||||||
|
const ICONS: Record<string, typeof FaLayerGroup> = {
|
||||||
|
Services: FaLayerGroup,
|
||||||
|
Optimization: FaGaugeHigh,
|
||||||
|
Advantages: FaAward,
|
||||||
|
Contact: FaEnvelope,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MobileMenuProps {
|
||||||
|
navigation: { title: string; path: string }[];
|
||||||
|
handleNavigation: (path: string) => void;
|
||||||
|
setState: (v: boolean) => void;
|
||||||
|
currentLang: "en" | "pt";
|
||||||
|
changeLanguage: (lng: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MobileMenu = ({
|
||||||
|
navigation,
|
||||||
|
handleNavigation,
|
||||||
|
setState,
|
||||||
|
currentLang,
|
||||||
|
changeLanguage,
|
||||||
|
}: MobileMenuProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex w-full flex-col px-3 pt-2 pb-3">
|
||||||
|
<ul className="flex flex-col gap-1">
|
||||||
|
{navigation.map((item, idx) => {
|
||||||
|
const Icon = ICONS[item.path];
|
||||||
|
return (
|
||||||
|
<li key={idx}>
|
||||||
|
<motion.a
|
||||||
|
href={item.path}
|
||||||
|
className="flex items-center gap-3 rounded-xl px-4 py-3 text-lg font-medium text-white/70"
|
||||||
|
whileHover={{ backgroundColor: "rgba(255,255,255,0.06)", color: "#ffffff" }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setState(false);
|
||||||
|
handleNavigation(item.path);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Icon && <Icon className="h-4 w-4 text-gold-light shrink-0" />}
|
||||||
|
{item.title}
|
||||||
|
</motion.a>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="my-3 h-px bg-white/10" />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center justify-between rounded-xl px-4 py-3"
|
||||||
|
onClick={() => changeLanguage(currentLang === "en" ? "pt" : "en")}
|
||||||
|
>
|
||||||
|
<span className="text-sm text-white/45">Language</span>
|
||||||
|
<motion.span
|
||||||
|
className="flex items-center gap-1.5 rounded-full border border-gold-light/25 px-3 py-1 text-sm font-semibold text-gold-light"
|
||||||
|
whileHover={{ borderColor: "rgba(250,204,151,0.6)" }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={langFlagUrl(currentLang === "en" ? "pt" : "en")}
|
||||||
|
alt=""
|
||||||
|
className="h-3.5 w-5 rounded-[2px] object-cover"
|
||||||
|
/>
|
||||||
|
{currentLang === "en" ? "PT" : "EN"}
|
||||||
|
</motion.span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileMenu;
|
||||||
@@ -1,23 +1,27 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { FaTimes, FaBars } from "react-icons/fa";
|
import { FaTimes, FaBars } from "react-icons/fa";
|
||||||
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||||
|
|
||||||
import Logo from "../../assets/logo.svg";
|
import Logo from "../../assets/logo.svg";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import MenuContent from "./MenuContent";
|
||||||
|
import MobileMenu from "./MobileMenu";
|
||||||
|
import { langFlagUrl } from "./langFlag";
|
||||||
|
|
||||||
export default () => {
|
const NAVBAR_HEIGHT = 110;
|
||||||
|
|
||||||
|
const Navbar = () => {
|
||||||
const [state, setState] = useState(false);
|
const [state, setState] = useState(false);
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
const [colorChange, setColorChange] = useState(false);
|
const [colorChange, setColorChange] = useState(false);
|
||||||
const changeColor = () => {
|
const [activeSection, setActiveSection] = useState("Services");
|
||||||
if (window.scrollY >= 100) {
|
|
||||||
setColorChange(true);
|
|
||||||
} else {
|
|
||||||
setColorChange(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener("scroll", changeColor);
|
// Browsers report region variants (e.g. "en-US", "pt-BR"), not just the
|
||||||
|
// bare "en"/"pt" i18next is configured with - matching on that exactly
|
||||||
|
// would leave the toggle mislabeled for most real visitors.
|
||||||
|
const currentLang = i18n.language?.startsWith("pt") ? "pt" : "en";
|
||||||
|
|
||||||
const navigation = [
|
const navigation = [
|
||||||
{ title: t("navbar.services"), path: "Services" },
|
{ title: t("navbar.services"), path: "Services" },
|
||||||
@@ -26,12 +30,86 @@ export default () => {
|
|||||||
{ title: t("navbar.contact"), path: "Contact" },
|
{ title: t("navbar.contact"), path: "Contact" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Each section's offset, measured once (and on resize) instead of on
|
||||||
|
// every scroll tick - getBoundingClientRect() forces a synchronous
|
||||||
|
// layout recalc, and calling it on every native scroll event (which
|
||||||
|
// fires far more often than the display's refresh rate during a fast
|
||||||
|
// swipe or a smooth-scroll animation) is what made scrolling feel
|
||||||
|
// sluggish: layout was being thrashed dozens of times a second.
|
||||||
|
let sectionTops: { path: string; top: number }[] = [];
|
||||||
|
|
||||||
|
const measureSections = () => {
|
||||||
|
sectionTops = navigation.map((item) => {
|
||||||
|
const el = document.getElementById(item.path);
|
||||||
|
const top = el
|
||||||
|
? el.getBoundingClientRect().top + window.scrollY
|
||||||
|
: Infinity;
|
||||||
|
return { path: item.path, top };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let ticking = false;
|
||||||
|
|
||||||
|
const updateFromScroll = () => {
|
||||||
|
setColorChange(window.scrollY >= 100);
|
||||||
|
|
||||||
|
// The active section is whichever nav target's start we've scrolled
|
||||||
|
// past most recently - not just "is it in the viewport", since a
|
||||||
|
// short section could otherwise never register as active at all.
|
||||||
|
let current = navigation[0].path;
|
||||||
|
for (const { path, top } of sectionTops) {
|
||||||
|
if (window.scrollY + NAVBAR_HEIGHT + 40 >= top) {
|
||||||
|
current = path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveSection(current);
|
||||||
|
ticking = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (ticking) return;
|
||||||
|
ticking = true;
|
||||||
|
requestAnimationFrame(updateFromScroll);
|
||||||
|
};
|
||||||
|
|
||||||
|
measureSections();
|
||||||
|
updateFromScroll();
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
|
window.addEventListener("resize", measureSections);
|
||||||
|
|
||||||
|
const scrollPath = sessionStorage.getItem("scrollPath");
|
||||||
|
if (scrollPath) {
|
||||||
|
smoothScroll(scrollPath);
|
||||||
|
sessionStorage.removeItem("scrollPath");
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("scroll", handleScroll);
|
||||||
|
window.removeEventListener("resize", measureSections);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleNavigation = (path: string) => {
|
const handleNavigation = (path: string) => {
|
||||||
|
setState(false);
|
||||||
|
if (
|
||||||
|
(window.location.pathname === "/tos" ||
|
||||||
|
window.location.pathname === "/privacy") &&
|
||||||
|
navigation.some((navItem) => navItem.path === path)
|
||||||
|
) {
|
||||||
|
sessionStorage.setItem("scrollPath", path);
|
||||||
|
window.location.href = "/";
|
||||||
|
} else {
|
||||||
|
smoothScroll(path);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const smoothScroll = (path: string) => {
|
||||||
const element = document.getElementById(path);
|
const element = document.getElementById(path);
|
||||||
const navbarHeight = 110; // Replace with your navbar's height
|
|
||||||
if (element) {
|
if (element) {
|
||||||
const top =
|
const top =
|
||||||
element.getBoundingClientRect().top + window.scrollY - navbarHeight;
|
element.getBoundingClientRect().top + window.scrollY - NAVBAR_HEIGHT;
|
||||||
window.scrollTo({ top: top, behavior: "smooth" });
|
window.scrollTo({ top: top, behavior: "smooth" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -40,74 +118,159 @@ export default () => {
|
|||||||
i18n.changeLanguage(lng);
|
i18n.changeLanguage(lng);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.body.style.overflow = state ? "hidden" : "";
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, [state]);
|
||||||
|
|
||||||
|
const springTap = prefersReducedMotion ? {} : { whileTap: { scale: 0.94 } };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav
|
<motion.nav
|
||||||
className={`${
|
className="sticky top-0 z-40 w-full"
|
||||||
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: -16 }}
|
||||||
|
animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 22, stiffness: 140 }}
|
||||||
|
>
|
||||||
|
<div className={colorChange ? "px-4" : ""}>
|
||||||
|
<div
|
||||||
|
className={`relative mx-auto flex items-center justify-between transition-[max-width,margin,padding] duration-500 ${
|
||||||
colorChange
|
colorChange
|
||||||
? "bg-black bg-opacity-80 transition duration-500 w-full border-0 sticky top-0 z-10"
|
? "mt-3 max-w-3xl px-5 py-2.5"
|
||||||
: "z-10"
|
: "max-w-screen-xl px-4 py-3 md:px-8 md:py-5"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="items-center px-4 max-w-screen-xl mx-auto md:flex md:px-8">
|
{/*
|
||||||
<div className="flex items-center justify-between py-3 md:py-5 md:block">
|
The glass material is its own layer, separate from the box
|
||||||
<a
|
above whose size/margin is animating. backdrop-filter can't be
|
||||||
|
smoothly interpolated between "none" and "blur(20px)" - toggling
|
||||||
|
it on the same element that's also resizing is what produced a
|
||||||
|
seam along the bottom edge mid-transition. Here it's always
|
||||||
|
present and only crossfades via opacity, which is safe.
|
||||||
|
*/}
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 -z-10 rounded-full glass-nav shadow-glass-lg transition-opacity duration-500 ${
|
||||||
|
colorChange ? "opacity-100" : "opacity-0"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.a
|
||||||
href="Top"
|
href="Top"
|
||||||
|
whileTap={{ scale: 0.92 }}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault(); // Prevent the default anchor tag behavior
|
e.preventDefault();
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" }); // Scroll to the top of the page
|
if (window.location.pathname === "/") {
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
} else {
|
||||||
|
window.location.href = "/";
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img src={Logo} width={60} height={60} alt="logo" />
|
<img
|
||||||
</a>
|
src={Logo}
|
||||||
<div className="md:hidden">
|
width={colorChange ? 40 : 56}
|
||||||
<button
|
height={colorChange ? 40 : 56}
|
||||||
className="outline-none p-2 rounded-md focus:border-gray-400 focus:border"
|
alt="logo"
|
||||||
|
className="transition-all duration-500"
|
||||||
|
/>
|
||||||
|
</motion.a>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Centered against the whole bar (absolute + left-1/2), not just
|
||||||
|
"between" the logo and the language toggle - those two groups
|
||||||
|
are different widths, so a plain flex justify-between would
|
||||||
|
center the nav in the remaining space rather than in the bar
|
||||||
|
itself, visibly pulling it off-center.
|
||||||
|
*/}
|
||||||
|
<div className="absolute left-1/2 hidden -translate-x-1/2 md:block">
|
||||||
|
<MenuContent
|
||||||
|
navigation={navigation}
|
||||||
|
activeSection={activeSection}
|
||||||
|
handleNavigation={handleNavigation}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<motion.button
|
||||||
|
className="hidden md:flex items-center gap-1.5 px-3 py-1.5 rounded-full border border-gold-light/25 text-sm font-semibold text-gold-light"
|
||||||
|
whileHover={{ borderColor: "rgba(250,204,151,0.6)" }}
|
||||||
|
whileTap={{ scale: 0.9 }}
|
||||||
|
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||||
|
onClick={() => changeLanguage(currentLang === "en" ? "pt" : "en")}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={langFlagUrl(currentLang === "en" ? "pt" : "en")}
|
||||||
|
alt=""
|
||||||
|
className="h-3.5 w-5 rounded-[2px] object-cover"
|
||||||
|
/>
|
||||||
|
{currentLang === "en" ? "PT" : "EN"}
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
className="md:hidden outline-none p-2 rounded-full bg-white/5 hover:bg-white/10 transition-colors"
|
||||||
onClick={() => setState(!state)}
|
onClick={() => setState(!state)}
|
||||||
|
{...springTap}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
|
aria-label={state ? "Close menu" : "Open menu"}
|
||||||
>
|
>
|
||||||
{state ? (
|
{state ? (
|
||||||
<FaTimes className="h-6 w-6" />
|
<FaTimes className="h-5 w-5" />
|
||||||
) : (
|
) : (
|
||||||
<FaBars className="h-6 w-6" />
|
<FaBars className="h-5 w-5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
className={`absolute top-[3.24rem] md:top-0 left-0 w-full z-50 flex flex-col md:flex-row items-center justify-center pb-3 mt-8 md:relative md:bg-transparent md:pb-0 md:mt-0 ${
|
|
||||||
state
|
<AnimatePresence>
|
||||||
? colorChange
|
{state && (
|
||||||
? "block bg-black bg-opacity-80 transition duration-500"
|
<>
|
||||||
: "block bg-gradient-to-br from-custom-bglight to-custom-bgdark"
|
<motion.div
|
||||||
: "hidden md:flex"
|
key="mobile-scrim"
|
||||||
}`}
|
className="fixed inset-0 z-40 bg-black/60 md:hidden"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.25 }}
|
||||||
|
onClick={() => setState(false)}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
key="mobile-menu"
|
||||||
|
initial={
|
||||||
|
prefersReducedMotion
|
||||||
|
? { opacity: 0 }
|
||||||
|
: { opacity: 0, y: -12, scale: 0.98 }
|
||||||
|
}
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion
|
||||||
|
? { opacity: 1 }
|
||||||
|
: { opacity: 1, y: 0, scale: 1 }
|
||||||
|
}
|
||||||
|
exit={
|
||||||
|
prefersReducedMotion
|
||||||
|
? { opacity: 0 }
|
||||||
|
: { opacity: 0, y: -12, scale: 0.98 }
|
||||||
|
}
|
||||||
|
transition={{ type: "spring", damping: 26, stiffness: 300 }}
|
||||||
|
className="absolute top-[3.6rem] left-0 right-0 z-50 mx-4 flex flex-col rounded-2xl glass-sheet shadow-glass-lg md:hidden"
|
||||||
>
|
>
|
||||||
<ul className="text-text-gold space-y-8 md:ml-auto md:flex md:space-x-6 md:space-y-0 justify-center items-center">
|
<MobileMenu
|
||||||
{navigation.map((item, idx) => {
|
navigation={navigation}
|
||||||
return (
|
handleNavigation={handleNavigation}
|
||||||
<li key={idx} className="hover:text-white">
|
setState={setState}
|
||||||
<a
|
currentLang={currentLang}
|
||||||
href={item.path}
|
changeLanguage={changeLanguage}
|
||||||
className="block py-2 md:py-0 md:px-4 text-xl font-medium"
|
/>
|
||||||
onClick={(e) => {
|
</motion.div>
|
||||||
e.preventDefault(); // Prevent the default anchor tag behavior
|
</>
|
||||||
setState(false); // Close the navigation
|
)}
|
||||||
handleNavigation(item.path); // Navigate to the section
|
</AnimatePresence>
|
||||||
}}
|
</motion.nav>
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
<button
|
|
||||||
className="mt-5 md:mt-0 md:ml-auto"
|
|
||||||
onClick={() => changeLanguage(i18n.language === "en" ? "pt" : "en")}
|
|
||||||
>
|
|
||||||
{i18n.language === "en" ? "PT" : "EN"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Flag images (not emoji) for the same reason the phone country-code
|
||||||
|
// picker uses them: emoji flag glyphs render with wildly inconsistent
|
||||||
|
// metrics across platforms (and don't render as flags at all on some,
|
||||||
|
// e.g. Windows), while a fixed-size image is pixel-identical everywhere.
|
||||||
|
const LANG_ISO: Record<string, string> = {
|
||||||
|
en: "gb",
|
||||||
|
pt: "pt",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const langFlagUrl = (lng: string) =>
|
||||||
|
`https://flagcdn.com/${LANG_ISO[lng] ?? lng}.svg`;
|
||||||
@@ -1,11 +1,60 @@
|
|||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
import emailjs from "@emailjs/browser";
|
import emailjs from "@emailjs/browser";
|
||||||
import { ToastContainer, toast } from "react-toastify";
|
import { ToastContainer, toast } from "react-toastify";
|
||||||
import "react-toastify/dist/ReactToastify.css";
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
import PhoneCodeSelect from "./PhoneCodeSelect";
|
||||||
|
|
||||||
|
const PHONE_PATTERNS: Record<string, string> = {
|
||||||
|
"+351": "### ### ###",
|
||||||
|
"+34": "### ### ###",
|
||||||
|
"+33": "# ## ## ## ##",
|
||||||
|
"+44": "#### ######",
|
||||||
|
"+1": "(###) ###-####",
|
||||||
|
"+55": "(##) #####-####",
|
||||||
|
};
|
||||||
|
|
||||||
|
const digitsOnly = (value: string) => value.replace(/\D/g, "");
|
||||||
|
|
||||||
|
const maxDigitsFor = (pattern: string) =>
|
||||||
|
pattern.split("").filter((c) => c === "#").length;
|
||||||
|
|
||||||
|
const formatPhoneNumber = (digits: string, pattern: string) => {
|
||||||
|
let result = "";
|
||||||
|
let digitIndex = 0;
|
||||||
|
for (let i = 0; i < pattern.length && digitIndex < digits.length; i++) {
|
||||||
|
const ch = pattern[i];
|
||||||
|
if (ch === "#") {
|
||||||
|
result += digits[digitIndex];
|
||||||
|
digitIndex++;
|
||||||
|
} else {
|
||||||
|
result += ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
const ContactForm = () => {
|
const ContactForm = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
const [countryCode, setCountryCode] = useState("+351");
|
||||||
|
const [phoneDigits, setPhoneDigits] = useState("");
|
||||||
|
|
||||||
|
const activePattern = PHONE_PATTERNS[countryCode];
|
||||||
|
const formattedPhone = formatPhoneNumber(phoneDigits, activePattern);
|
||||||
|
|
||||||
|
const handleCountryChange = (code: string) => {
|
||||||
|
setCountryCode(code);
|
||||||
|
setPhoneDigits((prev) =>
|
||||||
|
prev.slice(0, maxDigitsFor(PHONE_PATTERNS[code]))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (raw: string) => {
|
||||||
|
const digits = digitsOnly(raw).slice(0, maxDigitsFor(activePattern));
|
||||||
|
setPhoneDigits(digits);
|
||||||
|
};
|
||||||
|
|
||||||
const notify = () => {
|
const notify = () => {
|
||||||
toast.success(t("contact.toasts.success"));
|
toast.success(t("contact.toasts.success"));
|
||||||
@@ -22,11 +71,12 @@ const ContactForm = () => {
|
|||||||
const sendEmail = (e: React.FormEvent<HTMLFormElement>) => {
|
const sendEmail = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
let isEmpty = Array.from(e.currentTarget.elements).some(
|
const isEmpty = Array.from(e.currentTarget.elements).some((el) => {
|
||||||
(input) =>
|
if (el.tagName !== "INPUT" && el.tagName !== "TEXTAREA") return false;
|
||||||
(input as HTMLInputElement).type !== "submit" &&
|
const field = el as HTMLInputElement | HTMLTextAreaElement;
|
||||||
(input as HTMLInputElement).value.trim() === ""
|
if (field.type === "hidden") return false;
|
||||||
);
|
return field.value.trim() === "";
|
||||||
|
});
|
||||||
|
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
emptyForm();
|
emptyForm();
|
||||||
@@ -34,7 +84,7 @@ const ContactForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emailjs
|
emailjs
|
||||||
.sendForm("service_42i3ig6", "template_f30zlog", e.currentTarget, {
|
.sendForm("service_o7kxotj", "template_f30zlog", e.currentTarget, {
|
||||||
publicKey: "vcSOwPedqkRH3nmr9",
|
publicKey: "vcSOwPedqkRH3nmr9",
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
@@ -49,20 +99,31 @@ const ContactForm = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
e.currentTarget.reset();
|
e.currentTarget.reset();
|
||||||
|
setPhoneDigits("");
|
||||||
|
setCountryCode("+351");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 mx-5 lg:mx-0 flex justify-center items-center">
|
<div className="mt-10 mx-5 mb-10 lg:mx-0 flex justify-center items-center">
|
||||||
<form onSubmit={sendEmail} className="w-full md:w-auto">
|
<motion.form
|
||||||
|
onSubmit={sendEmail}
|
||||||
|
className="w-full md:w-[42rem] rounded-3xl glass-panel shadow-glass-lg p-6 md:p-10"
|
||||||
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 24 }}
|
||||||
|
whileInView={
|
||||||
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
|
viewport={{ once: true, margin: "-80px" }}
|
||||||
|
transition={{ type: "spring", damping: 22, stiffness: 140 }}
|
||||||
|
>
|
||||||
<div className="flex flex-col md:flex-row md:gap-3">
|
<div className="flex flex-col md:flex-row md:gap-3">
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input flex-1"
|
||||||
type="text"
|
type="text"
|
||||||
name="user_name"
|
name="user_name"
|
||||||
placeholder={t("contact.form.name")}
|
placeholder={t("contact.form.name")}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input flex-1"
|
||||||
type="email"
|
type="email"
|
||||||
name="user_email"
|
name="user_email"
|
||||||
placeholder={t("contact.form.email")}
|
placeholder={t("contact.form.email")}
|
||||||
@@ -70,18 +131,25 @@ const ContactForm = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col md:flex-row md:gap-3">
|
<div className="flex flex-col md:flex-row md:gap-3">
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input flex-1"
|
||||||
type="text"
|
type="text"
|
||||||
name="user_company"
|
name="user_company"
|
||||||
placeholder={t("contact.form.company")}
|
placeholder={t("contact.form.company")}
|
||||||
/>
|
/>
|
||||||
|
<div className="phone-input-group flex-1 flex items-center">
|
||||||
|
<PhoneCodeSelect value={countryCode} onChange={handleCountryChange} />
|
||||||
|
<span className="h-5 w-px bg-white/10 mx-2" aria-hidden="true" />
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="phone-number-input"
|
||||||
type="number"
|
type="tel"
|
||||||
|
inputMode="numeric"
|
||||||
name="user_phone"
|
name="user_phone"
|
||||||
|
value={formattedPhone}
|
||||||
|
onChange={(e) => handlePhoneChange(e.target.value)}
|
||||||
placeholder={t("contact.form.phone")}
|
placeholder={t("contact.form.phone")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input"
|
||||||
@@ -91,20 +159,22 @@ const ContactForm = () => {
|
|||||||
></input>
|
></input>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
className="form-input"
|
className="form-input resize-none"
|
||||||
name="message"
|
name="message"
|
||||||
placeholder={t("contact.form.message")}
|
placeholder={t("contact.form.message")}
|
||||||
rows={6}
|
rows={5}
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex mt-5 mb-10 lg:mt-12 mx-10 lg:mx-20 rounded-xl bg-gradient-to-br from-gold-light to-gold-dark p-0.5 shadow-lg">
|
<div className="flex mt-6 rounded-xl bg-gradient-to-br from-gold-light to-gold-mid p-0.5 shadow-gold-glow">
|
||||||
<button
|
<motion.button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex-1 font-medium md:px-8 py-3 text-2xl md:text-3xl bg-gradient-to-br from-custom-bglight to-custom-bgdark rounded-xl hover:bg-transparent hover:from-transparent hover:to-transparent transition duration-500"
|
className="flex-1 font-semibold px-8 py-3 text-xl md:text-2xl bg-custom-bgdark text-white rounded-[0.65rem] hover:bg-transparent transition-colors duration-300"
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
>
|
>
|
||||||
{t("contact.form.button")}
|
{t("contact.form.button")}
|
||||||
</button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
@@ -119,7 +189,7 @@ const ContactForm = () => {
|
|||||||
pauseOnHover
|
pauseOnHover
|
||||||
theme="dark"
|
theme="dark"
|
||||||
/>
|
/>
|
||||||
</form>
|
</motion.form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,22 +1,48 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import ContactForm from "./ContactForm";
|
import ContactForm from "./ContactForm";
|
||||||
|
|
||||||
const ContactPage = () => {
|
const ContactPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<div className="header-text">
|
<div className="header-text">
|
||||||
<h1>
|
<motion.h1
|
||||||
|
initial={
|
||||||
|
prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 20 }
|
||||||
|
}
|
||||||
|
whileInView={
|
||||||
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
transition={{ type: "spring", damping: 22, stiffness: 140 }}
|
||||||
|
>
|
||||||
{t("contact.heading.part1")} <br />
|
{t("contact.heading.part1")} <br />
|
||||||
{t("contact.heading.part2")}{" "}
|
{t("contact.heading.part2")}{" "}
|
||||||
<span className="gold-text">{t("contact.heading.part3")}</span>{" "}
|
<span className="gold-text">{t("contact.heading.part3")}</span>{" "}
|
||||||
{t("contact.heading.part4")}
|
{t("contact.heading.part4")}
|
||||||
</h1>
|
</motion.h1>
|
||||||
<h2 className="sub-header-text ">
|
<motion.h2
|
||||||
|
className="sub-header-text"
|
||||||
|
initial={
|
||||||
|
prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 20 }
|
||||||
|
}
|
||||||
|
whileInView={
|
||||||
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
damping: 22,
|
||||||
|
stiffness: 140,
|
||||||
|
delay: 0.05,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{t("contact.subheading.part1")} <br />
|
{t("contact.subheading.part1")} <br />
|
||||||
{t("contact.subheading.part2")}
|
{t("contact.subheading.part2")}
|
||||||
</h2>
|
</motion.h2>
|
||||||
</div>
|
</div>
|
||||||
<ContactForm />
|
<ContactForm />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface Country {
|
||||||
|
code: string;
|
||||||
|
iso: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COUNTRIES: Country[] = [
|
||||||
|
{ code: "+351", iso: "pt", label: "Portugal" },
|
||||||
|
{ code: "+34", iso: "es", label: "Spain" },
|
||||||
|
{ code: "+33", iso: "fr", label: "France" },
|
||||||
|
{ code: "+44", iso: "gb", label: "United Kingdom" },
|
||||||
|
{ code: "+1", iso: "us", label: "United States" },
|
||||||
|
{ code: "+55", iso: "br", label: "Brazil" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const flagUrl = (iso: string) => `https://flagcdn.com/${iso}.svg`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: string;
|
||||||
|
onChange: (code: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PhoneCodeSelect = ({ value, onChange }: Props) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const active = COUNTRIES.find((c) => c.code === value) ?? COUNTRIES[0];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const handlePointerDown = (e: MouseEvent) => {
|
||||||
|
if (
|
||||||
|
containerRef.current &&
|
||||||
|
!containerRef.current.contains(e.target as Node)
|
||||||
|
) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handlePointerDown);
|
||||||
|
return () => document.removeEventListener("mousedown", handlePointerDown);
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative" ref={containerRef}>
|
||||||
|
<input type="hidden" name="phone_code" value={active.code} />
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1.5 outline-none"
|
||||||
|
onClick={() => setOpen((o) => !o)}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
|
aria-label="Country code"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={flagUrl(active.iso)}
|
||||||
|
alt={active.label}
|
||||||
|
className="w-5 h-[14px] rounded-[2px] object-cover shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="text-sm md:text-base text-white font-medium leading-none">
|
||||||
|
{active.code}
|
||||||
|
</span>
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 0.95, y: -6 }}
|
||||||
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95, y: -6 }}
|
||||||
|
transition={{ type: "spring", damping: 24, stiffness: 300 }}
|
||||||
|
style={{ transformOrigin: "top left" }}
|
||||||
|
className="absolute left-0 top-full mt-2 w-48 rounded-xl glass-panel shadow-glass-lg overflow-hidden z-20"
|
||||||
|
>
|
||||||
|
{COUNTRIES.map((country) => (
|
||||||
|
<button
|
||||||
|
key={country.code}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
onChange(country.code);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className={`flex w-full items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${
|
||||||
|
country.code === active.code
|
||||||
|
? "text-gold-light bg-white/5"
|
||||||
|
: "text-white/70 hover:bg-white/5 hover:text-white"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={flagUrl(country.iso)}
|
||||||
|
alt={country.label}
|
||||||
|
className="w-5 h-[14px] rounded-[2px] object-cover shrink-0"
|
||||||
|
/>
|
||||||
|
<span className="flex-1">{country.label}</span>
|
||||||
|
<span className="text-white/40">{country.code}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PhoneCodeSelect;
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import HeroImg from "../../../assets/HeroImg.svg";
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import Stars from "../../../assets/Stars.svg";
|
import Stars from "../../../assets/Stars.svg";
|
||||||
|
import HeroVisual from "./HeroVisual";
|
||||||
|
|
||||||
const Hero = () => {
|
const Hero = () => {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
|
// Browsers report region variants (e.g. "en-US", "pt-BR"), not just the
|
||||||
|
// bare "en"/"pt" i18next is configured with - matching on that exactly
|
||||||
|
// would pick the wrong sized text class for most real visitors.
|
||||||
|
const currentLang = i18n.language?.startsWith("pt") ? "pt" : "en";
|
||||||
|
|
||||||
const handleNavigation = (path: string) => {
|
const handleNavigation = (path: string) => {
|
||||||
const element = document.getElementById(path);
|
const element = document.getElementById(path);
|
||||||
const navbarHeight = 110; // Replace with your navbar's height
|
const navbarHeight = 110;
|
||||||
if (element) {
|
if (element) {
|
||||||
const top =
|
const top =
|
||||||
element.getBoundingClientRect().top + window.scrollY - navbarHeight;
|
element.getBoundingClientRect().top + window.scrollY - navbarHeight;
|
||||||
@@ -15,51 +22,92 @@ const Hero = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rise = (delay = 0) =>
|
||||||
|
prefersReducedMotion
|
||||||
|
? { initial: { opacity: 0 }, animate: { opacity: 1 } }
|
||||||
|
: {
|
||||||
|
initial: { opacity: 0, y: 24 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
transition: {
|
||||||
|
type: "spring" as const,
|
||||||
|
damping: 22,
|
||||||
|
stiffness: 140,
|
||||||
|
delay,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col lg:mt-20 lg:flex-row 1920:padding-calc items-center">
|
<div className="flex flex-col lg:mt-20 lg:flex-row 1920:padding-calc items-center">
|
||||||
<div className="lg:w-1/2 w-full xl:w-1/2 hidden lg:flex transform origin-left">
|
<motion.div
|
||||||
<img className="items-center" src={HeroImg} alt="hero"></img>
|
className="lg:w-1/2 w-full xl:w-1/2 hidden lg:flex"
|
||||||
</div>
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, x: -30 }}
|
||||||
|
animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, x: 0 }}
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 100 }}
|
||||||
|
>
|
||||||
|
<HeroVisual />
|
||||||
|
</motion.div>
|
||||||
<div className="justify-center items-center lg:w-1/2 xl:w-1/2">
|
<div className="justify-center items-center lg:w-1/2 xl:w-1/2">
|
||||||
<div className="flex flex-col text-center lg:pt-10 lg:pr-20 lg:text-right 2xl:pr-40">
|
<div className="flex flex-col text-center lg:pt-10 lg:pr-20 lg:text-right 2xl:pr-40">
|
||||||
<img
|
<motion.img
|
||||||
src={Stars}
|
src={Stars}
|
||||||
alt="hero"
|
alt="hero"
|
||||||
className="md:px-0 ml-auto mr-auto lg:mr-0 lg:text-right"
|
className="md:px-0 ml-auto mr-auto lg:mr-0 lg:text-right"
|
||||||
></img>
|
{...rise(0)}
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="hero-text">{t("hero.heading.part1")}</h1>
|
<motion.h1
|
||||||
<h1 className="hero-text">{t("hero.heading.part2")}</h1>
|
className={currentLang === "en" ? "hero-text" : "hero-text-pt"}
|
||||||
<div>
|
{...rise(0.05)}
|
||||||
<img
|
>
|
||||||
src={HeroImg}
|
{t("hero.heading.part1")}
|
||||||
alt="heroSm"
|
</motion.h1>
|
||||||
className="flex mt-6 lg:hidden"
|
<motion.h1
|
||||||
></img>
|
className={currentLang === "en" ? "hero-text" : "hero-text-pt"}
|
||||||
|
{...rise(0.1)}
|
||||||
|
>
|
||||||
|
<span className="gold-text">{t("hero.heading.part2")}</span>
|
||||||
|
</motion.h1>
|
||||||
|
<div className="flex mt-6 lg:hidden">
|
||||||
|
<HeroVisual />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-5 lg:mx-0">
|
<motion.div className="mx-5 lg:mx-0" {...rise(0.15)}>
|
||||||
<h2 className="italic text-lg md:text-2xl 2xl:text-3xl mt-4 md:mt-10">
|
<h2
|
||||||
|
className={
|
||||||
|
currentLang === "en"
|
||||||
|
? "hero-sub-text mt-4 md:mt-10"
|
||||||
|
: "hero-sub-text-pt mt-4 md:mt-10"
|
||||||
|
}
|
||||||
|
>
|
||||||
{t("hero.subheading.part1")}
|
{t("hero.subheading.part1")}
|
||||||
</h2>
|
</h2>
|
||||||
<h2 className="italic text-lg md:text-2xl 2xl:text-3xl">
|
<h2
|
||||||
|
className={
|
||||||
|
currentLang === "en" ? "hero-sub-text" : "hero-sub-text-pt"
|
||||||
|
}
|
||||||
|
>
|
||||||
{t("hero.subheading.part2")}
|
{t("hero.subheading.part2")}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</motion.div>
|
||||||
<div className="flex mt-5 lg:mt-12 mx-10 lg:mx-0 lg:ml-auto rounded-xl bg-gradient-to-br from-gold-light to-gold-dark p-0.5 shadow-lg">
|
<motion.div
|
||||||
<a
|
className="flex mt-5 lg:mt-12 mx-10 lg:mx-0 lg:ml-auto rounded-xl bg-gradient-to-br from-gold-light to-gold-dark p-0.5 shadow-gold-glow"
|
||||||
|
{...rise(0.2)}
|
||||||
|
>
|
||||||
|
<motion.a
|
||||||
className="flex w-full"
|
className="flex w-full"
|
||||||
href="Contact"
|
href="Contact"
|
||||||
|
whileTap={{ scale: 0.97 }}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleNavigation("Contact");
|
handleNavigation("Contact");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<button className="flex-1 font-medium md:px-8 py-3 text-2xl md:text-3xl bg-gradient-to-br from-custom-bglight to-custom-bgdark rounded-xl hover:bg-transparent hover:from-transparent hover:to-transparent transition duration-500">
|
<button className="flex-1 font-medium md:px-8 py-3 text-2xl md:text-3xl bg-custom-bgdark rounded-xl hover:bg-transparent transition-colors duration-300">
|
||||||
{t("hero.button")}
|
{t("hero.button")}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</motion.a>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
import Logo from "../../../assets/logo.svg";
|
||||||
|
|
||||||
|
interface OrbitConfig {
|
||||||
|
rotateZ: number;
|
||||||
|
rotateX: number;
|
||||||
|
duration: number;
|
||||||
|
reverse: boolean;
|
||||||
|
inset: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orbits: OrbitConfig[] = [
|
||||||
|
{ rotateZ: 55, rotateX: 66, duration: 9, reverse: false, inset: "6%" },
|
||||||
|
{ rotateZ: -55, rotateX: 66, duration: 7, reverse: true, inset: "16%" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const HeroVisual = () => {
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full max-w-xs sm:max-w-sm lg:max-w-lg mx-auto aspect-square">
|
||||||
|
<div className="absolute inset-[26%] rounded-full bg-gradient-to-br from-gold-light to-gold-dark opacity-30 blur-3xl" />
|
||||||
|
|
||||||
|
{orbits.map((orbit, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="absolute rounded-full border border-gold-light/30"
|
||||||
|
style={{
|
||||||
|
inset: orbit.inset,
|
||||||
|
transform: `rotateZ(${orbit.rotateZ}deg) rotateX(${orbit.rotateX}deg)`,
|
||||||
|
}}
|
||||||
|
initial={{ opacity: 0, scale: 0.85 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
damping: 20,
|
||||||
|
stiffness: 100,
|
||||||
|
delay: i * 0.15,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0"
|
||||||
|
animate={{
|
||||||
|
rotate: prefersReducedMotion
|
||||||
|
? orbit.reverse
|
||||||
|
? -30
|
||||||
|
: 30
|
||||||
|
: orbit.reverse
|
||||||
|
? -360
|
||||||
|
: 360,
|
||||||
|
}}
|
||||||
|
transition={
|
||||||
|
prefersReducedMotion
|
||||||
|
? { duration: 0.6, ease: "easeOut" }
|
||||||
|
: {
|
||||||
|
duration: orbit.duration,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "linear",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="absolute -top-[5px] left-1/2 -translate-x-1/2 h-2.5 w-2.5 rounded-full bg-gold-light shadow-[0_0_10px_2px_rgba(250,204,151,0.55)]" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-[32%] rounded-full glass-panel shadow-glass-lg flex items-center justify-center z-10"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ type: "spring", damping: 20, stiffness: 100 }}
|
||||||
|
>
|
||||||
|
<img src={Logo} alt="LK Results" className="w-2/5 h-2/5 opacity-90" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HeroVisual;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import Card from "../../Card";
|
import Card from "../../Card";
|
||||||
import yourself from "../../../assets/Yourself.svg"
|
import yourself from "../../../assets/Yourself.svg"
|
||||||
import staff from "../../../assets/staff.svg"
|
import staff from "../../../assets/Staff.svg"
|
||||||
import agency from "../../../assets/agency.svg"
|
import agency from "../../../assets/Agency.svg"
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const CardContainer = () => {
|
const CardContainer = () => {
|
||||||
@@ -9,21 +9,24 @@ const CardContainer = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 lg:mt-32 flex gap-10 flex-col h-full lg:flex-row px-10 lg:px-32 1920:margin-calc">
|
<div className="mt-10 lg:mt-16 flex gap-5 lg:gap-6 flex-col h-full lg:flex-row px-10 lg:px-32 1920:margin-calc">
|
||||||
<Card
|
<Card
|
||||||
title={t("optimization.cards.card1.title")}
|
title={t("optimization.cards.card1.title")}
|
||||||
description={t("optimization.cards.card1.description")}
|
description={t("optimization.cards.card1.description")}
|
||||||
image={yourself}
|
image={yourself}
|
||||||
|
index={0}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title={t("optimization.cards.card2.title")}
|
title={t("optimization.cards.card2.title")}
|
||||||
description={t("optimization.cards.card2.description")}
|
description={t("optimization.cards.card2.description")}
|
||||||
image={staff}
|
image={staff}
|
||||||
|
index={1}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title={t("optimization.cards.card3.title")}
|
title={t("optimization.cards.card3.title")}
|
||||||
description={t("optimization.cards.card3.description")}
|
description={t("optimization.cards.card3.description")}
|
||||||
image={agency}
|
image={agency}
|
||||||
|
index={2}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import CardContainer from './CardContainer'
|
import CardContainer from "./CardContainer";
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
|
||||||
const Optimization = () => {
|
const Optimization = () => {
|
||||||
const { t } = useTranslation('optimization')
|
const { t } = useTranslation();
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header-text">
|
<div className="header-text">
|
||||||
<h1>
|
<motion.h1
|
||||||
<span className="gold-text">"</span>
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 20 }}
|
||||||
{t("optimization.heading.part1")} <br /> {t("optimization.heading.part2")}
|
whileInView={
|
||||||
<span className="gold-text">"</span>
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
||||||
</h1>
|
}
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
transition={{ type: "spring", damping: 22, stiffness: 140 }}
|
||||||
|
>
|
||||||
|
<span className="gold-text">“</span>
|
||||||
|
{t("optimization.heading.part1")} <br />{" "}
|
||||||
|
{t("optimization.heading.part2")}
|
||||||
|
<span className="gold-text">”</span>
|
||||||
|
</motion.h1>
|
||||||
<CardContainer />
|
<CardContainer />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default Optimization
|
export default Optimization;
|
||||||
@@ -5,26 +5,30 @@ const CardContainer = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex-col h-full lg:grid lg:grid-cols-2 lg:gap-10 px-10 lg:px-40 2xl:px-96 1920:margin-calc">
|
<div className="mt-10 flex flex-col gap-5 h-full lg:grid lg:grid-cols-2 lg:gap-6 px-10 lg:px-40 2xl:px-96 1920:margin-calc">
|
||||||
<Card
|
<Card
|
||||||
title={t("results.cards.card1.title")}
|
title={t("results.cards.card1.title")}
|
||||||
description={t("results.cards.card1.description")}
|
description={t("results.cards.card1.description")}
|
||||||
image=""
|
image=""
|
||||||
|
index={0}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title={t("results.cards.card2.title")}
|
title={t("results.cards.card2.title")}
|
||||||
description={t("results.cards.card2.description")}
|
description={t("results.cards.card2.description")}
|
||||||
image=""
|
image=""
|
||||||
|
index={1}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title={t("results.cards.card3.title")}
|
title={t("results.cards.card3.title")}
|
||||||
description={t("results.cards.card3.description")}
|
description={t("results.cards.card3.description")}
|
||||||
image=""
|
image=""
|
||||||
|
index={2}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
title={t("results.cards.card4.title")}
|
title={t("results.cards.card4.title")}
|
||||||
description={t("results.cards.card4.description")}
|
description={t("results.cards.card4.description")}
|
||||||
image=""
|
image=""
|
||||||
|
index={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import CardContainer from "./CardContainer";
|
import CardContainer from "./CardContainer";
|
||||||
|
|
||||||
const ResultsPage = () => {
|
const ResultsPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header-text">
|
<div className="header-text">
|
||||||
<h1>
|
<motion.h1
|
||||||
<span className="gold-text">"</span>
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 20 }}
|
||||||
|
whileInView={
|
||||||
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
transition={{ type: "spring", damping: 22, stiffness: 140 }}
|
||||||
|
>
|
||||||
|
<span className="gold-text">“</span>
|
||||||
{t("results.heading.part1")} <br /> {t("results.heading.part2")}
|
{t("results.heading.part1")} <br /> {t("results.heading.part2")}
|
||||||
<span className="gold-text">"</span>
|
<span className="gold-text">”</span>
|
||||||
</h1>
|
</motion.h1>
|
||||||
<CardContainer />
|
<CardContainer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const Buttons = ({ changeActiveGroup }: props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lg:px-40 flex flex-row 2xl:gap-8 gap-2 mx-2 md:gap-5 mt-10 mb-4 flex-wrap justify-center">
|
<div className="flex flex-row gap-1 mx-auto w-full sm:w-fit justify-center rounded-full bg-black/25 border border-white/5 p-1">
|
||||||
<CardBtn
|
<CardBtn
|
||||||
title="Marketing"
|
title="Marketing"
|
||||||
onClick={() => handleClick("markCards")}
|
onClick={() => handleClick("markCards")}
|
||||||
@@ -28,12 +28,12 @@ const Buttons = ({ changeActiveGroup }: props) => {
|
|||||||
isSelected={selectedButton === "webCards"}
|
isSelected={selectedButton === "webCards"}
|
||||||
/>
|
/>
|
||||||
<CardBtn
|
<CardBtn
|
||||||
title="Social Media"
|
title="Social"
|
||||||
onClick={() => handleClick("smCards")}
|
onClick={() => handleClick("smCards")}
|
||||||
isSelected={selectedButton === "smCards"}
|
isSelected={selectedButton === "smCards"}
|
||||||
/>
|
/>
|
||||||
<CardBtn
|
<CardBtn
|
||||||
title="Camera & Edits"
|
title="Camera"
|
||||||
onClick={() => handleClick("camCards")}
|
onClick={() => handleClick("camCards")}
|
||||||
isSelected={selectedButton === "camCards"}
|
isSelected={selectedButton === "camCards"}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,30 +9,34 @@ const CameraCards = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex lg:mt-20 flex-col flex-wrap gap-4 xl:flex-row mx-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-6">
|
||||||
<Card
|
<Card
|
||||||
|
step={1}
|
||||||
title={t("services.cards.media.card1.title")}
|
title={t("services.cards.media.card1.title")}
|
||||||
description={t("services.cards.media.card1.description")}
|
description={t("services.cards.media.card1.description")}
|
||||||
imgSrc={EnvisionImg}
|
imgSrc={EnvisionImg}
|
||||||
delay="0s"
|
delay={0}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={2}
|
||||||
title={t("services.cards.media.card2.title")}
|
title={t("services.cards.media.card2.title")}
|
||||||
description={t("services.cards.media.card2.description")}
|
description={t("services.cards.media.card2.description")}
|
||||||
imgSrc={CaptureImg}
|
imgSrc={CaptureImg}
|
||||||
delay="0.2s"
|
delay={0.08}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={3}
|
||||||
title={t("services.cards.media.card3.title")}
|
title={t("services.cards.media.card3.title")}
|
||||||
description={t("services.cards.media.card3.description")}
|
description={t("services.cards.media.card3.description")}
|
||||||
imgSrc={EditImg}
|
imgSrc={EditImg}
|
||||||
delay="0.4s"
|
delay={0.16}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={4}
|
||||||
title={t("services.cards.media.card4.title")}
|
title={t("services.cards.media.card4.title")}
|
||||||
description={t("services.cards.media.card4.description")}
|
description={t("services.cards.media.card4.description")}
|
||||||
imgSrc={PublishImg}
|
imgSrc={PublishImg}
|
||||||
delay="0.6s"
|
delay={0.24}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
interface props {
|
interface props {
|
||||||
title: string;
|
title: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
@@ -5,20 +7,24 @@ interface props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CardBtn = ({ title, onClick, isSelected }: props) => {
|
const CardBtn = ({ title, onClick, isSelected }: props) => {
|
||||||
const buttonClass = isSelected
|
|
||||||
? "w-full font-bold md:px-4 py-1 text-xs lg:text-sm xl:text-lg bg-gradient-to-br from-custom-bglight to-custom-bgdark"
|
|
||||||
: "w-full font-medium md:px-4 py-1 text-xs lg:text-sm xl:text-lg bg-gradient-to-br from-custom-bglight to-custom-bgdark text-gray-400 shadow-inner";
|
|
||||||
|
|
||||||
const divClass = isSelected
|
|
||||||
? "flex flex-1 flex-row mt-5 lg:mt-12 bg-gradient-to-b from-gold-light to-gold-dark p-0.5 shadow-lg transform scale-110"
|
|
||||||
: "flex flex-1 flex-row mt-5 lg:mt-12 bg-gradient-to-b from-gold-light to-gold-dark p-0.5 shadow-lg";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={divClass}>
|
<motion.button
|
||||||
<button className={buttonClass} onClick={onClick}>
|
className={`relative flex-1 whitespace-nowrap rounded-full py-1.5 px-3 sm:px-4 text-xs sm:text-sm font-medium cursor-pointer transition-colors duration-300 ${
|
||||||
{title}
|
isSelected ? "text-custom-bgdark" : "text-white/50 hover:text-white"
|
||||||
</button>
|
}`}
|
||||||
</div>
|
onClick={onClick}
|
||||||
|
whileTap={{ scale: 0.96 }}
|
||||||
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
||||||
|
>
|
||||||
|
{isSelected && (
|
||||||
|
<motion.span
|
||||||
|
layoutId="service-tab-pill"
|
||||||
|
className="absolute inset-0 rounded-full bg-gradient-to-br from-gold-light to-gold-mid shadow-gold-glow"
|
||||||
|
transition={{ type: "spring", stiffness: 380, damping: 32 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="relative font-semibold">{title}</span>
|
||||||
|
</motion.button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
||||||
import WesiteCards from "./WesiteCards";
|
import WesiteCards from "./WesiteCards";
|
||||||
import Buttons from "./Buttons";
|
import Buttons from "./Buttons";
|
||||||
import MarketingCards from "./MarketingCards";
|
import MarketingCards from "./MarketingCards";
|
||||||
@@ -7,6 +8,7 @@ import SocialMediaCards from "./SocialMedia";
|
|||||||
|
|
||||||
const CardGroup = () => {
|
const CardGroup = () => {
|
||||||
const [activeGroup, setActiveGroup] = useState("webCards");
|
const [activeGroup, setActiveGroup] = useState("webCards");
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
const changeActiveGroup = (group: string) => {
|
const changeActiveGroup = (group: string) => {
|
||||||
setActiveGroup(group);
|
setActiveGroup(group);
|
||||||
@@ -31,9 +33,29 @@ const CardGroup = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full font-medium md:px-8 py-3 text-2xl md:text-3xl bg-gradient-to-br from-custom-bglight to-custom-bgdark rounded-xl flex flex-col">
|
<div className="flex flex-col gap-8 lg:gap-10">
|
||||||
{ActiveCards && <ActiveCards />}
|
|
||||||
<Buttons changeActiveGroup={changeActiveGroup} />
|
<Buttons changeActiveGroup={changeActiveGroup} />
|
||||||
|
<div className="relative">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{ActiveCards && (
|
||||||
|
<motion.div
|
||||||
|
key={activeGroup}
|
||||||
|
initial={
|
||||||
|
prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 12 }
|
||||||
|
}
|
||||||
|
animate={
|
||||||
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
|
exit={
|
||||||
|
prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: -12 }
|
||||||
|
}
|
||||||
|
transition={{ type: "spring", damping: 26, stiffness: 220 }}
|
||||||
|
>
|
||||||
|
<ActiveCards />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,30 +9,34 @@ const MarketingCards = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex lg:mt-20 flex-col flex-wrap gap-4 xl:flex-row mx-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-6">
|
||||||
<Card
|
<Card
|
||||||
|
step={1}
|
||||||
title={t("services.cards.marketing.card1.title")}
|
title={t("services.cards.marketing.card1.title")}
|
||||||
description={t("services.cards.marketing.card1.description")}
|
description={t("services.cards.marketing.card1.description")}
|
||||||
imgSrc={AnaliseImg}
|
imgSrc={AnaliseImg}
|
||||||
delay="0s"
|
delay={0}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={2}
|
||||||
title={t("services.cards.marketing.card2.title")}
|
title={t("services.cards.marketing.card2.title")}
|
||||||
description={t("services.cards.marketing.card2.description")}
|
description={t("services.cards.marketing.card2.description")}
|
||||||
imgSrc={CreateImg}
|
imgSrc={CreateImg}
|
||||||
delay="0.2s"
|
delay={0.08}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={3}
|
||||||
title={t("services.cards.marketing.card3.title")}
|
title={t("services.cards.marketing.card3.title")}
|
||||||
description={t("services.cards.marketing.card3.description")}
|
description={t("services.cards.marketing.card3.description")}
|
||||||
imgSrc={ScaleImg}
|
imgSrc={ScaleImg}
|
||||||
delay="0.4s"
|
delay={0.16}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={4}
|
||||||
title={t("services.cards.marketing.card4.title")}
|
title={t("services.cards.marketing.card4.title")}
|
||||||
description={t("services.cards.marketing.card4.description")}
|
description={t("services.cards.marketing.card4.description")}
|
||||||
imgSrc={ExecuteImg}
|
imgSrc={ExecuteImg}
|
||||||
delay="0.6s"
|
delay={0.24}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,35 +1,49 @@
|
|||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
step: number;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
imgSrc: string;
|
imgSrc: string;
|
||||||
delay: string;
|
delay: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Card = ({ title, description, imgSrc, delay }: IProps) => {
|
const Card = ({ step, title, description, imgSrc, delay }: IProps) => {
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<motion.div
|
||||||
className="flex flex-col justify-end flex-1 h-full border-solid border shadow-lg bg-cover bg-center text-center"
|
className="flex flex-col gap-4"
|
||||||
style={{
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 16 }}
|
||||||
backgroundImage: `url(${imgSrc})`,
|
animate={prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
|
||||||
animation: `fadeEffect 1s ${delay} forwards`,
|
transition={{
|
||||||
opacity: 0, // Add this line
|
type: "spring",
|
||||||
|
damping: 24,
|
||||||
|
stiffness: 160,
|
||||||
|
delay: prefersReducedMotion ? 0 : delay,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="lg:my-6">
|
<div className="relative aspect-[4/5] rounded-2xl overflow-hidden ring-1 ring-white/10">
|
||||||
<h2
|
<motion.div
|
||||||
className="drop-shadow-[0_1.2px_1.2px_rgba(0,0,0,0.8)]
|
className="absolute inset-0 bg-cover bg-center"
|
||||||
text-3xl font-bold"
|
style={{ backgroundImage: `url(${imgSrc})` }}
|
||||||
>
|
whileHover={prefersReducedMotion ? {} : { scale: 1.06 }}
|
||||||
|
transition={{ type: "spring", damping: 22, stiffness: 120 }}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/35 via-transparent to-transparent" />
|
||||||
|
<span className="absolute top-3 left-3 h-8 w-8 rounded-full glass-panel flex items-center justify-center text-xs font-bold text-gold-light">
|
||||||
|
{String(step).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-0.5">
|
||||||
|
<h3 className="text-lg lg:text-xl font-semibold text-white tracking-tight">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h3>
|
||||||
<p
|
<p className="mt-1.5 text-sm lg:text-base text-white/55 leading-snug">
|
||||||
className="drop-shadow-[0_1.2px_1.2px_rgba(0,0,0,0.8)]
|
|
||||||
xl:mb-10 font-thin text-xl"
|
|
||||||
>
|
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import CardGroup from "./CardGroup";
|
|||||||
|
|
||||||
const ServiceCardContainer = () => {
|
const ServiceCardContainer = () => {
|
||||||
return (
|
return (
|
||||||
<div className="h-full xl:h-[65vh] 1920:h-[60vh] rounded-xl mx-4 lg:mx-20 xl:mx-40 2xl:mx-60 1920:margin-calc mt-10 bg-gradient-to-br from-gold-light to-gold-dark p-0.5 shadow-lg ">
|
<div className="rounded-3xl mx-4 lg:mx-16 xl:mx-24 2xl:mx-48 1920:margin-calc mt-10 glass-panel shadow-glass-lg p-5 md:p-8 lg:p-10">
|
||||||
<CardGroup />
|
<CardGroup />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import Underline from "../../../assets/Underline.svg";
|
import Underline from "../../../assets/Underline.svg";
|
||||||
import ServiceCardContainer from "./ServiceCardContainer";
|
import ServiceCardContainer from "./ServiceCardContainer";
|
||||||
|
|
||||||
const Services = () => {
|
const Services = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const prefersReducedMotion = useReducedMotion();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header-text">
|
<div className="header-text">
|
||||||
<h1>
|
<motion.h1
|
||||||
{t("services.heading.part1")}{" "}
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 20 }}
|
||||||
|
whileInView={
|
||||||
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
transition={{ type: "spring", damping: 22, stiffness: 140 }}
|
||||||
|
>
|
||||||
|
{t("services.heading.part1")}
|
||||||
<span className="relative inline-block">
|
<span className="relative inline-block">
|
||||||
{t("services.heading.part2")}{" "}
|
{t("services.heading.part2")}{" "}
|
||||||
<img
|
<img
|
||||||
@@ -17,15 +26,17 @@ const Services = () => {
|
|||||||
className="absolute bottom-0 left-0"
|
className="absolute bottom-0 left-0"
|
||||||
/>
|
/>
|
||||||
</span>{" "}
|
</span>{" "}
|
||||||
</h1>
|
</motion.h1>
|
||||||
<h1>
|
<motion.h1
|
||||||
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 20 }}
|
||||||
|
whileInView={
|
||||||
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
transition={{ type: "spring", damping: 22, stiffness: 140, delay: 0.05 }}
|
||||||
|
>
|
||||||
{t("services.heading.part3")}
|
{t("services.heading.part3")}
|
||||||
<span className="gold-text">{t("services.heading.part4")}</span>
|
</motion.h1>
|
||||||
{t("services.heading.part5")}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<h2 className="sub-header-text">{t("services.subheading")}</h2>
|
|
||||||
|
|
||||||
<ServiceCardContainer />
|
<ServiceCardContainer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,30 +9,34 @@ const SocialMediaCards = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex lg:mt-20 flex-col flex-wrap gap-4 xl:flex-row mx-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-6">
|
||||||
<Card
|
<Card
|
||||||
|
step={1}
|
||||||
title={t("services.cards.social.card1.title")}
|
title={t("services.cards.social.card1.title")}
|
||||||
description={t("services.cards.social.card1.description")}
|
description={t("services.cards.social.card1.description")}
|
||||||
imgSrc={AlgorithmImg}
|
imgSrc={AlgorithmImg}
|
||||||
delay="0s"
|
delay={0}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={2}
|
||||||
title={t("services.cards.social.card2.title")}
|
title={t("services.cards.social.card2.title")}
|
||||||
description={t("services.cards.social.card2.description")}
|
description={t("services.cards.social.card2.description")}
|
||||||
imgSrc={ScheduleImg}
|
imgSrc={ScheduleImg}
|
||||||
delay="0.2s"
|
delay={0.08}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={3}
|
||||||
title={t("services.cards.social.card3.title")}
|
title={t("services.cards.social.card3.title")}
|
||||||
description={t("services.cards.social.card3.description")}
|
description={t("services.cards.social.card3.description")}
|
||||||
imgSrc={TargetImg}
|
imgSrc={TargetImg}
|
||||||
delay="0.4s"
|
delay={0.16}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={4}
|
||||||
title={t("services.cards.social.card4.title")}
|
title={t("services.cards.social.card4.title")}
|
||||||
description={t("services.cards.social.card4.description")}
|
description={t("services.cards.social.card4.description")}
|
||||||
imgSrc={ExpandImg}
|
imgSrc={ExpandImg}
|
||||||
delay="0.6s"
|
delay={0.24}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,30 +9,34 @@ const WesiteCards = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex lg:mt-20 flex-col flex-wrap gap-4 xl:flex-row mx-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-6">
|
||||||
<Card
|
<Card
|
||||||
|
step={1}
|
||||||
title={t("services.cards.website.card1.title")}
|
title={t("services.cards.website.card1.title")}
|
||||||
description={t("services.cards.website.card1.description")}
|
description={t("services.cards.website.card1.description")}
|
||||||
imgSrc={IdealizeImg}
|
imgSrc={IdealizeImg}
|
||||||
delay="0s"
|
delay={0}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={2}
|
||||||
title={t("services.cards.website.card2.title")}
|
title={t("services.cards.website.card2.title")}
|
||||||
description={t("services.cards.website.card2.description")}
|
description={t("services.cards.website.card2.description")}
|
||||||
imgSrc={DesignImg}
|
imgSrc={DesignImg}
|
||||||
delay="0.2s"
|
delay={0.08}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={3}
|
||||||
title={t("services.cards.website.card3.title")}
|
title={t("services.cards.website.card3.title")}
|
||||||
description={t("services.cards.website.card3.description")}
|
description={t("services.cards.website.card3.description")}
|
||||||
imgSrc={CodeImg}
|
imgSrc={CodeImg}
|
||||||
delay="0.4s"
|
delay={0.16}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
step={4}
|
||||||
title={t("services.cards.website.card4.title")}
|
title={t("services.cards.website.card4.title")}
|
||||||
description={t("services.cards.website.card4.description")}
|
description={t("services.cards.website.card4.description")}
|
||||||
imgSrc={BuildImg}
|
imgSrc={BuildImg}
|
||||||
delay="0.6s"
|
delay={0.24}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-1
@@ -13,9 +13,11 @@ i18n
|
|||||||
// init i18next
|
// init i18next
|
||||||
// for all options read: https://www.i18next.com/overview/configuration-options
|
// for all options read: https://www.i18next.com/overview/configuration-options
|
||||||
.init({
|
.init({
|
||||||
|
ns: ['common', 'TOS'],
|
||||||
|
defaultNS: 'common',
|
||||||
backend: {
|
backend: {
|
||||||
// translation file path
|
// translation file path
|
||||||
loadPath: "/locales/{{lng}}.json",
|
loadPath: (lng : string, ns : string) => `/locales/${ns}/${lng}.json`,
|
||||||
},
|
},
|
||||||
debug: true,
|
debug: true,
|
||||||
fallbackLng: "en",
|
fallbackLng: "en",
|
||||||
|
|||||||
+144
-9
@@ -1,42 +1,177 @@
|
|||||||
@import url("https://fonts.googleapis.com/css2?family=Bai+Jamjuree:wght@400;700&display=swap");
|
@import url("https://fonts.googleapis.com/css2?family=Bai+Jamjuree:wght@400;500;600;700&display=swap");
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
body {
|
::selection {
|
||||||
@apply bg-gradient-to-br from-custom-bglight to-custom-bgdark;
|
background: rgba(250, 204, 151, 0.25);
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply bg-custom-bgdark text-white;
|
||||||
|
background: radial-gradient(120% 90% at 50% -10%, #26262c 0%, #131316 42%, #06060b 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #06060b;
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: linear-gradient(180deg, #facc97, #54330f);
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid #06060b;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus-visible {
|
||||||
|
outline: 2px solid #facc97;
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display / hero type — negative tracking + tight leading, sized down for room to breathe */
|
||||||
.hero-text {
|
.hero-text {
|
||||||
@apply font-medium text-5xl lg:text-7xl xl:text-8xl;
|
@apply font-semibold text-5xl lg:text-7xl xl:text-8xl;
|
||||||
|
line-height: 1.02;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.hero-text-pt {
|
||||||
|
@apply font-semibold text-4xl lg:text-6xl xl:text-7xl;
|
||||||
|
line-height: 1.04;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-sub-text {
|
||||||
|
@apply text-lg md:text-2xl 2xl:text-3xl text-white/60;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: -0.005em;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
.hero-sub-text-pt {
|
||||||
|
@apply text-lg md:text-xl 2xl:text-2xl text-white/60;
|
||||||
|
line-height: 1.4;
|
||||||
|
letter-spacing: 0;
|
||||||
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-text {
|
.header-text {
|
||||||
@apply mt-20 lg:mt-48 text-3xl md:text-5xl xl:text-7xl text-center;
|
@apply mt-20 lg:mt-48 text-3xl md:text-5xl xl:text-7xl text-center font-semibold;
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub-header-text {
|
.sub-header-text {
|
||||||
@apply italic font-thin block text-sm mx-10 md:text-2xl mt-5 text-gray-400;
|
@apply block text-sm mx-10 md:text-2xl mt-5 text-white/50;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input {
|
.form-input {
|
||||||
@apply rounded-xl px-4 py-2 mt-2 bg-gray-bg border-solid border border-gray-300 text-sm md:text-lg;
|
@apply rounded-xl px-4 py-3 mt-2 bg-white/[0.03] border border-white/10 text-sm md:text-base text-white placeholder-white/35 transition-colors duration-200;
|
||||||
|
}
|
||||||
|
.form-input:focus {
|
||||||
|
@apply border-gold-light/50 bg-white/[0.05];
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-input-group {
|
||||||
|
@apply rounded-xl mt-2 px-4 py-3 bg-white/[0.03] border border-white/10 transition-colors duration-200;
|
||||||
|
}
|
||||||
|
.phone-input-group:focus-within {
|
||||||
|
@apply border-gold-light/50 bg-white/[0.05];
|
||||||
|
}
|
||||||
|
.phone-number-input {
|
||||||
|
@apply flex-1 min-w-0 bg-transparent text-white placeholder-white/35 text-sm md:text-base pl-2 outline-none leading-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gold-text {
|
.gold-text {
|
||||||
@apply bg-clip-text text-transparent bg-gradient-to-br from-gold-light to-gold-dark;
|
@apply bg-clip-text text-transparent bg-gradient-to-br from-gold-light to-gold-mid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-effect {
|
.fade-effect {
|
||||||
animation: fadeEffect 1s ease-in-out;
|
animation: fadeEffect 0.6s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text {
|
||||||
|
@apply text-white/45 font-medium text-xs md:text-sm tracking-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-text-pt {
|
||||||
|
@apply text-white/45 font-medium text-xs md:text-sm tracking-wide;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass material — the floating functional layer (§12) */
|
||||||
|
.glass-panel {
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.015));
|
||||||
|
border: 1px solid rgba(250, 204, 151, 0.14);
|
||||||
|
backdrop-filter: blur(20px) saturate(160%);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(160%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-nav {
|
||||||
|
background: rgba(10, 10, 13, 0.55);
|
||||||
|
backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||||
|
border: 1px solid rgba(250, 204, 151, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Heavier material for full takeover surfaces (mobile menu sheet) — a
|
||||||
|
bigger surface needs to read as thicker, per the materials guidance. */
|
||||||
|
.glass-sheet {
|
||||||
|
background: rgba(14, 14, 17, 0.92);
|
||||||
|
backdrop-filter: blur(28px) saturate(150%);
|
||||||
|
-webkit-backdrop-filter: blur(28px) saturate(150%);
|
||||||
|
border: 1px solid rgba(250, 204, 151, 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-transparency: reduce) {
|
||||||
|
.glass-sheet {
|
||||||
|
background: #0c0c10;
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeEffect {
|
@keyframes fadeEffect {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
transform: translateY(6px);
|
||||||
}
|
}
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
.glass-panel,
|
||||||
|
.glass-nav {
|
||||||
|
background: #0c0c10;
|
||||||
|
border-color: rgba(250, 204, 151, 0.4);
|
||||||
|
backdrop-filter: none;
|
||||||
|
-webkit-backdrop-filter: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-1
@@ -5,11 +5,12 @@ import "./index.css";
|
|||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
import "./i18n";
|
import "./i18n";
|
||||||
|
import Loading from "./Components/Loading.tsx";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Suspense fallback="...is loading">
|
<Suspense fallback={<Loading />}>
|
||||||
<App />
|
<App />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Navbar from "../Components/Navbar/Navbar";
|
||||||
|
import Footer from "../Components/Footer/Footer";
|
||||||
|
|
||||||
|
const PP = () => {
|
||||||
|
const { t } = useTranslation("PP");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<div className="container mx-auto px-4 py-12 max-w-4xl">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-semibold text-center mb-10 tracking-tight">
|
||||||
|
{t("pp.pageTitle")}
|
||||||
|
</h1>
|
||||||
|
<div className="glass-panel shadow-glass-lg rounded-3xl p-6 md:p-12">
|
||||||
|
<p className="mb-4 text-white/45 text-sm">{t("pp.lastUpdated")}</p>
|
||||||
|
<p className="mb-8 text-white/70 leading-relaxed">
|
||||||
|
{t("pp.introText")}
|
||||||
|
</p>
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("pp.pp1.title")}
|
||||||
|
</h2>
|
||||||
|
{t("pp.pp1.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("pp.pp2.title")}
|
||||||
|
</h2>
|
||||||
|
{t("pp.pp2.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("pp.pp3.title")}
|
||||||
|
</h2>
|
||||||
|
{t("pp.pp3.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("pp.pp4.title")}
|
||||||
|
</h2>
|
||||||
|
{t("pp.pp4.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("pp.pp5.title")}
|
||||||
|
</h2>
|
||||||
|
{t("pp.pp5.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("pp.pp6.title")}
|
||||||
|
</h2>
|
||||||
|
{t("pp.pp6.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("pp.pp7.title")}
|
||||||
|
</h2>
|
||||||
|
{t("pp.pp7.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("pp.pp8.title")}
|
||||||
|
</h2>
|
||||||
|
{t("pp.pp8.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PP;
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Navbar from "../Components/Navbar/Navbar";
|
||||||
|
import Footer from "../Components/Footer/Footer";
|
||||||
|
|
||||||
|
const TOS = () => {
|
||||||
|
const { t } = useTranslation("TOS");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<div className="container mx-auto px-4 py-12 max-w-4xl">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-semibold text-center mb-10 tracking-tight">
|
||||||
|
{t("tos.pageTitle")}
|
||||||
|
</h1>
|
||||||
|
<div className="glass-panel shadow-glass-lg rounded-3xl p-6 md:p-12">
|
||||||
|
<p className="mb-4 text-white/45 text-sm">{t("tos.lastUpdated")}</p>
|
||||||
|
<p className="mb-8 text-white/70 leading-relaxed">
|
||||||
|
{t("tos.introText")}
|
||||||
|
</p>
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("tos.tos1.title")}
|
||||||
|
</h2>
|
||||||
|
{t("tos.tos1.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("tos.tos2.title")}
|
||||||
|
</h2>
|
||||||
|
{t("tos.tos2.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("tos.tos3.title")}
|
||||||
|
</h2>
|
||||||
|
{t("tos.tos3.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("tos.tos4.title")}
|
||||||
|
</h2>
|
||||||
|
{t("tos.tos4.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("tos.tos5.title")}
|
||||||
|
</h2>
|
||||||
|
{t("tos.tos5.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<h2 className="text-xl md:text-2xl font-semibold mb-4 text-gold-light">
|
||||||
|
{t("tos.tos6.title")}
|
||||||
|
</h2>
|
||||||
|
{t("tos.tos6.desc")
|
||||||
|
.split("\n\n")
|
||||||
|
.map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4 text-white/70 leading-relaxed">
|
||||||
|
{paragraph.split("\n").map((line, index) => (
|
||||||
|
<span key={index}>
|
||||||
|
{line}
|
||||||
|
<br />
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TOS;
|
||||||
+10
-2
@@ -12,13 +12,21 @@ export default {
|
|||||||
"text-gold": "#FACC97",
|
"text-gold": "#FACC97",
|
||||||
"gold-light": "#FACC97",
|
"gold-light": "#FACC97",
|
||||||
"gold-dark": "#54330F",
|
"gold-dark": "#54330F",
|
||||||
|
"gold-mid": "#C9924F",
|
||||||
"gray-bg": "#1C1C1C",
|
"gray-bg": "#1C1C1C",
|
||||||
|
surface: "rgba(255, 255, 255, 0.04)",
|
||||||
|
"surface-border": "rgba(250, 204, 151, 0.14)",
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
baijam: ['"Bai Jamjuree"', "sans-serif"],
|
baijam: ['"Bai Jamjuree"', "sans-serif"],
|
||||||
},
|
},
|
||||||
backgroundImage: {
|
boxShadow: {
|
||||||
"hero-pattern": "url('./src/assets/HeroImg.svg')",
|
glass: "0 1px 1px rgba(250, 204, 151, 0.08), 0 20px 60px -20px rgba(0, 0, 0, 0.6)",
|
||||||
|
"glass-lg": "0 1px 1px rgba(250, 204, 151, 0.1), 0 30px 90px -20px rgba(0, 0, 0, 0.7)",
|
||||||
|
"gold-glow": "0 8px 30px -8px rgba(250, 204, 151, 0.35)",
|
||||||
|
},
|
||||||
|
backdropBlur: {
|
||||||
|
xs: "2px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user