diff --git a/, b/, deleted file mode 100644 index e69de29..0000000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8f70456 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..f40cdcf --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,19 @@ +name: Deploy + +on: + workflow_run: + workflows: ["CI"] + types: + - completed + branches: [main, master] + +jobs: + deploy: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Deploying to Production + run: echo "Starting deployment to production environment..." + # Add actual deployment steps here (e.g., Vercel, Netlify, GH Pages) + - name: Completion + run: echo "Deployment workflow triggered successfully." diff --git a/index.html b/index.html index c7936ed..c44ae63 100644 --- a/index.html +++ b/index.html @@ -7,15 +7,8 @@ - - - - - - - - - + + diff --git a/package-lock.json b/package-lock.json index 2c229a9..6639ef5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", "react-hook-form": "^7.61.1", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", @@ -3013,6 +3014,7 @@ "integrity": "sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -3030,6 +3032,7 @@ "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -3041,6 +3044,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3091,6 +3095,7 @@ "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", @@ -3323,6 +3328,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3536,6 +3542,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -3868,6 +3875,7 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", "license": "MIT", + "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -3949,7 +3957,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -4050,6 +4059,7 @@ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4611,6 +4621,15 @@ "node": ">=12" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -5205,6 +5224,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -5391,6 +5411,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5417,6 +5438,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5425,11 +5447,32 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet-async": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-2.0.5.tgz", + "integrity": "sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==", + "license": "Apache-2.0", + "dependencies": { + "invariant": "^2.2.4", + "react-fast-compare": "^3.2.2", + "shallowequal": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.6.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-hook-form": { "version": "7.61.1", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.61.1.tgz", "integrity": "sha512-2vbXUFDYgqEgM2RcXcAT2PwDW/80QARi+PKmHy5q2KhuKvOlG8iIYgf7eIlIANR5trW9fJbP4r5aub3a4egsew==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5766,6 +5809,12 @@ "node": ">=10" } }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5989,6 +6038,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -6098,6 +6148,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6161,6 +6212,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6340,6 +6392,7 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6433,6 +6486,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 0378061..3b9bcc7 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "react": "^18.3.1", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-helmet-async": "^2.0.5", "react-hook-form": "^7.61.1", "react-resizable-panels": "^2.1.9", "react-router-dom": "^6.30.1", diff --git a/public/avatar.jpg b/public/avatar.jpg new file mode 100644 index 0000000..e1ea558 Binary files /dev/null and b/public/avatar.jpg differ diff --git a/public/boy.jpg b/public/boy.jpg new file mode 100644 index 0000000..5030590 Binary files /dev/null and b/public/boy.jpg differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..78a92b2 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,20 @@ +{ + "short_name": "NexaTech", + "name": "NexaTech Rwanda", + "icons": [ + { + "src": "/favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "/og-image.png", + "type": "image/png", + "sizes": "1200x630" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#0057B8", + "background_color": "#ffffff" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..84004df --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://nexatech.co.rw/sitemap.xml diff --git a/src/App.tsx b/src/App.tsx index 9d86c2f..7da7a01 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,38 +3,52 @@ import { Toaster as Sonner } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { HelmetProvider } from "react-helmet-async"; +import React, { Suspense, lazy } from "react"; -import Index from "./pages/Index"; -import About from "./pages/About"; -import Work from "./pages/Work"; -import ShoppaDetail from "./pages/ShoppaDetail"; -import Contact from "./pages/Contact"; -import NotFound from "./pages/NotFound"; +// Lazy load pages for better performance +const Index = lazy(() => import("./pages/Index")); +const About = lazy(() => import("./pages/About")); +const Work = lazy(() => import("./pages/Work")); +const ShoppaDetail = lazy(() => import("./pages/ShoppaDetail")); +const Contact = lazy(() => import("./pages/Contact")); +const NotFound = lazy(() => import("./pages/NotFound")); const queryClient = new QueryClient(); +// Loading component for Suspense +const PageLoader = () => ( +
+
+
+); + const App = () => ( - - - - - - - {/* Home */} - } /> + + + + + + + }> + + {/* Home */} + } /> - {/* Other pages */} - } /> - } /> - } /> - } /> + {/* Other pages */} + } /> + } /> + } /> + } /> - {/* 404 – MUST stay last */} - } /> - - - - + {/* 404 – MUST stay last */} + } /> + + + + + + ); export default App; diff --git a/src/components/AIChatModal.tsx b/src/components/AIChatModal.tsx new file mode 100644 index 0000000..52d4a80 --- /dev/null +++ b/src/components/AIChatModal.tsx @@ -0,0 +1,151 @@ +import { useState, useRef, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { MessageSquare, X, Send, Bot, User } from "lucide-react"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; + +const AIChatModal = () => { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([ + { + role: "bot", + content: "Hello! I'm NexaBot, your guide to Africa's digital future. How can I help you today?", + }, + ]); + const [inputValue, setInputValue] = useState(""); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + if (isOpen) { + scrollToBottom(); + } + }, [messages, isOpen]); + + const handleSend = () => { + if (!inputValue.trim()) return; + + const userMessage = { role: "user", content: inputValue }; + setMessages((prev) => [...prev, userMessage]); + setInputValue(""); + + // Simulated AI Response Logic + setTimeout(() => { + let botResponse = "That's a great question! At NexaTech Rwanda, we specialize in building scalable, ready-to-market solutions for the African continent. Would you like to know more about our Custom Software, Mobile Engineering, or AI services?"; + + const input = userMessage.content.toLowerCase(); + if (input.includes("who") || input.includes("nexatech")) { + botResponse = "NexaTech Rwanda is a product-focused technology firm based in Kigali. We architect comprehensive digital ecosystems to solve complex operational challenges and unlock new revenue streams."; + } else if (input.includes("service") || input.includes("what you do")) { + botResponse = "We provide End-to-End Digital Transformation, including Custom Enterprise Platforms, Mobile Product Engineering, and AI & Data Intelligence."; + } else if (input.includes("contact") || input.includes("hire") || input.includes("start")) { + botResponse = "You can reach us at info@nexatech.co.rw or tap the WhatsApp button on the bottom right to chat with our team directly!"; + } + + setMessages((prev) => [...prev, { role: "bot", content: botResponse }]); + }, 1000); + }; + + return ( + <> + {/* Floating Trigger Button (Bottom Left) */} + setIsOpen(true)} + className="fixed bottom-24 right-8 z-[9999] bg-[#0057B8] text-white p-4 rounded-full shadow-[0_10px_25px_rgba(0,0,0,0.2)] flex flex-row-reverse items-center justify-center group transition-all duration-300" + > + + + Ask NexaBot + + + + {/* Chat Modal */} + + {isOpen && ( + + {/* Header */} +
+
+
+ +
+
+

NexaBot

+

+ + Online | AI Assistant +

+
+
+ +
+ + {/* Messages Area */} +
+ {messages.map((msg, index) => ( + +
+
+ {msg.role === "bot" ? : } +
+
+ {msg.content} +
+
+
+ ))} +
+
+ + {/* Input Area */} +
+
+ setInputValue(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && handleSend()} + placeholder="Ask me something..." + className="rounded-full bg-gray-100 border-none px-6 pr-12 focus-visible:ring-1 focus-visible:ring-[#0057B8]" + /> + +
+
+ + )} + + + ); +}; + +export default AIChatModal; diff --git a/src/components/About.tsx b/src/components/About.tsx index e70da18..d96fc84 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -1,9 +1,41 @@ -import { motion, useInView } from "framer-motion"; -import { useRef } from "react"; +import { motion, useInView, useSpring, useTransform } from "framer-motion"; +import { useRef, useEffect, useState } from "react"; import { Button } from "./ui/button"; import { Hand } from "lucide-react"; import { Link } from "react-router-dom"; +const Counter = ({ value, isInView }: { value: string; isInView: boolean }) => { + const numericPart = parseInt(value) || 0; + const suffix = value.replace(/[0-9]/g, "").trim(); + const [display, setDisplay] = useState(0); + + const springValue = useSpring(0, { + bounce: 0, + duration: 2000, + }); + + const displayValue = useTransform(springValue, (latest) => Math.round(latest)); + + useEffect(() => { + return displayValue.on("change", (latest) => { + setDisplay(latest); + }); + }, [displayValue]); + + useEffect(() => { + if (isInView) { + springValue.set(numericPart); + } + }, [isInView, numericPart, springValue]); + + return ( + + {display} + {suffix && {suffix}} + + ); +}; + const About = () => { const ref = useRef(null); const isInView = useInView(ref, { once: true, margin: "-100px" }); @@ -32,10 +64,10 @@ const About = () => { who we are

- Who Are We? The Brain Of
Africa's Tech Ecosystem + Your Strategic Partner for
Africa's Digital Backbone

- At NexaTech Rwanda, we are more than just a tech company—we are the architects of Africa's digital future. + NexaTech Rwanda is a product-focused technology firm based in Kigali. We don’t just write code; we architect comprehensive digital ecosystems that are ready to scale and solve Africa's most complex operational challenges.

@@ -58,9 +90,9 @@ const About = () => {
-

Africa Tech Brain

+

Ready-to-Market Innovation

- Leveraging cutting-edge technology to enhance efficiency, precision, and sustainability in every project. + We focus on stability, scalability, and user experience, ensuring that the solutions we build today are ready to deliver value for years to come across the continent.

    @@ -105,7 +137,9 @@ const About = () => { transition={{ duration: 0.6, delay: 0.5 + index * 0.15 }} className="px-4" > -
    {stat.value}
    +
    + +
    {stat.label}

    {stat.desc}

    diff --git a/src/components/CTA.tsx b/src/components/CTA.tsx index b5dcea2..90cbb02 100644 --- a/src/components/CTA.tsx +++ b/src/components/CTA.tsx @@ -37,7 +37,7 @@ const CTA = () => { className="text-center" >

    - Join Africa's Tech Revolution — Start Today! + Join Africa's Tech Revolution Start Today!

    diff --git a/src/components/FAQ.tsx b/src/components/FAQ.tsx index 7af8856..c1b6b2d 100644 --- a/src/components/FAQ.tsx +++ b/src/components/FAQ.tsx @@ -116,7 +116,7 @@ const FAQ = () => { {/* Animated Logo Carousel */} -

    + {/*
    {
    ))} -
    +
*/} diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index db0087f..405a872 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,7 +1,7 @@ import { motion, useInView } from "framer-motion"; import { useRef } from "react"; import { Button } from "./ui/button"; -import { Hand, Twitter, Instagram, Linkedin } from "lucide-react"; +import { Hand, Twitter, Instagram, Linkedin, MessageSquare } from "lucide-react"; import { Link } from "react-router-dom"; const Footer = () => { @@ -21,7 +21,7 @@ const Footer = () => { >

NexaTech Rwanda

- Transforming Africa and Rwanda tech solutions + Engineering Scalable, Ready-to-Market Solutions for Africa's Future.

diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 57840a8..4e9ee38 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -1,6 +1,8 @@ import { ReactNode } from "react"; import Navbar from "./Navbar"; import Footer from "./Footer"; +import WhatsAppButton from "./WhatsAppButton"; +import AIChatModal from "./AIChatModal"; interface LayoutProps { children: ReactNode; @@ -12,6 +14,8 @@ const Layout = ({ children }: LayoutProps) => {
{children}