From 945efd8cd946ff9852c8ba5623f56c465a8decc3 Mon Sep 17 00:00:00 2001 From: Caesarr Date: Fri, 26 Jun 2026 00:08:11 +0100 Subject: [PATCH] SEO & Metadata Implementation for NexaFx Added SEO metadata, Open Graph tags, Twitter cards, robots.txt, sitemap, and OG image to all public pages. Added noindex headers to dashboard and admin pages so search engines don't index user data. Set page titles for auth pages like sign-in ("Sign In") and signup ("Create Account"). Split client components that couldn't export metadata into server wrapper + client pattern. Build passes with zero errors. Closes #243 --- app/(admin)/admin-layout-client.tsx | 76 ++++ app/(admin)/layout.tsx | 78 +--- .../forgot-password-client.tsx | 221 +++++++++ app/(auth)/forgot-password/page.tsx | 223 +-------- app/(auth)/reset-password/page.tsx | 424 +----------------- .../reset-password/reset-password-client.tsx | 422 +++++++++++++++++ app/(auth)/sign-in/page.tsx | 318 +------------ app/(auth)/sign-in/sign-in-client.tsx | 316 +++++++++++++ app/(auth)/verify-otp/page.tsx | 281 +----------- app/(auth)/verify-otp/verify-otp-client.tsx | 279 ++++++++++++ app/(dashboard)/dashboard-layout-client.tsx | 75 ++++ app/(dashboard)/layout.tsx | 73 +-- app/landing-page-client.tsx | 34 ++ app/layout.tsx | 42 +- app/page.tsx | 44 +- app/signup/page.tsx | 213 +-------- app/signup/signup-client.tsx | 209 +++++++++ app/signup/success/page.tsx | 64 +-- app/signup/success/success-client.tsx | 62 +++ app/signup/verify/page.tsx | 148 +----- app/signup/verify/verify-client.tsx | 144 ++++++ app/sitemap.ts | 32 ++ package-lock.json | 2 + package.json | 2 + public/og-image.png | Bin 0 -> 128565 bytes public/robots.txt | 6 + 26 files changed, 1990 insertions(+), 1798 deletions(-) create mode 100644 app/(admin)/admin-layout-client.tsx create mode 100644 app/(auth)/forgot-password/forgot-password-client.tsx create mode 100644 app/(auth)/reset-password/reset-password-client.tsx create mode 100644 app/(auth)/sign-in/sign-in-client.tsx create mode 100644 app/(auth)/verify-otp/verify-otp-client.tsx create mode 100644 app/(dashboard)/dashboard-layout-client.tsx create mode 100644 app/landing-page-client.tsx create mode 100644 app/signup/signup-client.tsx create mode 100644 app/signup/success/success-client.tsx create mode 100644 app/signup/verify/verify-client.tsx create mode 100644 app/sitemap.ts create mode 100644 public/og-image.png create mode 100644 public/robots.txt diff --git a/app/(admin)/admin-layout-client.tsx b/app/(admin)/admin-layout-client.tsx new file mode 100644 index 00000000..267da70c --- /dev/null +++ b/app/(admin)/admin-layout-client.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { useState } from "react"; +import { Bell, User, Menu } from "lucide-react"; +import { AdminSidebar } from "@/components/admin/AdminSidebar"; +import { AdminGuard } from "@/components/admin/AdminGuard"; +import { usePathname } from "next/navigation"; + +export default function AdminLayoutClient({ + children, +}: { + children: React.ReactNode; +}) { + const pathname = usePathname(); + const title = pathname.split("/").filter(Boolean).pop() || "Admin"; + const capitalisedTitle = + title.charAt(0).toUpperCase() + title.slice(1).replace(/-/g, " "); + + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + + return ( + +
+ {/* Sidebar */} + setIsSidebarOpen(false)} + /> + + {/* Main Content Area */} +
+ {/* Topbar */} +
+
+ +

+ {capitalisedTitle} +

+
+ +
+ + +
+
+

+ Admin User +

+

+ Super Admin +

+
+
+ +
+
+
+
+ + {/* Page Content */} +
+ {children} +
+
+
+
+ ); +} diff --git a/app/(admin)/layout.tsx b/app/(admin)/layout.tsx index 9b42c637..0e28eded 100644 --- a/app/(admin)/layout.tsx +++ b/app/(admin)/layout.tsx @@ -1,76 +1,14 @@ -"use client"; +import type { Metadata } from "next"; +import AdminLayoutClient from "./admin-layout-client"; -import { useState } from "react"; -import { Bell, User, Menu } from "lucide-react"; -import { AdminSidebar } from "@/components/admin/AdminSidebar"; -import { AdminGuard } from "@/components/admin/AdminGuard"; -import { usePathname } from "next/navigation"; +export const metadata: Metadata = { + robots: { index: false, follow: false }, +}; export default function AdminLayout({ - children, + children, }: { - children: React.ReactNode; + children: React.ReactNode; }) { - const pathname = usePathname(); - const title = pathname.split("/").filter(Boolean).pop() || "Admin"; - const capitalisedTitle = - title.charAt(0).toUpperCase() + title.slice(1).replace(/-/g, " "); - - const [isSidebarOpen, setIsSidebarOpen] = useState(false); - - return ( - -
- {/* Sidebar */} - setIsSidebarOpen(false)} - /> - - {/* Main Content Area */} -
- {/* Topbar */} -
-
- -

- {capitalisedTitle} -

-
- -
- - -
-
-

- Admin User -

-

- Super Admin -

-
-
- -
-
-
-
- - {/* Page Content */} -
- {children} -
-
-
-
- ); + return {children}; } diff --git a/app/(auth)/forgot-password/forgot-password-client.tsx b/app/(auth)/forgot-password/forgot-password-client.tsx new file mode 100644 index 00000000..0cd8ffba --- /dev/null +++ b/app/(auth)/forgot-password/forgot-password-client.tsx @@ -0,0 +1,221 @@ +"use client"; + +import { useState } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { CheckCircle2 } from "lucide-react"; +import { forgotPassword } from "@/lib/api/auth"; + +export default function ForgotPasswordClient() { + const router = useRouter(); + const [email, setEmail] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); + const [status, setStatus] = useState<"form" | "confirmation">("form"); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!emailRegex.test(email)) { + setError("Please enter a valid email address"); + return; + } + + setIsLoading(true); + + try { + await forgotPassword({ email }); + + setStatus("confirmation"); + + // Redirect after short delay + setTimeout(() => { + router.push( + `/reset-password?email=${encodeURIComponent(email)}`, + ); + }, 2000); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Something went wrong'); + } finally { + setIsLoading(false); + } + + { + /* + setTimeout(() => { + setIsLoading(false); + setStatus("confirmation"); + }, 1000); + + setTimeout(() => { + setStatus("form"); + router.push("/reset-password"); + }, 3000); + */ + } + }; + + const ConfirmationModal = () => ( +
+
+
+ +
+ +

+ Reset Code Sent +

+ +
+

+ Check your email for a reset code +

+
+
+ ); + + return ( +
+ {/* Desktop Header */} +
+
+ NexaFX +
+ + Sign in + +
+
+
+ + {/* Desktop View */} + +
+ {status === "form" ? ( +
+
+

+ Forgot password +

+

+ Please enter your email address and you we would + send you an OTP +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + placeholder="" + className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" + disabled={isLoading} + /> +
+ + +
+
+ ) : ( + + )} +
+ + {/* Mobile View */} +
+ {status === "form" ? ( +
+
+
+ NexaFX +
+ +

+ Forgot password +

+

+ Please enter your email address and you we would + send you an OTP +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + placeholder="" + className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" + disabled={isLoading} + /> +
+ + +
+
+ ) : ( + + )} +
+
+ ); +} diff --git a/app/(auth)/forgot-password/page.tsx b/app/(auth)/forgot-password/page.tsx index 28a0cc68..3e703e4b 100644 --- a/app/(auth)/forgot-password/page.tsx +++ b/app/(auth)/forgot-password/page.tsx @@ -1,221 +1,10 @@ -"use client"; +import type { Metadata } from "next"; +import ForgotPasswordClient from "./forgot-password-client"; -import { useState } from "react"; -import Image from "next/image"; -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { CheckCircle2 } from "lucide-react"; -import { forgotPassword } from "@/lib/api/auth"; +export const metadata: Metadata = { + title: "Reset Password", +}; export default function ForgotPasswordPage() { - const router = useRouter(); - const [email, setEmail] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(""); - const [status, setStatus] = useState<"form" | "confirmation">("form"); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(""); - - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - if (!emailRegex.test(email)) { - setError("Please enter a valid email address"); - return; - } - - setIsLoading(true); - - try { - await forgotPassword({ email }); - - setStatus("confirmation"); - - // Redirect after short delay - setTimeout(() => { - router.push( - `/reset-password?email=${encodeURIComponent(email)}`, - ); - }, 2000); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : 'Something went wrong'); - } finally { - setIsLoading(false); - } - - { - /* - setTimeout(() => { - setIsLoading(false); - setStatus("confirmation"); - }, 1000); - - setTimeout(() => { - setStatus("form"); - router.push("/reset-password"); - }, 3000); - */ - } - }; - - const ConfirmationModal = () => ( -
-
-
- -
- -

- Reset Code Sent -

- -
-

- Check your email for a reset code -

-
-
- ); - - return ( -
- {/* Desktop Header */} -
-
- NexaFX -
- - Sign in - -
-
-
- - {/* Desktop View */} - -
- {status === "form" ? ( -
-
-

- Forgot password -

-

- Please enter your email address and you we would - send you an OTP -

-
- - {error && ( -
- {error} -
- )} - -
-
- - setEmail(e.target.value)} - placeholder="" - className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" - disabled={isLoading} - /> -
- - -
-
- ) : ( - - )} -
- - {/* Mobile View */} -
- {status === "form" ? ( -
-
-
- NexaFX -
- -

- Forgot password -

-

- Please enter your email address and you we would - send you an OTP -

-
- - {error && ( -
- {error} -
- )} - -
-
- - setEmail(e.target.value)} - placeholder="" - className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" - disabled={isLoading} - /> -
- - -
-
- ) : ( - - )} -
-
- ); + return ; } diff --git a/app/(auth)/reset-password/page.tsx b/app/(auth)/reset-password/page.tsx index 62550e4c..2c8fd9ef 100644 --- a/app/(auth)/reset-password/page.tsx +++ b/app/(auth)/reset-password/page.tsx @@ -1,422 +1,10 @@ -"use client"; +import type { Metadata } from "next"; +import ResetPasswordClient from "./reset-password-client"; -import { Suspense, useEffect, useRef, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { resetPassword } from "@/lib/api/auth"; - -import Image from "next/image"; +export const metadata: Metadata = { + title: "Reset Password", +}; export default function ResetPasswordPage() { - return ( - - - - ); -} - -function ResetPasswordContent() { - const router = useRouter(); - const searchParams = useSearchParams(); - const email = searchParams.get("email") || ""; - - const [otp, setOtp] = useState(["", "", "", "", "", ""]); - const [newPassword, setNewPassword] = useState(""); - const [confirmPassword, setConfirmPassword] = useState(""); - - const [errors, setErrors] = useState<{ [key: string]: string }>({}); - const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(""); - const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - - useEffect(() => { - inputRefs.current[0]?.focus(); - }, []); - - const handleChange = (index: number, value: string) => { - if (!/^\d*$/.test(value)) return; - - const updatedOtp = [...otp]; - updatedOtp[index] = value.slice(-1); - setOtp(updatedOtp); - - if (value && index < 5) { - inputRefs.current[index + 1]?.focus(); - } - }; - - const handleKeyDown = ( - index: number, - e: React.KeyboardEvent, - ) => { - if (e.key === "Backspace" && !otp[index] && index > 0) { - inputRefs.current[index - 1]?.focus(); - } - }; - - const handlePaste = (e: React.ClipboardEvent) => { - e.preventDefault(); - const pastedData = e.clipboardData.getData("text").slice(0, 6); - if (!/^\d+$/.test(pastedData)) return; - - const updatedOtp = [...otp]; - pastedData.split("").forEach((char, index) => { - if (index < 6) updatedOtp[index] = char; - }); - setOtp(updatedOtp); - const lastFilledIndex = Math.min(pastedData.length, 5); - inputRefs.current[lastFilledIndex]?.focus(); - }; - - const validate = () => { - const newErrors: { [key: string]: string } = {}; - const otpCode = otp.join(""); - - if (otpCode.length !== 6) { - newErrors.otp = "Please enter all 6 digits"; - } - - if (!newPassword) newErrors.newPassword = "Password is required"; - else if (newPassword.length < 8) - newErrors.newPassword = "Password must be at least 8 characters"; - - if (newPassword !== confirmPassword) { - newErrors.confirmPassword = "Passwords do not match"; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!validate()) return; - - if (!email) { - setApiError("Session expired. Please start again."); - return; - } - - const otpCode = otp.join(""); - - setIsLoading(true); - setApiError(""); - - try { - await resetPassword({ - email, - otp: otpCode, - password: newPassword, - }); - - router.push("/sign-in?reset=success"); - } catch (err) { - setApiError( - err instanceof Error ? err.message : "Invalid or expired OTP", - ); - } finally { - setIsLoading(false); - } - }; - - return ( -
- {/* Desktop Header */} -
-
- NexaFX -
-
- - {/* Desktop View */} -
-
-
-

- Reset Password -

-
- - {apiError && ( -
- {apiError} -
- )} - -
-
-
- {otp.map((digit, index) => ( - { - inputRefs.current[index] = el; - }} - type="text" - inputMode="numeric" - maxLength={1} - value={digit} - onChange={(e) => handleChange(index, e.target.value)} - onKeyDown={(e) => handleKeyDown(index, e)} - onPaste={handlePaste} - className="w-12 h-12 text-center text-xl font-semibold bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all" - disabled={isLoading} - /> - ))} -
- {errors.otp && ( -

{errors.otp}

- )} -
-
- - -
- setNewPassword(e.target.value)} - placeholder="" - className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" - disabled={isLoading} - /> - {errors.newPassword && ( -

- {errors.newPassword} -

- )} -
-
- -
- -
- setConfirmPassword(e.target.value)} - placeholder="" - className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" - disabled={isLoading} - /> - {errors.confirmPassword && ( -

- {errors.confirmPassword} -

- )} - -
-
- - -
-
-
- - {/* Mobile View */} -
-
-
-
- NexaFX -
- -

- Reset Password -

-
- - {apiError && ( -
- {apiError} -
- )} - -
-
-
- {otp.map((digit, index) => ( - { - inputRefs.current[index] = el; - }} - type="text" - inputMode="numeric" - maxLength={1} - value={digit} - onChange={(e) => handleChange(index, e.target.value)} - onKeyDown={(e) => handleKeyDown(index, e)} - onPaste={handlePaste} - className="w-10 h-11 text-center text-lg font-semibold bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all" - disabled={isLoading} - /> - ))} -
- {errors.otp && ( -

{errors.otp}

- )} -
-
- - setNewPassword(e.target.value)} - placeholder="" - className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" - disabled={isLoading} - /> - {errors.newPassword && ( -

- {errors.newPassword} -

- )} -
- -
- -
- setConfirmPassword(e.target.value)} - placeholder="" - className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" - disabled={isLoading} - /> - {errors.confirmPassword && ( -

- {errors.confirmPassword} -

- )} - -
-
- - -
-
-
-
- ); + return ; } diff --git a/app/(auth)/reset-password/reset-password-client.tsx b/app/(auth)/reset-password/reset-password-client.tsx new file mode 100644 index 00000000..de38c3c6 --- /dev/null +++ b/app/(auth)/reset-password/reset-password-client.tsx @@ -0,0 +1,422 @@ +"use client"; + +import { Suspense, useEffect, useRef, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { resetPassword } from "@/lib/api/auth"; + +import Image from "next/image"; + +export default function ResetPasswordClient() { + return ( + + + + ); +} + +function ResetPasswordContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const email = searchParams.get("email") || ""; + + const [otp, setOtp] = useState(["", "", "", "", "", ""]); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(""); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + useEffect(() => { + inputRefs.current[0]?.focus(); + }, []); + + const handleChange = (index: number, value: string) => { + if (!/^\d*$/.test(value)) return; + + const updatedOtp = [...otp]; + updatedOtp[index] = value.slice(-1); + setOtp(updatedOtp); + + if (value && index < 5) { + inputRefs.current[index + 1]?.focus(); + } + }; + + const handleKeyDown = ( + index: number, + e: React.KeyboardEvent, + ) => { + if (e.key === "Backspace" && !otp[index] && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + }; + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + const pastedData = e.clipboardData.getData("text").slice(0, 6); + if (!/^\d+$/.test(pastedData)) return; + + const updatedOtp = [...otp]; + pastedData.split("").forEach((char, index) => { + if (index < 6) updatedOtp[index] = char; + }); + setOtp(updatedOtp); + const lastFilledIndex = Math.min(pastedData.length, 5); + inputRefs.current[lastFilledIndex]?.focus(); + }; + + const validate = () => { + const newErrors: { [key: string]: string } = {}; + const otpCode = otp.join(""); + + if (otpCode.length !== 6) { + newErrors.otp = "Please enter all 6 digits"; + } + + if (!newPassword) newErrors.newPassword = "Password is required"; + else if (newPassword.length < 8) + newErrors.newPassword = "Password must be at least 8 characters"; + + if (newPassword !== confirmPassword) { + newErrors.confirmPassword = "Passwords do not match"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validate()) return; + + if (!email) { + setApiError("Session expired. Please start again."); + return; + } + + const otpCode = otp.join(""); + + setIsLoading(true); + setApiError(""); + + try { + await resetPassword({ + email, + otp: otpCode, + password: newPassword, + }); + + router.push("/sign-in?reset=success"); + } catch (err) { + setApiError( + err instanceof Error ? err.message : "Invalid or expired OTP", + ); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {/* Desktop Header */} +
+
+ NexaFX +
+
+ + {/* Desktop View */} +
+
+
+

+ Reset Password +

+
+ + {apiError && ( +
+ {apiError} +
+ )} + +
+
+
+ {otp.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={handlePaste} + className="w-12 h-12 text-center text-xl font-semibold bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all" + disabled={isLoading} + /> + ))} +
+ {errors.otp && ( +

{errors.otp}

+ )} +
+
+ + +
+ setNewPassword(e.target.value)} + placeholder="" + className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" + disabled={isLoading} + /> + {errors.newPassword && ( +

+ {errors.newPassword} +

+ )} +
+
+ +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="" + className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" + disabled={isLoading} + /> + {errors.confirmPassword && ( +

+ {errors.confirmPassword} +

+ )} + +
+
+ + +
+
+
+ + {/* Mobile View */} +
+
+
+
+ NexaFX +
+ +

+ Reset Password +

+
+ + {apiError && ( +
+ {apiError} +
+ )} + +
+
+
+ {otp.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={handlePaste} + className="w-10 h-11 text-center text-lg font-semibold bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all" + disabled={isLoading} + /> + ))} +
+ {errors.otp && ( +

{errors.otp}

+ )} +
+
+ + setNewPassword(e.target.value)} + placeholder="" + className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" + disabled={isLoading} + /> + {errors.newPassword && ( +

+ {errors.newPassword} +

+ )} +
+ +
+ +
+ setConfirmPassword(e.target.value)} + placeholder="" + className="w-full px-4 py-2.5 bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm" + disabled={isLoading} + /> + {errors.confirmPassword && ( +

+ {errors.confirmPassword} +

+ )} + +
+
+ + +
+
+
+
+ ); +} diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx index 891bc26e..d08a5e9e 100644 --- a/app/(auth)/sign-in/page.tsx +++ b/app/(auth)/sign-in/page.tsx @@ -1,316 +1,10 @@ -'use client'; +import type { Metadata } from "next"; +import SignInClient from "./sign-in-client"; -import Image from 'next/image'; -import Link from 'next/link'; -import { login } from '@/lib/api/auth'; -import { useRouter } from 'next/navigation'; -import { useState } from 'react'; +export const metadata: Metadata = { + title: "Sign In", +}; export default function SignInPage() { - const router = useRouter(); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(''); - - if (!email || !password) { - setError('Please fill in all fields'); - return; - } - - setIsLoading(true); - - try { - await login({ email, password }); - setIsLoading(false); - sessionStorage.setItem('login-email', email); - router.push('/verify-otp'); - } catch (err: unknown) { - setIsLoading(false); - setError(err instanceof Error ? err.message : 'Login failed'); - } - }; - - return ( -
- {/* Desktop Header */} -
-
- NexaFX -
- Don't have an account?{' '} - - Sign up - -
-
-
- - {/* Desktop View */} -
-
-
-

Sign in

-

- Hey, Welcome back -

-
- - {error && ( -
- {error} -
- )} - -
-
- - setEmail(e.target.value)} - placeholder="Enter email address or phone" - title="Email address or mobile number" - className="w-full px-4 py-2.5 bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm text-foreground" - disabled={isLoading} - /> -
- -
- -
- setPassword(e.target.value)} - placeholder="Enter password" - title="Password" - className="w-full px-4 py-2.5 bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm text-foreground" - disabled={isLoading} - /> - -
-
- - Forgotten password? - -
-
- - -
- -
- Don't have an account?{' '} - - Sign up - -
-
-
- - {/* Mobile View */} -
-
-
-
- NexaFX -
- -

Sign in

-

- Hey, welcome back -

-
- - {error && ( -
- {error} -
- )} - -
-
- - setEmail(e.target.value)} - placeholder="Enter email address" - title="Email address" - className="w-full px-4 py-2.5 bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm text-foreground" - disabled={isLoading} - /> -
- -
- -
- setPassword(e.target.value)} - placeholder="Enter password" - title="Password" - className="w-full px-4 py-2.5 bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm text-foreground" - disabled={isLoading} - /> - -
-
- - Forgotten password? - -
-
- - -
- -
- Don't have an account?{' '} - - Sign up - -
-
-
-
- ); + return ; } diff --git a/app/(auth)/sign-in/sign-in-client.tsx b/app/(auth)/sign-in/sign-in-client.tsx new file mode 100644 index 00000000..b708bd31 --- /dev/null +++ b/app/(auth)/sign-in/sign-in-client.tsx @@ -0,0 +1,316 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { login } from '@/lib/api/auth'; +import { useRouter } from 'next/navigation'; +import { useState } from 'react'; + +export default function SignInClient() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + + if (!email || !password) { + setError('Please fill in all fields'); + return; + } + + setIsLoading(true); + + try { + await login({ email, password }); + setIsLoading(false); + sessionStorage.setItem('login-email', email); + router.push('/verify-otp'); + } catch (err: unknown) { + setIsLoading(false); + setError(err instanceof Error ? err.message : 'Login failed'); + } + }; + + return ( +
+ {/* Desktop Header */} +
+
+ NexaFX +
+ Don't have an account?{' '} + + Sign up + +
+
+
+ + {/* Desktop View */} +
+
+
+

Sign in

+

+ Hey, Welcome back +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + placeholder="Enter email address or phone" + title="Email address or mobile number" + className="w-full px-4 py-2.5 bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm text-foreground" + disabled={isLoading} + /> +
+ +
+ +
+ setPassword(e.target.value)} + placeholder="Enter password" + title="Password" + className="w-full px-4 py-2.5 bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm text-foreground" + disabled={isLoading} + /> + +
+
+ + Forgotten password? + +
+
+ + +
+ +
+ Don't have an account?{' '} + + Sign up + +
+
+
+ + {/* Mobile View */} +
+
+
+
+ NexaFX +
+ +

Sign in

+

+ Hey, welcome back +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ + setEmail(e.target.value)} + placeholder="Enter email address" + title="Email address" + className="w-full px-4 py-2.5 bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm text-foreground" + disabled={isLoading} + /> +
+ +
+ +
+ setPassword(e.target.value)} + placeholder="Enter password" + title="Password" + className="w-full px-4 py-2.5 bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all text-sm text-foreground" + disabled={isLoading} + /> + +
+
+ + Forgotten password? + +
+
+ + +
+ +
+ Don't have an account?{' '} + + Sign up + +
+
+
+
+ ); +} diff --git a/app/(auth)/verify-otp/page.tsx b/app/(auth)/verify-otp/page.tsx index d9c8e786..1b45d401 100644 --- a/app/(auth)/verify-otp/page.tsx +++ b/app/(auth)/verify-otp/page.tsx @@ -1,279 +1,10 @@ -'use client'; +import type { Metadata } from "next"; +import VerifyOtpClient from "./verify-otp-client"; -import { verifyLoginOtp, resendLoginOtp } from '@/lib/api/auth'; -import { useEffect, useRef, useState } from 'react'; - -import Image from 'next/image'; -import Link from 'next/link'; -import { useAuthStore } from '@/hooks/use-auth-store'; -import { useRouter } from 'next/navigation'; +export const metadata: Metadata = { + title: "Verify OTP", +}; export default function VerifyOtpPage() { - const router = useRouter(); - const setAuth = useAuthStore((s) => s.setAuth); - const [otp, setOtp] = useState(['', '', '', '', '', '']); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); - const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - - useEffect(() => { - inputRefs.current[0]?.focus(); - }, []); - - const handleChange = (index: number, value: string) => { - if (!/^\d*$/.test(value)) return; - - const newOtp = [...otp]; - newOtp[index] = value.slice(-1); - setOtp(newOtp); - setError(''); - - if (value && index < 5) { - inputRefs.current[index + 1]?.focus(); - } - }; - - const handleKeyDown = ( - index: number, - e: React.KeyboardEvent, - ) => { - if (e.key === 'Backspace' && !otp[index] && index > 0) { - inputRefs.current[index - 1]?.focus(); - } - }; - - const handlePaste = (e: React.ClipboardEvent) => { - e.preventDefault(); - const pastedData = e.clipboardData.getData('text').slice(0, 6); - if (!/^\d+$/.test(pastedData)) return; - - const newOtp = [...otp]; - pastedData.split('').forEach((char, index) => { - if (index < 6) newOtp[index] = char; - }); - setOtp(newOtp); - - const lastFilledIndex = Math.min(pastedData.length, 5); - inputRefs.current[lastFilledIndex]?.focus(); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const otpCode = otp.join(''); - if (otpCode.length !== 6) { - setError('Please enter all 6 digits'); - return; - } - const storedEmail = sessionStorage.getItem('login-email'); - if (!storedEmail) { - setError('No email found. Please sign in again.'); - return; - } - setIsLoading(true); - setError(''); - try { - const res = await verifyLoginOtp({ email: storedEmail, otp: otpCode }) as { user: { id: string; firstName: string; lastName: string; email: string; role: 'USER' | 'ADMIN' }; accessToken: string; refreshToken: string }; - const fullName = [res.user.firstName, res.user.lastName].filter(Boolean).join(' '); - setAuth({ ...res.user, name: fullName }, res.accessToken, res.refreshToken); - setIsLoading(false); - router.push('/dashboard'); - } catch (err: unknown) { - setIsLoading(false); - setError(err instanceof Error ? err.message : 'Invalid or expired OTP'); - } - }; - - const handleResend = async () => { - setOtp(['', '', '', '', '', '']); - setError(''); - inputRefs.current[0]?.focus(); - const storedEmail = sessionStorage.getItem('login-email'); - if (!storedEmail) { - setError('Missing email. Please sign in again.'); - return; - } - try { - await resendLoginOtp({ email: storedEmail }); - } catch { - setError('Failed to resend code'); - } - }; - - const isComplete = otp.every((digit) => digit !== ''); - - return ( -
- {/* Desktop Header */} -
-
- NexaFX -
-
- - {/* Desktop View */} -
-
-
-

- VERIFY CODE -

-

- Confirmation code sent. Check inbox or spam folder for the code -

-
- - {error && ( -
- {error} -
- )} - -
-
- {otp.map((digit, index) => ( - { - inputRefs.current[index] = el; - }} - type="text" - inputMode="numeric" - maxLength={1} - value={digit} - onChange={(e) => handleChange(index, e.target.value)} - onKeyDown={(e) => handleKeyDown(index, e)} - onPaste={handlePaste} - className="w-12 h-12 text-center text-xl font-semibold bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all" - disabled={isLoading} - /> - ))} -
- -
- -
- - -
-
-
- - {/* Mobile View */} -
- {/* Mobile Header */} -
- - - Request access - -
- -
-
-
-

- VERIFY CODE -

-

- Confirmation code sent. Check inbox or spam folder for the code -

-
- - {error && ( -
- {error} -
- )} - -
-
- {otp.map((digit, index) => ( - { - inputRefs.current[index] = el; - }} - type="text" - inputMode="numeric" - maxLength={1} - value={digit} - onChange={(e) => handleChange(index, e.target.value)} - onKeyDown={(e) => handleKeyDown(index, e)} - onPaste={handlePaste} - className="w-11 h-11 text-center text-lg font-semibold bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all" - disabled={isLoading} - /> - ))} -
- -
- -
- - -
-
-
-
-
- ); + return ; } diff --git a/app/(auth)/verify-otp/verify-otp-client.tsx b/app/(auth)/verify-otp/verify-otp-client.tsx new file mode 100644 index 00000000..fa007563 --- /dev/null +++ b/app/(auth)/verify-otp/verify-otp-client.tsx @@ -0,0 +1,279 @@ +'use client'; + +import { verifyLoginOtp, resendLoginOtp } from '@/lib/api/auth'; +import { useEffect, useRef, useState } from 'react'; + +import Image from 'next/image'; +import Link from 'next/link'; +import { useAuthStore } from '@/hooks/use-auth-store'; +import { useRouter } from 'next/navigation'; + +export default function VerifyOtpClient() { + const router = useRouter(); + const setAuth = useAuthStore((s) => s.setAuth); + const [otp, setOtp] = useState(['', '', '', '', '', '']); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + useEffect(() => { + inputRefs.current[0]?.focus(); + }, []); + + const handleChange = (index: number, value: string) => { + if (!/^\d*$/.test(value)) return; + + const newOtp = [...otp]; + newOtp[index] = value.slice(-1); + setOtp(newOtp); + setError(''); + + if (value && index < 5) { + inputRefs.current[index + 1]?.focus(); + } + }; + + const handleKeyDown = ( + index: number, + e: React.KeyboardEvent, + ) => { + if (e.key === 'Backspace' && !otp[index] && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + }; + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + const pastedData = e.clipboardData.getData('text').slice(0, 6); + if (!/^\d+$/.test(pastedData)) return; + + const newOtp = [...otp]; + pastedData.split('').forEach((char, index) => { + if (index < 6) newOtp[index] = char; + }); + setOtp(newOtp); + + const lastFilledIndex = Math.min(pastedData.length, 5); + inputRefs.current[lastFilledIndex]?.focus(); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const otpCode = otp.join(''); + if (otpCode.length !== 6) { + setError('Please enter all 6 digits'); + return; + } + const storedEmail = sessionStorage.getItem('login-email'); + if (!storedEmail) { + setError('No email found. Please sign in again.'); + return; + } + setIsLoading(true); + setError(''); + try { + const res = await verifyLoginOtp({ email: storedEmail, otp: otpCode }) as { user: { id: string; firstName: string; lastName: string; email: string; role: 'USER' | 'ADMIN' }; accessToken: string; refreshToken: string }; + const fullName = [res.user.firstName, res.user.lastName].filter(Boolean).join(' '); + setAuth({ ...res.user, name: fullName }, res.accessToken, res.refreshToken); + setIsLoading(false); + router.push('/dashboard'); + } catch (err: unknown) { + setIsLoading(false); + setError(err instanceof Error ? err.message : 'Invalid or expired OTP'); + } + }; + + const handleResend = async () => { + setOtp(['', '', '', '', '', '']); + setError(''); + inputRefs.current[0]?.focus(); + const storedEmail = sessionStorage.getItem('login-email'); + if (!storedEmail) { + setError('Missing email. Please sign in again.'); + return; + } + try { + await resendLoginOtp({ email: storedEmail }); + } catch { + setError('Failed to resend code'); + } + }; + + const isComplete = otp.every((digit) => digit !== ''); + + return ( +
+ {/* Desktop Header */} +
+
+ NexaFX +
+
+ + {/* Desktop View */} +
+
+
+

+ VERIFY CODE +

+

+ Confirmation code sent. Check inbox or spam folder for the code +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ {otp.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={handlePaste} + className="w-12 h-12 text-center text-xl font-semibold bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all" + disabled={isLoading} + /> + ))} +
+ +
+ +
+ + +
+
+
+ + {/* Mobile View */} +
+ {/* Mobile Header */} +
+ + + Request access + +
+ +
+
+
+

+ VERIFY CODE +

+

+ Confirmation code sent. Check inbox or spam folder for the code +

+
+ + {error && ( +
+ {error} +
+ )} + +
+
+ {otp.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleChange(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={handlePaste} + className="w-11 h-11 text-center text-lg font-semibold bg-[#F5F5F5] border-0 rounded-md focus:outline-none focus:ring-2 focus:ring-[#F39A00] transition-all" + disabled={isLoading} + /> + ))} +
+ +
+ +
+ + +
+
+
+
+
+ ); +} diff --git a/app/(dashboard)/dashboard-layout-client.tsx b/app/(dashboard)/dashboard-layout-client.tsx new file mode 100644 index 00000000..d199c503 --- /dev/null +++ b/app/(dashboard)/dashboard-layout-client.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import { Sidebar } from '../../components/dashboard/sidebar'; +import { Topbar } from '../../components/dashboard/topbar'; +import { cn } from '../../lib/utils'; +import { useAuthStore } from '../../hooks/use-auth-store'; +import { useRouter } from 'next/navigation'; +import { useSidebarStore } from '../../hooks/use-sidebar-store'; + +export default function DashboardLayoutClient({ + children, +}: { + children: React.ReactNode; +}) { + const { isOpen, close } = useSidebarStore(); + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); + const { isAuthenticated, accessToken } = useAuthStore(); + const router = useRouter(); + + useEffect(() => { + if (!isAuthenticated || !accessToken) { + router.replace('/sign-in'); + } + }, [isAuthenticated, accessToken, router]); + + if (!isAuthenticated || !accessToken) { + return null; + } + + return ( +
+ {/* Sidebar - Desktop */} + + + {/* Sidebar - Mobile Drawer Overlay */} + {isOpen && ( +
+ )} + + {/* Sidebar - Mobile Drawer */} + + +
+
+ +
+
+ {children} +
+
+
+ ); +} diff --git a/app/(dashboard)/layout.tsx b/app/(dashboard)/layout.tsx index eae4b06d..2c8b85d4 100644 --- a/app/(dashboard)/layout.tsx +++ b/app/(dashboard)/layout.tsx @@ -1,75 +1,14 @@ -'use client'; +import type { Metadata } from "next"; +import DashboardLayoutClient from "./dashboard-layout-client"; -import { useEffect, useState } from 'react'; - -import { Sidebar } from '../../components/dashboard/sidebar'; -import { Topbar } from '../../components/dashboard/topbar'; -import { cn } from '../../lib/utils'; -import { useAuthStore } from '../../hooks/use-auth-store'; -import { useRouter } from 'next/navigation'; -import { useSidebarStore } from '../../hooks/use-sidebar-store'; +export const metadata: Metadata = { + robots: { index: false, follow: false }, +}; export default function DashboardLayout({ children, }: { children: React.ReactNode; }) { - const { isOpen, close } = useSidebarStore(); - const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); - const { isAuthenticated, accessToken } = useAuthStore(); - const router = useRouter(); - - useEffect(() => { - if (!isAuthenticated || !accessToken) { - router.replace('/sign-in'); - } - }, [isAuthenticated, accessToken, router]); - - if (!isAuthenticated || !accessToken) { - return null; - } - - return ( -
- {/* Sidebar - Desktop */} - - - {/* Sidebar - Mobile Drawer Overlay */} - {isOpen && ( -
- )} - - {/* Sidebar - Mobile Drawer */} - - -
-
- -
-
- {children} -
-
-
- ); + return {children}; } diff --git a/app/landing-page-client.tsx b/app/landing-page-client.tsx new file mode 100644 index 00000000..98335a23 --- /dev/null +++ b/app/landing-page-client.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAuthStore } from "@/hooks/use-auth-store"; + +import Navbar from "@/components/landing/Navbar"; +import Hero from "@/components/landing/Hero"; +import Features from "@/components/landing/Features"; +import CTA from "@/components/landing/CTA"; +import Footer from "@/components/landing/Footer"; + +export default function LandingPageClient() { + const { user } = useAuthStore(); + const router = useRouter(); + + useEffect(() => { + if (user) router.replace("/dashboard"); + }, [user, router]); + + if (user) return null; + + return ( +
+ +
+ + + +
+
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 9190fbd2..7d991c22 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,15 +12,49 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); -export const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); -export const manrope = Manrope({ +const inter = Inter({ subsets: ["latin"], variable: "--font-inter" }); +const manrope = Manrope({ subsets: ["latin"], variable: "--font-manrope", }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + metadataBase: new URL("https://nexafx.io"), + title: { + default: "NexaFx \u2014 Multi-Currency Finance on Stellar", + template: "%s | NexaFx", + }, + description: + "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.", + keywords: [ + "currency exchange", + "Stellar blockchain", + "cross-border payments", + "NGN to USD", + "crypto finance", + ], + authors: [{ name: "Nexacore" }], + creator: "Nexacore", + openGraph: { + type: "website", + locale: "en_US", + url: "https://nexafx.io", + siteName: "NexaFx", + title: "NexaFx \u2014 Multi-Currency Finance on Stellar", + description: + "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.", + images: [ + { url: "/og-image.png", width: 1200, height: 630, alt: "NexaFx" }, + ], + }, + twitter: { + card: "summary_large_image", + title: "NexaFx \u2014 Multi-Currency Finance on Stellar", + description: + "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.", + images: ["/og-image.png"], + }, + robots: { index: true, follow: true }, }; export default function RootLayout({ diff --git a/app/page.tsx b/app/page.tsx index 3120bc62..0bfb4f8d 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,34 +1,18 @@ -"use client"; +import type { Metadata } from "next"; +import LandingPageClient from "./landing-page-client"; -import { useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { useAuthStore } from "@/hooks/use-auth-store"; - -import Navbar from "@/components/landing/Navbar"; -import Hero from "@/components/landing/Hero"; -import Features from "@/components/landing/Features"; -import CTA from "@/components/landing/CTA"; -import Footer from "@/components/landing/Footer"; +export const metadata: Metadata = { + title: "NexaFx \u2014 Multi-Currency Finance on Stellar", + description: + "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.", + openGraph: { + title: "NexaFx \u2014 Multi-Currency Finance on Stellar", + description: + "Convert, deposit, and transfer currencies instantly on the Stellar blockchain.", + url: "https://nexafx.io", + }, +}; export default function HomePage() { - const { user } = useAuthStore(); - const router = useRouter(); - - useEffect(() => { - if (user) router.replace("/dashboard"); - }, [user, router]); - - if (user) return null; - - return ( -
- -
- - - -
-
-
- ); + return ; } diff --git a/app/signup/page.tsx b/app/signup/page.tsx index 7cc43a66..31588d59 100644 --- a/app/signup/page.tsx +++ b/app/signup/page.tsx @@ -1,209 +1,10 @@ -"use client"; +import type { Metadata } from "next"; +import SignupClient from "./signup-client"; -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import { Eye, EyeOff } from "lucide-react"; -import { signUp } from "@/lib/api/auth"; +export const metadata: Metadata = { + title: "Create Account", +}; -export default function CreateAccountPage() { - const router = useRouter(); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(""); - - const [formData, setFormData] = useState({ - email: "", - phone: "", - password: "", - confirmPassword: "", - acceptTerms: true, - }); - - const [errors, setErrors] = useState<{ [key: string]: string }>({}); - - const validate = () => { - const newErrors: { [key: string]: string } = {}; - if (!formData.email) newErrors.email = "Email is required"; - else if (!/\S+@\S+\.\S+/.test(formData.email)) - newErrors.email = "Invalid email address"; - - if (!formData.phone) newErrors.phone = "Phone number is required"; - - if (!formData.password) newErrors.password = "Password is required"; - else if (formData.password.length < 8) - newErrors.password = "Password must be at least 8 characters"; - - if (formData.password !== formData.confirmPassword) { - newErrors.confirmPassword = "Passwords do not match"; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!validate()) return; - - setIsLoading(true); - setApiError(""); - try { - await signUp({ - email: formData.email, - phone: formData.phone, - password: formData.password, - }); - sessionStorage.setItem("signup_email", formData.email); - router.push("/signup/verify"); - } catch (err) { - setApiError(err instanceof Error ? err.message : "Something went wrong"); - } finally { - setIsLoading(false); - } - }; - - return ( -
-
-

- Create an account -

-

Let's get started...

-
- -
-
- - setFormData({ ...formData, email: e.target.value }) - } - /> - {errors.email && ( -

{errors.email}

- )} -
- -
- - setFormData({ ...formData, phone: e.target.value }) - } - /> - {errors.phone && ( -

{errors.phone}

- )} -
- -
- - setFormData({ ...formData, password: e.target.value }) - } - /> - - {errors.password && ( -

- {errors.password} -

- )} -
- -
- - setFormData({ ...formData, confirmPassword: e.target.value }) - } - /> - - {errors.confirmPassword && ( -

- {errors.confirmPassword} -

- )} -
- -
-
- - setFormData({ ...formData, acceptTerms: e.target.checked }) - } - className="peer h-5 w-5 cursor-pointer appearance-none rounded-md border border-zinc-300 dark:border-border transition-all checked:bg-orange-500 checked:border-orange-500" - /> - - - -
- -
- - {apiError && ( -

{apiError}

- )} - -
-
- ); +export default function SignupPage() { + return ; } diff --git a/app/signup/signup-client.tsx b/app/signup/signup-client.tsx new file mode 100644 index 00000000..279d900b --- /dev/null +++ b/app/signup/signup-client.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Eye, EyeOff } from "lucide-react"; +import { signUp } from "@/lib/api/auth"; + +export default function SignupClient() { + const router = useRouter(); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(""); + + const [formData, setFormData] = useState({ + email: "", + phone: "", + password: "", + confirmPassword: "", + acceptTerms: true, + }); + + const [errors, setErrors] = useState<{ [key: string]: string }>({}); + + const validate = () => { + const newErrors: { [key: string]: string } = {}; + if (!formData.email) newErrors.email = "Email is required"; + else if (!/\S+@\S+\.\S+/.test(formData.email)) + newErrors.email = "Invalid email address"; + + if (!formData.phone) newErrors.phone = "Phone number is required"; + + if (!formData.password) newErrors.password = "Password is required"; + else if (formData.password.length < 8) + newErrors.password = "Password must be at least 8 characters"; + + if (formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = "Passwords do not match"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validate()) return; + + setIsLoading(true); + setApiError(""); + try { + await signUp({ + email: formData.email, + phone: formData.phone, + password: formData.password, + }); + sessionStorage.setItem("signup_email", formData.email); + router.push("/signup/verify"); + } catch (err) { + setApiError(err instanceof Error ? err.message : "Something went wrong"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

+ Create an account +

+

Let's get started...

+
+ +
+
+ + setFormData({ ...formData, email: e.target.value }) + } + /> + {errors.email && ( +

{errors.email}

+ )} +
+ +
+ + setFormData({ ...formData, phone: e.target.value }) + } + /> + {errors.phone && ( +

{errors.phone}

+ )} +
+ +
+ + setFormData({ ...formData, password: e.target.value }) + } + /> + + {errors.password && ( +

+ {errors.password} +

+ )} +
+ +
+ + setFormData({ ...formData, confirmPassword: e.target.value }) + } + /> + + {errors.confirmPassword && ( +

+ {errors.confirmPassword} +

+ )} +
+ +
+
+ + setFormData({ ...formData, acceptTerms: e.target.checked }) + } + className="peer h-5 w-5 cursor-pointer appearance-none rounded-md border border-zinc-300 dark:border-border transition-all checked:bg-orange-500 checked:border-orange-500" + /> + + + +
+ +
+ + {apiError && ( +

{apiError}

+ )} + +
+
+ ); +} diff --git a/app/signup/success/page.tsx b/app/signup/success/page.tsx index 4f973e21..07656dab 100644 --- a/app/signup/success/page.tsx +++ b/app/signup/success/page.tsx @@ -1,62 +1,10 @@ -"use client"; +import type { Metadata } from "next"; +import SuccessClient from "./success-client"; -import { useEffect, useState } from "react"; -import { CheckCircle2 } from "lucide-react"; -import { useRouter } from "next/navigation"; +export const metadata: Metadata = { + title: "Email Confirmed", +}; export default function SuccessPage() { - const router = useRouter(); - const [dots, setDots] = useState(""); - - useEffect(() => { - const interval = setInterval(() => { - setDots((prev) => (prev.length >= 3 ? "" : prev + ".")); - }, 500); - - // Redirect after 5 seconds - const timeout = setTimeout(() => { - router.push("/"); - }, 5000); - - return () => { - clearInterval(interval); - clearTimeout(timeout); - }; - }, [router]); - - return ( -
-
-
- -
- -

- EMAIL CONFIRMED -

- -
-

- Your account has been successfully verified. You now have full access - to NexaFX. -

- -
-

- Redirecting you to dashboard{dots} -

- -
-
-
- ); + return ; } diff --git a/app/signup/success/success-client.tsx b/app/signup/success/success-client.tsx new file mode 100644 index 00000000..7c69a180 --- /dev/null +++ b/app/signup/success/success-client.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { CheckCircle2 } from "lucide-react"; +import { useRouter } from "next/navigation"; + +export default function SuccessClient() { + const router = useRouter(); + const [dots, setDots] = useState(""); + + useEffect(() => { + const interval = setInterval(() => { + setDots((prev) => (prev.length >= 3 ? "" : prev + ".")); + }, 500); + + // Redirect after 5 seconds + const timeout = setTimeout(() => { + router.push("/"); + }, 5000); + + return () => { + clearInterval(interval); + clearTimeout(timeout); + }; + }, [router]); + + return ( +
+
+
+ +
+ +

+ EMAIL CONFIRMED +

+ +
+

+ Your account has been successfully verified. You now have full access + to NexaFX. +

+ +
+

+ Redirecting you to dashboard{dots} +

+ +
+
+
+ ); +} diff --git a/app/signup/verify/page.tsx b/app/signup/verify/page.tsx index 26c1cd0e..d18989fd 100644 --- a/app/signup/verify/page.tsx +++ b/app/signup/verify/page.tsx @@ -1,144 +1,10 @@ -"use client"; +import type { Metadata } from "next"; +import VerifyClient from "./verify-client"; -import { useState, useRef, useEffect } from "react"; -import { useRouter } from "next/navigation"; -import { verifySignupOtp, resendSignupOtp } from "@/lib/api/auth"; +export const metadata: Metadata = { + title: "Verify Email", +}; -export default function VerifyOtpPage() { - const router = useRouter(); - const [otp, setOtp] = useState(new Array(6).fill("")); - const [isLoading, setIsLoading] = useState(false); - const [apiError, setApiError] = useState(""); - const [resendMessage, setResendMessage] = useState(""); - const [isResending, setIsResending] = useState(false); - const [email, setEmail] = useState(""); - const inputRefs = useRef<(HTMLInputElement | null)[]>([]); - - useEffect(() => { - inputRefs.current[0]?.focus(); - const stored = sessionStorage.getItem("signup_email"); - if (stored) setEmail(stored); - }, []); - - const handleChange = (element: HTMLInputElement, index: number) => { - if (isNaN(Number(element.value))) return; - - const newOtp = [...otp]; - newOtp[index] = element.value.substring(element.value.length - 1); - setOtp(newOtp); - - // Auto-advance - if (element.value && index < 5) { - inputRefs.current[index + 1]?.focus(); - } - }; - - const handleKeyDown = ( - e: React.KeyboardEvent, - index: number, - ) => { - if (e.key === "Backspace" && !otp[index] && index > 0) { - inputRefs.current[index - 1]?.focus(); - } - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (otp.some((digit) => digit === "")) return; - - setIsLoading(true); - setApiError(""); - try { - await verifySignupOtp({ email, otp: otp.join("") }); - router.push("/signup/success"); - } catch (err) { - setApiError( - err instanceof Error ? err.message : "Invalid or expired OTP", - ); - } finally { - setIsLoading(false); - } - }; - - const handleResend = async () => { - setIsResending(true); - setApiError(""); - setResendMessage(""); - try { - await resendSignupOtp({ email }); - setResendMessage("Code resent successfully"); - } catch (err) { - setApiError(err instanceof Error ? err.message : "Failed to resend code"); - } finally { - setIsResending(false); - } - }; - - const isOtpComplete = otp.every((digit) => digit !== ""); - - return ( -
-
-

- VERIFY CODE -

-

- Confirmation code sent. Check inbox or spam folder for the code -

-
- -
-
- {otp.map((digit, index) => ( - { - inputRefs.current[index] = el; - }} - type="text" - inputMode="numeric" - maxLength={1} - value={digit} - onChange={(e) => handleChange(e.target, index)} - onKeyDown={(e) => handleKeyDown(e, index)} - className="w-full aspect-square text-center text-2xl font-bold border-2 border-zinc-100 rounded-xl focus:border-orange-500 focus:ring-4 focus:ring-orange-50 outline-none transition-all text-zinc-900 bg-zinc-50/50" - /> - ))} -
- - {apiError && ( -

{apiError}

- )} - {resendMessage && ( -

{resendMessage}

- )} - - - -
- -
-
-
- ); +export default function VerifyPage() { + return ; } diff --git a/app/signup/verify/verify-client.tsx b/app/signup/verify/verify-client.tsx new file mode 100644 index 00000000..a1e3cdd4 --- /dev/null +++ b/app/signup/verify/verify-client.tsx @@ -0,0 +1,144 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { verifySignupOtp, resendSignupOtp } from "@/lib/api/auth"; + +export default function VerifyClient() { + const router = useRouter(); + const [otp, setOtp] = useState(new Array(6).fill("")); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(""); + const [resendMessage, setResendMessage] = useState(""); + const [isResending, setIsResending] = useState(false); + const [email, setEmail] = useState(""); + const inputRefs = useRef<(HTMLInputElement | null)[]>([]); + + useEffect(() => { + inputRefs.current[0]?.focus(); + const stored = sessionStorage.getItem("signup_email"); + if (stored) setEmail(stored); + }, []); + + const handleChange = (element: HTMLInputElement, index: number) => { + if (isNaN(Number(element.value))) return; + + const newOtp = [...otp]; + newOtp[index] = element.value.substring(element.value.length - 1); + setOtp(newOtp); + + // Auto-advance + if (element.value && index < 5) { + inputRefs.current[index + 1]?.focus(); + } + }; + + const handleKeyDown = ( + e: React.KeyboardEvent, + index: number, + ) => { + if (e.key === "Backspace" && !otp[index] && index > 0) { + inputRefs.current[index - 1]?.focus(); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (otp.some((digit) => digit === "")) return; + + setIsLoading(true); + setApiError(""); + try { + await verifySignupOtp({ email, otp: otp.join("") }); + router.push("/signup/success"); + } catch (err) { + setApiError( + err instanceof Error ? err.message : "Invalid or expired OTP", + ); + } finally { + setIsLoading(false); + } + }; + + const handleResend = async () => { + setIsResending(true); + setApiError(""); + setResendMessage(""); + try { + await resendSignupOtp({ email }); + setResendMessage("Code resent successfully"); + } catch (err) { + setApiError(err instanceof Error ? err.message : "Failed to resend code"); + } finally { + setIsResending(false); + } + }; + + const isOtpComplete = otp.every((digit) => digit !== ""); + + return ( +
+
+

+ VERIFY CODE +

+

+ Confirmation code sent. Check inbox or spam folder for the code +

+
+ +
+
+ {otp.map((digit, index) => ( + { + inputRefs.current[index] = el; + }} + type="text" + inputMode="numeric" + maxLength={1} + value={digit} + onChange={(e) => handleChange(e.target, index)} + onKeyDown={(e) => handleKeyDown(e, index)} + className="w-full aspect-square text-center text-2xl font-bold border-2 border-zinc-100 rounded-xl focus:border-orange-500 focus:ring-4 focus:ring-orange-50 outline-none transition-all text-zinc-900 bg-zinc-50/50" + /> + ))} +
+ + {apiError && ( +

{apiError}

+ )} + {resendMessage && ( +

{resendMessage}

+ )} + + + +
+ +
+
+
+ ); +} diff --git a/app/sitemap.ts b/app/sitemap.ts new file mode 100644 index 00000000..e9d0838a --- /dev/null +++ b/app/sitemap.ts @@ -0,0 +1,32 @@ +import type { MetadataRoute } from "next"; + +export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = "https://nexafx.io"; + + return [ + { + url: baseUrl, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 1, + }, + { + url: `${baseUrl}/sign-in`, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.8, + }, + { + url: `${baseUrl}/signup`, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.8, + }, + { + url: `${baseUrl}/forgot-password`, + lastModified: new Date(), + changeFrequency: "monthly", + priority: 0.3, + }, + ]; +} diff --git a/package-lock.json b/package-lock.json index 49392d9e..c94c5866 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,8 @@ "react": "19.2.3", "react-dom": "19.2.3", "recharts": "^3.7.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", "tailwind-merge": "^3.4.0", "zustand": "^5.0.10" }, diff --git a/package.json b/package.json index dc7461f0..319ae9cc 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "react": "19.2.3", "react-dom": "19.2.3", "recharts": "^3.7.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", "tailwind-merge": "^3.4.0", "zustand": "^5.0.10" }, diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..7e64eee034e474fdc971eef38ddb18f471901df5 GIT binary patch literal 128565 zcmdRX3tW_C`ZjnhHLcXtH0iKr9?A}DC<;_onwFRchC~_4-7H*0Q^dewNY^aQY&Q?B zhU9?=2OL~Q6Jfwi{iiErGO%JWDH%DF66uHx%*=N^&+{JVeP$#uzr%WDyTbH}L1Ox=!_QHf`rv?Oc$O{N)x8#;i_-`VfN|}ZKFL2S+ z@y`V8E*)?Q|DnU&r(b$HAYjkBt}U;2#DBhZ!Gu>91q5_ILjEuCc)}Nd3J7p#zVPhR z)0f8l;*9_JPt9*<#W#$6@7^1~Jo;$t?R~p-T5&k|u78fK*b)E2!>1ljIJ^0=zTMsp zyl?A$Pn~^h{_G;hzh_jwVmP#S-n^nwU)@oC&^7G9_aoxc8&2l9ztUffc3a$*MJ|KI z;U4?1;-(6B^ETH!{lcs6v{ z=iHU690|JS${u!$>$>~mMOXFPmK#aM_-<*myKYrez3Y(s%xHaLX^_jTPyWJPqjd%a zS&Q8o_o^&iU9DqBeap#l&K{<##rmqG@ZIk7N$#W`_Tv^Oz8j>g|4HBcsJph_nJ~~A zVQP%%VGnO|xgGDgG+IYS>SFhe`}L03+zabEkGLA1a$LXkJ$F-dam&)w8-?M`=flVT zOMm@HvHlD9QLUxr!mF+pWAV~25?ppmXu12hgjClQi(`wsWt;0lg3EcN<)Wpg<(H4& zelm8*(BAH!Mz}0l4_SMyethNjiQ68U`snHx-y8Vx_Gh82G1%Z^ymaFy7)kwc^KHlOGy>ZHlI) zAz|QGrQfcYv90rt3lS#wmR0}g*|OlDb)7#f3U?k(7}$66(#BUmYrfp$w-Hz8h8G@m z-s^hYefC_?&ax)$_o;ndLlg89Tyc6=#pP9JH#PlqF6h~eUxOo>wXRc_JAbpOx#C<< zhm2njL^Q7{cAo0-*5H=wo%d{NzP>JX$%*3T(-9F(hM=O+7srIVz8PY1Hmr+is#&LB ze49|r3eo#>(et#ZWm%7po;OHWt|n+|u?xhLtjEUQmSb(n&5nf0%Sr`pDh z_*HZ5;~g3II1l9qYaQ3uIIqpsH|^8A_hmSni$`6wxSJC~FNf=^^sWr|h=`^pz3Wt# zv*k^FR1Ban8+Zaax>bhq!-Da$oJ!e6dfkd+#*Ykv{t56Z)15WM6Nr z+2a0YjnkOgFgUBlsBZ`lyO`l}{iMJ4ntMyJyLOuEfZiQG_F{(nynew6_mv>mLrq@< z+AJ+c52j__J7dVic;~s|v3>G-?d;OYaO>f?sUHpPoBLn~!wWC(ip|@6d+viBv!8i+ z*Oc``du4WcPWZUrbt9#&-Nxn9Gat&g54MEU@8e%Sy=%%x+`oK!SM0vc-49zQ;D`2+ zA9^l(`RTZ4_HCv=l$+rGc~;5Pk9PIS+;6&CyC`rH`|8dv?C*Uvbin8PWA4l|HSW$Z z)sH)B=zl!BZg{Qv)TO2NFVpOM4-EMH$-X&w&Ig{$&dn&cpEAB1J95Z`-d(1yTt0o< zy)zz~$bEc>d|W+s<@V{@xR259mL3_eluV_sBze2%4Y!_-!++a1_sZ=#AL6+N7MZXb zyX_lxly78TpCJDw`lI=Yxkl5{yO*1H-aF&bsF!Yu{1^{F{}i4@{Hy%O`1IM@(G%lm z+YCvy=CyM+6gK_+NO`9f=Eb)}j$b{s;|}v*@g5U`9@;BFCtDdJze0TZug}KH7nfuw6 zwz5&v9$)%6SRDb%RW?k2)O2>(pF5DZgun`JUzGziltC$Tz*UzKZM? zZyO3*ywS_Ih2E_{7ffXl@ynOoMH%8tUg{AMf!u;1a$Zm?ygN+|HKKo&b ztFVvUJHbOjs7>>(vfpH{D9d=4`;};wHD2&Zl_ONE)qS+oenA*Eqj#p3JM^;Pmenj5%a_4bD%!VC!rxUIw-ddxKw57hoTy?{9uJ4=}8ezvDG`92N z7b=@CL|f?|QwMu`*VOe@@CNPJ+Yr4b5Ka^=oi^DMZ2$$2)~-PGInHWG6~7- z^;p1@u5Oj%<9e;l@B|H?_M>NR&mlPwPJtXK+M4JTRms;J6QAC7!}*GK^pw!!ZP*Bw zqa+Txoy&pry&5JX(Td@detcfC8C=$5CO>6;mE%bLEdH^Ds4rO|nwzRKis7k8-$+~) zo4Av%lb5KoDm%B#xnqU4-rgJGFm@yhfNZs>!FVhjZv%^>w6gCbSWFUK3` zYv@A~Envo?SIFiB@3xzb84P%Ji|k1u5JOvRgFF&MG@h`_)D3(}N171fA$1KA9`b3| zGsGafz<29v&Asztto|?7(!sF;HsqigqKOE(Xl@Z9SG>}%m6pmf_DV8J6uviq>|n?q{6bIY)n$?gHI7X(01(11^y5AZvbEVXGO#_^Q^ zgO(Db^>~qZ=h+_qgj-T~AlLvLgpp9qV{rinFfiun4seo(2!tnam;%hFca1t`m=?(1 z_Aw676EMvqI$CE)!XfuDAN4q_VJ;97GY7l|`RLQobj{;AkD94PDOU6+oxLc@c}*8I zG^QG%hlC-q;Mw1%%B10)_8U>Sh{mYQpMpOku(GYiyGRU~tPNj2k*MLgz=AJZ4ZJ1( z0o^xl3HX0YmegLEFtA@<*v0Xk69#;-^{rgv*3VHN%_Q}adTmnNg5rgRV1kg38n*Q5 zm?J?Z^16ooapCFMgL;E?zLK3cWXD-jOz0!Ay377WRXc`CX$>BuO5T4=s<* ztiCVm9&EIt3P5)7fz4K*ws!Xuwd|nbJs9Q_`OTIQ%nwNVk}N)Ov1eiE!xCmyUrYlE zV%a^_`NvkN0U?4&%H|Iu&(Iea>RK+Bk`aOijNJ_vo^?=02&IwqS$M~&K#(Fu1I}=2}5KWJlSOoO(r{TS2vkmDf_)fNRTCgN34c4DlbQliI z<0~FGj#`kc8D^J)-{t1hA+gMD{VHTy2Pa~Y$?%_8hVl8H)T1J!MoO9^Kmxnxb6e5~ zI_&uSnxMyGk(-Qqo&8i6tJHmkqA&|WCL!5KpF)C$m}7JlC{3ZT6|$@sm!$3w6kS(jax_G*=8F9$d%92S?=wJ)TTL=EjFJ^4JR$srV`y2Y;ljHT z=T$@-VRi8Yu3yIpphu->;-Yk2D^Z$5gXakl;mpozHHPZ&He%oP6vFw^G^W7J}< z=M$(^^3U^ZrVoNu^6IDxzACRs`H$*L>SAj=0i86&nw&cXk{qrV^G}nb<`BA*J$j?( zk@e(VfI^EUtFP#6;3=71OCsNk@%P0FRU-Xg`3y@+Y*JAu30Zlv7Gh1Dtt@pZfB@-r z@i>mU8UC*4no~k4TJQYnZ-KUHsf&Ll@rJ-`T3NGZ8N(e?TgkVd9077lih^ryjS{LH zl(Ke_jzy0`m5%iuIDMu!l^6n$gRoOdOPP!!8E=Z-0K9>%x7S)4HDy8jisF{vib#7z z&aKduN6zHBWfGhv>F}|TBQbd1X*qhsZ>#l97xnc?-ujkm z2I#G9RwPqO&2fEZwZH=KR+*^F|3DL*Rco$ohnJ(&beqc&q5pBtVO59I;Y$JS){yF* zmaYm=r*7;3b(DBIBeXses&0$(IQaC4c@+iCzGkX=pDKRPq6D(5Bug2?axf$WNabH(9*AZLO zKhO+`rL7O^=#xtm7ljb^MOmLKxmZG-+x}^~TL98R&HK#0g61ltk)}qjmZHAyH84f! z7r;}A^daYS7Ph5vg)v-wRXI;*pYNs2_mn$mTdD;xC^V?4t2HjQKTVVrj7dXOV_E=7 zc>ACYS^zO_d*Trlo`r{y>xtMx*1v38-(Y>96RAUZ%EN19cGIpUngXxwHttTd_eTvv z%q=l#i4Lt;Dc!}wkNWO;AQDP$v#urZggS(mcsAd)44Y6w_!eKf6L~7$t|Ty@h)M4r z#om_2W54Q3=BRizaY-dPMXdBa+sP^@3%SDnw$Yf{vhhOsqMCJ@;Gn@Vy01yM5y;mZ z%(Yuq`y^`7CPByV!v<@YsQ1WrARH)V0qU#DLjiyKl_;EO(zj*?+D?rjG71kUCMbWT zy)B_<;hAifeM_3fJb&&}1fai>F5nv>%~3Q+Ba@H-D6S*;l7B6M(%}<~2%|0U8rBWS zle<0tuVfk`k!IE&x0_EHx<#QOjFRwUDFAOJ%9tn%L)1m)J`t#Y@Yt|K8dVwP*F~`RjRXq_8mvuMhL4>N_|RnxZbwK zv!jK);gv!-IU;-kxA<6*Ru*3p=zP^iG)+{u9m}AOhf9ulUlYuu2g)K6@e{> zP#oP#>=Aq=FC=*+DJP{3GAh`TZWY_LcxOYSv@DW!ggqh4UTA4|ki8b$;RvLga*R?< z=Gx~93r61~X%hPhakn6Fh7bmYcULT%hsr`>TQt4O0th7lsvLWRQh$=kdOR}=w>{+* z+G429BFO(u^C<{n%mPfrquUWR$iS&)84bntA}ICk)k*l0%&w^QJTaPtv-WTeQo!x( zGM=@!2`qO0^6;wchlTv62+)F)@;iSuyd@Q6bJz=<@|CuPm>gI56+a^l-(%~|kOf{V zT;IC9ZQX22(^8pq*l|Pu&bzXR+6Fd&$i*1@C?#V*q8Rk4qNz^Ui7ECOTW@1X4RTh0 zI+KdZ9KS;QqB6@9REH~B-Ai>~DBl^*=$K7B7g=cLF~gQK4c3Ju713x%;!pAt9e^5M z3C_zKA^YgDAEZxN9APtT(eQ;McqERw{a-FW76e>E4J8uN|KI43smf{>*1v2lr|VknzY+if2g5x91*6b^Vbl)zF|)1Mi?yRzW~A zt9lesEm}usJkAv?%FQjO9+ybTlIp@i!t8D*z?6mA5?L z!PkMk5J(HwVU|Kl(p#mAK~6xV<{E3(pwjvS6%Ki1B3V2b;kqJKsYtTXQE2J7CYW?D z2)FEA?#h?F!m`qc#G?tGS7j9(Sq|^1gyvC!XI$5p6f%(I5}_h5=kvC1?2jW%eN|r3 zJ1>o>@Cs6>6cwRYPFB#rB0@M000Ks^(g$Calrs2xDl0B|WqDVumBL-fP3qloE@OYb zzox6V>V5m~(iQeV1c>V#h*1{q5OJX?sr!)>??TjMm5`5xap+>S)ypE$j&WU_J5Vf0 z`O+9B;*m`JZAtQ+J_qPpTscN6CGfu+-zuzSf^Qfw&3XzvO{pPvA|hI-;TiJ<)8aFx z%*q&>)F^KUtLdlsCCC>$yd-SHXW5T!ZI% zkCxWYdxS30+5ZkLQ<0_VQu6>bxNk;pOl|o_ju>E2AH!+99n}${ft=#rHg698stwJ9mV(6zVwId?;vMfg{SIVqYzKNna}8o)u1r z4ti?gzuEUyLr#2SV5Q;E08Us&ErE2cDK`ytFSvsgx$bxUe2769WNCq$ri}o3vWvDP z+v-GuT*0Hl2c&0;vp(Qdt^OoNRHfX(xlgbQ!(#8bZ)P>32<@QbNOg4C?l!c4>1(v< z)Q2+zEY_xGCq#&SK(DplfmCe;3$|chFE)eyN})VlQL>ACR23sKC({vhFka4mcoy{a z#|>N7_2l&51P`Ov(JCIs5CT>yFZ%^tZ|^)DFZ|!9E^cil_*$z=!SjQ27wLXei9K)kBax4&~)$;I5GL!15}DJ>PA$QuoPmFT&}Upk*Bg%#&w z2vPnVC}zKE$*^x|Z(hXd%~tx;BUW%zw2==($n2BwJ8fnBB(<8Oo5$y&)pv`&<)<5806vV21bw4sMq70JS5OIkl)g;YsS zgc5zm2##Z>!V%v!rR(A*$=s5*A{`ZBAEQ?M73y#&;$&wliNJ?DbwJx5vgkqSCY9zp zgT3O|61wK<45-LK$f_h+-_qOBVF~0K#jm$eO-j1R3Q33<#Yyv%6eN@5m;G7VHuF45 z)lgyFD$M8*TwZAX$|2J)t1{m9?;sToIUzwfsKaL65PS|{wFHc*;&Xw|mGEg7%8!k| zHHwMgDZp)I*TloDw9ovXi;YQjb5q-h= zOo%jRHcpDHX41|IucR7>V=k<1Iz3 zSttl!D17jwxvCr$P-ETAeO;8U@CVnRb_(fdoLchMXUQMdmM_}KPC~p#HVG(V~Ua*o^H=n-yEi=2Ugj11Qh_OSW#IUk-}#>V2?j(z%y+u zQZrb_lh1$^^0TXL9aN>PnyF~o$5l|$TK z*jZ0>DvZeR*_;vjl+vMj;)`%}-&Gmi=c`GKOdNBkaeiw5-K8Ce`-y#J6#s?3#>pKq z?96Us=e;?lCglqxkWU)<%PTULxq0D&$9ZjO*3P%52XP~-)T;L^-wJPkw0}XPxI<(& zLzQUHDL}v^Bvtgk~Tyv{Fq8*8o&P62Asf zNr)iyD^Zv-X}xmc<7{;ax~t+TM}@Y~)7Z&uk`cu%Rg|Uf==+4Wq;%rd4Sto6p@&RF z0K(U@)a$W4tgID-D}$wzFx-ZpP=dIJ7O6kgq~atk{bT2?k95ufZ1aNoJEMLeEV}j?wRVsnvRlq+Y~;-$8N=yGL3#3=yLc$WW#i7Ztry z@Ft8#qAUNOSCitwVHK<{0lNIhc-4D{A75mW+#d3hpSm4%m^Nsz9QhdAP#66JgV z8AYvtEC9#fEZxvM>F+=E}$%V=te_e?62&LOk@P44_8NCWij^1;t)^17x_!ueM>U2+LnLYI11Z zv||P)YE{H4DSjn{t@Dh9gRqtRO^%tJ$q+dLv*`(J9FKy^nh*2LalHwC7~Vs+_n$Up znz2;Kn(HyZuE(8}y7ko}>7!2~)kEt6mh!JMbul`33`qbgF_2$xD08Zp9EtUs~?gi47jY0HonP9_4nz zC;6h*bL=R@2SOhEdCKGWk`uFB0!9s>6JtjpDn=1~Cv}3G8C)!)cxdu4xu5mY8k+Z- z7^8OVer))ou|;pMLSN3;O8JUF{J08&QNAn~S3Om%jt%Nf`P* zJmiL5g)bGOKBbD3r{zF7BJP}yOCBsM9sQhQ>=AtcP5o;AN>B$5EP)7|~9G^9kGJo6i#{w0zT4UVvFS3ad+|gDSTk%fx=(BiRuyU%Sd~#$+Bk zoZJ_w(%OM=j!q|<2n8zg+_sw?grqPD`c|w4KK*cvu3sK3R(+)Oc1a_nydp?nFU5M& z5?$9;-D{IK0U{gwaAyQmUS&^xQd`(uA>r(&NeOnW6TMFL4;1?0Og~ zNBobd8J{u*k1SO3ULncN<+JW1Yo)xn1x`wnBN# zKqQ>WH}(_9&i`MPAtU{Xlojtm%_aHdr6*jN`62BO!{R=-E%c!l5(~O_rkhvHier$;egeQ)DVU z&p~;MM*s`-Ij{@58xa1Pp~`{0bnDers4_?K6RW_Mnwr>Y245$%T$k$EY#ny&kOS9m zj4axvqLuMBe@&85{27=eiOqCw8=1rCmoAoX=XzmF_<9M;O;SYe6=AZ8U(My#y^j1Y zH@iv>c3FiCx1x5BgptwcupUns0>;LPBRSj9<4HJT9#3#p>!%tdtBC*(@J$X8VDNTb zPjU)Y!Su^Qf3*HLr=aU(_p$L2nSVO@zh^I99Bvy%879elth}?=MFIp9RBwvmKfQB`$>z-P~C2MK8_DbdQiZyP{U)zvFL?R;P4tZm% zl)G$QxVS(&7}7K0w8-Gol0z781xkWk$|i6JwMy`e5Vg<}N8X!KU~npcUMasp{}G_&!ufZM%C1s!NA%CQh_pOFR1i?HO1q z`=kflvbX;BCjpX6mz}dbdL~jRRi%X%H%;?7dO*xusuU2hvdOFhM}+3Q6m@WlYkI-O zO9XLXpgbKFM2jJhO0@ObvMlw6pA+eCmK0EbF|J^l5?Y6gB6)FufVzE8ToTv;p}wPl z!&ePtzZ@jo`;ZB##NX}a#v1#XMjZzG*RuNxviv$DE8^QXdz zfhXAoqvC*Gm7Ns>k7pt(u7?Z;{PrOY4o0578PUe{br_BJk$`79Es8qqRD6Mxii)BkgQwMg5tyG9aX&JSB;O7Y- zZ~Z^Kfrmr%oD4RCNQH`evg^dfJ4ybx-1iD>>@{q&`(Ak+CD(7qHCmxDy3OhhY-us$ zIZdc7s^+X&ZN2bSfaU^`t#VM7Qf;UlVfs(R=sxldzyH^%oCRj-!WlY<9DpFz?gy>& zV>3zk^eS{V)PmQQ^93^VyWjQ3SD2L)fs-LGj3{HHQGDUf*0?T7El7E?ByF=_5*8vK z$i6@rvS~5L9Vhs!M+FAX<9Xwq9#tlPXWfl`vdOBxaE})U@=Nu_bsiXWq7wh(%6r4Qj5ydSfV9`8b$qHexf9W)0O&;{SNuL5@Xd!!}ZJ{IAmSEEZ8(-ZXoa26-%p>C?_0a;%15mOn5q{~6svw~d5cozx>a@}sct~OU2>L;DUifYOak8E8xqIOW(A03fG zTFxnO8NvMw(U`OI8h-Ah>qd$Yig4oFCk1st51x+GfuUb8sX}8(sf@5Rf)&VdG^E^pIn3n@`?1{7#fF7~FLd^6H#kN&8-yGi@em*yCa zt_XL7trs`NOn6G)iJLojK4AvyK(nF*al+d{W&A8t`y9fBY%M-NJZ{>FtDzH<8;U4Nqnm(w004!0Ive|GoN|*V2FPz7Y*x*m?M-&ZM_xz&*Y0 zvRhRB*NS0TexEo|8Nx~=iJZ8%y7-tOv4o>^eK$n^OY@opg@CV4lB6JHR!0djHugT_ z@z}QAvg~DAmIuaj|A?tf-y@JIPXTb?n6@4H`;f~kRLA>@I#s^zTz=X*|7*hxl{Ihu z3VTU9=L4@oBcmL4$O9haTObSdamxZNf@B~*Gyg~E!3=&T69hB*+h%AlPU|F7M@xu5 zb+7iQqS>G;g-5NZLlx|tdI3$E&DJa7+?8rc?`d!mX?FWZiE*r#0dPBc_v6+ z_F&t|snE8rFU)bv-YH)g&eV zy6jf;<(@UXurO|1{hV4Sy|$0w@_1qB?*(j^v)MHZu4oxWFToUGi727)yng~e;aO-< zUKk_qLgRFaN7`8Ds0IpF=QIlknsXlvU25uZ#C*?obNW~18*7dq1IQraOIe*kG#NF4 zHk*l&!qTb)F>AX}l|;+tFUhYC{j1@SWZpG>SBX=KKcm9TaU3AM%>f+hz_Hq07*4q| z`{ANhYm?d|+R$s@rI%`a1<8P67Dg1`_&T1?G4sy<4Pe z5s?ji3SOvAcW%eK@KR2D^abiWFc{u?y(gp21^R}VQ{LwJ#i3L1 z(6a*PMPS_}H6);7ar3Dajz;If1-Q=u3IJ)L(*}0L12U6wDRu4}7T32#s*V$<^s1Bn zrdCEZDL0e}IiNws#E*4%>v$>A~8WADKXS_7a5 z|G!I9u;kSxs7`)P<;<8sc6ou6V2}cA-}m@@zOG;1{9fc#>xCCfLYj|W4koU{i_oax z+)?eYHr^LYCRR?CT%RO#pCykbA)vUAgxPQ$!|1xtqYE7+vgKtD%*>GnDizCQ50l~i z^@$!N$kzSSlsuhu)UN~umg|DOmN+iQsk*%;M{>s;a_>DRL!-_?SAo0^&%JVI@RWLC z(xDNr+1PHrHkm*-);*I<#^%X-x~XcLrj%a^^(u|oj$VPb=Nqh;n0Q0iotw5rsC-J! zf=W4LGZ<4!l1ES@U)MD%JVkars0;_l!z*3-XlXaoF{?sje8BU>fV7%c+A_2lOoGNn z5SG2Ev+rqNaq9SL&AKk+)&xFyNJ1?cuAExvaCGpt?77-h48|bTg|Kz9z$nK6m(tVh zTK5`v7QH&i&pWRaU=SPRdG_S}Nc31hY-1rl}%a+|jbL;B~X zUOQ8-TUq})@FiM*C)4bc>6N2s8j<<*(o+cRAPI9_ncqojMR0HV1XH$h>ym2I(o?p4 z>E%G%Px(XzirtWmO#=0NajAWw@(iskK`45w^4FjG(JnT9KbT0*?x{6rr`bEV@f^DJ zF4wY!y2Krw&#im<>0P6a87##TM8X!3ONP3(vv|*s@j=S<&pPcMF zQhMaz)tcbsJZDs|&-V}P#|AV@=Pjck)ZjiXXLAjKFUM}`1|Ae={pkoyFKzlc^fB^R47^ zJbC1%MHzofSiXHt@gcRLV0-{)sL6dzJnlz2A{F$M73t707R$%XD?zzJBMzSuBtRY4 z#y_yqFedchsV`!n<;#TyrYpu*6=#XC$o?tGX2;*> z=3V2YvHcXA>O+FDYMJHj+C-lDp;Nroa-bO18J{lvkkHF#$4d+^^w=M?<$kl~;5mLwU zOciDR35FI>uUD}PpZ-YqNP3y@tm>j5>Rz1GnNGc8;Eu*Z9~gjte-FL+8zV0rUQ!#a zzq-iP^zJho9-8@5NXocv6CsJcJ7?>Tr|i8tW{*kN4k2XQ_Pt$K8u~w%ZQQ=7?8cP9 zNvGq!D>_hm)Ue5DycRPQ3x8+c;w9S_Btp)6)72+*7yu~q$%IfvjF7qwDyn?@U<-GH z7W{V&wVTNC$q$_y)e`^^w`Sx%n0{zm)S%T&3`Lx{uUDYy|@bV=dIBr$cdS0{flSMy5yXmF|V-TWujD}lQw;iO+dbpm4m=5 zltihnGnoqBmtsWzNE|@he%G&KScgQIB|>uv?`HyWLBN%KQgdF&cPq|T9B^H)eQtP1 z!vpCDw?!oaP5f*M+Ke(4c2}Cy%Q(N6;n}(Khs>%T5>@^4rxAytnf(O;;7Y#zJ-fMn z92w*x0A0_gC&nT|W+Y>dUu9;8)}3L8r6ai@+t0qVd@nteLs5g0A3ayluA+Hy@wyj) z9}iDl{5nBhEftyq*R(}BA-ES}{^E>>amN_)lci(Mx;yrStwQ}Lu$bi)WfpSR7eW0D zXpnLIv|FRHHSj>8K}smw$QN!wsKuSVZ0sWMAnYs^04&;d3;?k(h;Sx71`fYtoPpRI zH9Xs6ZuEgP`(yWSd~L(eg|~Gyyzpw=xJJixGWl$Aq&>CdE$rYc(Ooo28S_dC7Vg`8 z9k1Q^ZzPG*RZ-fPlC>+_Z`@(txp#lB+$+G940yp7eGKc-iU5Yvu}HVyjjj|&w=ByjxH$6imWxKm+ngKV+?aSCZ4uV#{1NbU46v- z67j5-hdYun7V$(PRQ%?YdIL^*am)1=E zEcpZ^4b|-`R=r22W*>kZU~AfXqYyS!UN?~!D(l2apOl^$t#2t3L{bQ>+$1#`ns5m5 zZ`L8n6V7c8w2gai_{v>~%}@5)eWlkITen{gBK+Ij85uUb=-klnG)E10<(ZD}JxEJI4dw>xxaV>r_L!#PnitZHA=zAQ78IqT`1HY5!6g1NdV(pfjc@7_aZi%yM)~%& z2cTF12}n{VN4DTc;E(G4a-k$9XFH~sonBE@@mOqiyY(>#GAG!3rSCyvA?YaHIeN-x zRo}j{682X`?C-pFa$V@=b{of;HBE~HZF6#sVSg!kYv1O@mp77FNE2{tLp0P5hkN!- z`D;G?aYluvn?e~6pM-2K^ow|BSm)vC&3EeUJAxCyjs_Py?_5%}dkP|f^MSMw$mn5oEaItIx@+ls^)2N z$^9gXT!nxLzhNVB=fPpeKH^zevl%c#F|Qs}DXigc#l0H`Pm-g~g&Z@CDOxfKspn_M z+Px$w+|{_iekjd;$a4RU`<~f&zj<0%V_GK2Mc|G_Q@_B*9zBUuQ%!01J!5+#g%Maq z{%7ARQ{kFiDFGm9r;`jC2lr(chB8jhPv@0pNyTls7s}rq`0xFup|QGM?`6&l4l7=I zF)eY2Ex1><=>&vxzr2@OIsYNV&B*Z~jXzya>N0I*X?5}|C64&Q|hcPI86DZ<4RNK*Y_FPU^lF@T(bU*zLlg_;^C zCnU{$0>od8zqBphmW}P2w;y0}arLpf-o}Cb^FC}qJn0=YGFN8{V1x<8Pc1$ei62ypN0y$$cBWdb3^4hp$C8s@iJlN7SZ zHtlP}mN_2{CAae-L?Y`v5bM0Qp>s|WIj7jZVpYtmmqzwq4FoO*O-`@GmrSIGqs~Y1 zsMWJN%zn7TaEyZSzH4!|b*2Kt|%R z>WYW&t@cIvM)H?O4H3No2%Q_-SCrkTT01NjifH>(;#E_l6Lp!5H|`r9bx%={F=iYk zxBm)LOj$M@8xSlMBz)gPNuiU1MF?E@$iS(oyUl>XPvkm6ZsvuGw89t5!51;&sdBAW zcLGoL#D2_$sB5}+`RYeVJ0yw^bIgoeF~ z1ks;UcPi(`AR*heJQ8{JX%R4lgf~+a4LWV5E>(%hzY$vw)+0{rBwyTaU;Ox zWjJ%^%ve_VUM3|ZzDEV$xC3{iJLi4*3{U5qYLACIOS?L+Jg`EG&O}V#yqcC_&fsOY z6!)+hGKvWU$nTZcW8M@4F~?!e9A;N^DHB+?sI4ShV`8*cXH83V-||^$6?$^tjW!-8 zn=-oI9V(ez>}E(fI}Brp>1d9j6n1F3e4;B?HEq(P18km>jHiJn0@&`ej(i z4eri8nP<}sx!=_wJ7=hn#XeSRzLdr$ZwbdedC>#E^HK*CZ$)RYLosI?eE;~4IX!ys zxt7$Q)K%oQxLLgBA7)BiH zL5kzma|%OxvrEtXyd_lHOXlb<$_Y}={UK1`ug)h`(X2q*V=+e@*2zePc!b8$wFxyX zKMcEM@4v1`)M_dhTNDzbTigCFLMD6eHS+==iHsfFMA9)3p54$b%Cgjk+q`MLuLhGz zgcMLhwEp@LHu8?e6fGJ;r;mKU5hQU;^`-CEz4VTC2q{~~){R_xo6T@(T&Lmb7y~%x z{92UeAFSPU%+TWwGSIAk*>6h|SI}^QDg||{x*4m-We=tV5sJ2G7!!tU$@Q`2Ih6CX zRs`rE6tX%WeE8_lm%rS+`1fQ4WC|C!HTNo4Xbd zs7+yqYHuYu1`YC~$HM5E8=$6=?Ng4R4^*1S#U4Is6S|Jrb<@-Ae;nq!kHHFfR94aiuc zOT--6-Af6g@G8-l%t^asD>M}}<>6=#orJn;1<>B4U3J5V7xHF zrxAF%LRIp7UE;^>$dQs(olB8ORe~I0CA&mLB=Za276T^2j0RKLH)4)O@M?<=3q6=1H#VjoZsD+_{GtMsYxit;sX>whSU?dUX^er zfSp`m$`qjD!`6G;H}2yDpW+2PaWnIIKw|m>pIa~%@_4tXF4OxLl&$(aDv?fJiwk?U zq~K*xYue(`ee!x`ZQXHoe@p?Q+S}0|PLFt{6Dfr0B}1Cz;^+fQ6Q7K&erPh8qDuiR z-^r7PNaiGXL5e5YGl^a=5*Xan_j!9w69b7Fiw?j*6aY&i1zlB+9rZCPNYazMfRNIb zJ1#H8@nZXmwO8!-fvX@De!?WuxI+TFfOys8jy+(37p<8RGOrefmx8XOrf9G+YsstI}=`!rRz z;he+zVhL%qmkxAAPQ_&Z7Ub$1yt>n`G+J$%bZB2W>d zJt6V!5qdq(zL=@n1g{Cif>RY#;osH?2eRYXXWrA0_R7TGaHg!3)LYQ-BDw17pi*Fk zZ2a-ZD)2t>=w2>HdAchtbDs7BLJZDQHG-1*CM~luieRODko{soE1p+SKn95yif+Y9 zIoBE4QI|jKNDgO27ppdeEa>I%4qtL8oZ0{|dLS)Ps|^Kd82Ntb*m*sTi>v^g2n+0NF~>@mwRu16|tu%;7i|`Vg6UZ$La@p~a)$ zNMN>tQQ#z}G_dpW1%>?F_JY4kGrF?kMI9@ER`HhO&&L%yz9sihe$rqCQ4JV$l=PUw zJ9nAZKWLFIF>4-fLOFEM!-Z8dnM| zj@%YIs9-?Wf%u1&j=EMh7V)xUd-H71Vsw$fo!qbh82+vt2-6({ItyeIkp#!u(eh8>{x^MnIc*2Y;q00B?>##Km7paYaGC75+v^+R2$ftRiybY93z%b zh4ZU`AonQK8+cQ-=+S9P+WiN`dD(6wD9+H&;&ms`Y-s%T&Y+<&)!(P4{E&Xdp3QR8 zkSXy$=fpvgv-t9=#cMR6>_*2_FFPS>^T$zzJ$U@NGe9=X)FNAuGE{}AqtV?H1rr_) zCnp-k^O?K@u|OgwaqqaT6MyU$h=BE$sOSeG4ocDKrW2yELi0qduKOLVm-)=Aag*w1 ztR}9Mkr*9QU8xBQCNK7L;lrR($Z3k$!h%=9-5RezNbncWfmcipgTjmUg~{s**@u3V zx7MMGvykc8*IwzElTc_k_~6G>V4IX5+iNpKr+s(iEV+8|sr+q?Q=8^J_!2=@*jqtPRRzQKwE!qWH(I`gFo<+R*OG*08!h; zXAO`?Gv*6XA1;HcM%Cl|yPX+zjN|&cn52^L91|D2dk~#UNEUeC`9bb82RG1QlDJNj z@n9_I$xDT~@sztSd^AP}b<~y&-8qU9=XV{NKD!8FKJ>lXpmL@?JwAN6H|bkelnquk zA+TBMX+nDZd>J06A1vgn=?m9Q89^%C$k6dUN=7tP2f06ab=M77gzI`QG`n1bei_kF znXts7v#&3H`)YevLi3FIMmNl{PFJ6l>IlQ`tukb_+<=UV&g$9vA;ZR#EUAyt+*P0t zY@S3R?HPAQQE|#Y|59ulQ`S`c{j1}C4gciP*w7oML-9}az3)Elzdp#g{A8zoXP+2Y z6+dUi;ttR3uljQ28{a;?eE+C+uiZWFfZh7>MXK#P{udjV_`d=@Vo_!_XU^-}> zF-BiMYsQHaI}#f{9F<>jBK__8+J;w(A6`H7j-1z@GW=u8u0O0FdTZvyXR`l3B`!aq za8Zu&`X4?TdXH_`j`BG2U+%(xx+C}X{&`go;ybzQJJ0Uh9GLrhaKM__k?o^S{dMY~ z<>qd&BYRHRfREdzzwpqEJ16oV^Q&s?8=l(r;QE=D&l`;&Of}rPll-x#eEcqZmQb5v z5&LAeo7aQ{mT&DabeZ$LfBk^STvid`2v>e`Js_J<-u2bITdq9cbme!HH z@yz&o$HaSQ^qiQxB!@mcT`4@guxP?Z^Yz`v4pT>NH;+y;|AJSoF%2k*d+;N&xb5is zs(%G5g1@(AY2t~f%z>e&9?ie=aNPbtLwvjJJ5Q5`!Ph$&`h6RR)#tVee{tsJC~d#Y zNGv#86D&CW$gll-k$Z28itjmb!+ViWd}e;}q5P^~WAoP+(<1lwPkRn~m-G5F+4wu1 z_HAanA+-YapC?N`XuJ6@?IX`&kGGqv(cod0a}Rrl)Z?F2~Vbihh@v zDLnDnJmOWv=0&3lyJ|b`h>MWF6gKZPH4^%P>|*A`X9XjP#M{B+7E28F)wulhr5gfH zK9v}KEc@KduMN|{ow@>Sel8c=Qo7hL-aF&|nWq9GpTK&+`DdQGJ2o(9Kej0423<HIbcba*5QSm|q81kYyuVY5gL;0IM{t4Ob<`eY;Y=9THS7^S<-f&l-ty4_; zFYT&X2ai_#9WxKJv z&+iNA%%a1L9OLBbetCG{-^tt9YtGUAdHqa0rzBiH{vwI(*K5tgIt=F`cVX+1n`IQX zt`iW4XuO?3;%yq@?Tq`Q$U2-i4{3iF4RZd(89doqnoqHJcxmKQ8sHGWY0C4ycO*R3 zmQU=uG|~EDn*HbJ-_6Ez$P<9DF)tt&cu&Zl0gyUN$blr?bHp%+n;%Nlb%bT?F0Jkv ztGg;%dR_4{HRQnmpy;hutnC&UTyu;T_9cx0oVESwlbM%s`IXKda=tu1bRA6Wu9)=X zZW?1>82Dac18G9y^PRXho{^Z_8JY>%&QJ?IY(7{=91a;&zy?y(LNeO*HQLb?niQDI zhj#OrO#;xNnNRW;uU|9{v>8J2HkQ%0VeQTF{jwrwmKdfbb;ubX4C}(XdktCUD=A2u z9&n3aG#+HIT}a9?cGJ{d3CuISJ(}cn@`xNLX)S|N*tSUfW$B~&cc11rKv+o`a-i=; z^0v)o{UIfVZM^qTeJ*7|^ANHN`Bge2;7=y~RhaR)H1m^+lsWU%{C3PA06r1ASXA~m z>AyoV4*6Yt;8XE(4v0v$L%Jp})BLR|NnZQ_9OPNU*-pfW`CK{k@?~31;Q$U>iNM8c zwA4G+Rt_Lm$c)#B^JCV4oTCi*A=0T>Grxz(f7Ay`FVEJ}mnX1v{%kGsAW|kp!z|pg zj|`A=FyLJRV9m3CwU+P5BEdF_#SejdRqUbOWbWv#F z+dPmf6J~X%56O^8RA%>*nLqmjX_!AW76DYs+~2b_Kna0l8%{GEzvyzy^ZW*L1tGxl z39kdm=rCH*D>yVrV02{malluSZ2|Q8l0if>TxnKjN|Xjz>1tC0=1Vi_YV$F6A{Kl$ zmP=VG)g0lGv!bBz9boUsEoBfcDl=wuY|+B%;1QiEtnzpJnp+Y~KauxlsZnqOndOsJ zh|9Mm1Is>w*MDR%twklEhuO5WG$H^}34os>-lrOccg2*uuUMScKbN4a7)n3$_VQ zJgj4m#FGHrNU`HPj=mRJhrk7xm)uNHqi^3jWMT0py3A2^wV!cvzc196U;dz`hEmkf zAInP*8u#QYZ#KPMJ@^q~F4>PH-a#-P3*&w|6aKw-XCmOzYal@&tufdP&+#k)0ykY% zQji_kZKP3^Q3P;UxUyHafJRjYmfO)nny=I0zMmE3YC1)=9$Z+c*kekrWBb91qIUe& zt731k5vZ9EJPY2}WPLEhdiqSaX~U3C1QTSg!Ief*BNB2{4?TszG@{e6B(OEWoYJD6 z;=~1UnSI)AY(r^^<@kL{(>BH<=3Xoz@UVGQ{&&Lyb9~?&R=Zz6Xc+~ZN7JelU&7AF z9`O-?eI$_Uk-4AFd=qn7elQQ2vmyD~ACHtHa*NQyYx3j6u~_zso6LvrXN3yLRH4bb zC5qKzTx8`pl!Gd4`&4&Q&eP;xKWYOVB?mf^Z{tmo>MZANhE2;g#@j`NBLy^yz)XoE zK6c4#(dF6Z7zl0KQi;+(bvNbTgiJ_DN+DQzJb>Bs+nD3fT z1X-LR$or&L+iy2Q?);OZ5~Is7hC;J~+Y}PLueKh&n`>V}742^BQFqI@4 z7OMDYAcQLE#)woMSk04dsf7H9P`o?7M_A2+jV+EY6#lui`WsIK2Tl^{2hnTG<9tP8 zk*-MWPWTHFT1hsQ;ANUvX`cJ96s8i&fTK<>b&7=p2=?TOZJLi7L5`7X6ULu@vUMt# zd1z9p;{;{@Hr$0e;+{a;>9wtS3k#ey2Qo9Iv_ehH@zE3O;3>ip25$)ul=_D}qA; zkHekE=5>z>Fq~x-c;%mUi2)Ud+E7ffkmTCL|1B9*p)bfEU+<(0z4+Tr#^)t z_MhJb>BOUFY6%=cOqocT!Ei};m6nT9jDPXkT1d`LqvR7Qli7QVNmC?BJo_{^gNaU2 z_pe%~=^R@v7OF=OjcKO^8~P5{eUk0%AX*z(*AiD0z~Ih_J&mT?K)(ofw*1WDCFKQP z7E3NV2|h9+Ahm>$N|dP;{vO{X^85w~T#~9=@iJbC+ssW}qP&-*VsRqnui6BHNKJ}l zyiQa9DkukH;NdJy9MW{CjxZ1SuGEXr#4Ykv>4Z6QQs2ah1j`OMh^q!7ga$QvU4=#5vPrI52K zK?uEM$<=89Wye&yuQdB9jFFiZqBISeXW6S3h9nLysq*O7;c02=P=6}`>Y9U$aQ7$p zHs_9Syfw;Fs_7i@->OP8Cw@iQ+k}|zD2V*5N?YFwZxNaE>cWEXlH@XD8>&!w&zdwc zmm3!K%agYz{>mdvPn$;AqTL_H2y?6{^%2Gmc)APlnIFAsu?}p4qo?=)clUV1~q%!v@_?mFs9MVvIj&snIR)%;O%VrxMG^Thopu z7q~tgUs!VFmbX+O{9w(OB!=*^7MO5e)&6c#zZ+}7V7v-aEU(xMb%52A0*8YO z@EYEUZo^iLK8~ltM#T7hjd`cgrXeucFg+lL1tX~wi>)s2y?3zyWzqspqypdf!UJ`i zW+;Nj;=FbJJJ$UlOgms7i{{%%u04&vV*t;~cZL~Qk$MEjRfoHr0`FP05PBG)BMKxK zmANs%#uMYzX=M(<>`0{W6umcfzmQPwvr9qzw2MhEcpx^k;BT+Sp_wYDLwHpT{#4sK zX!<4Tuf{Qj51|Zu0BOhFA?e47PAzGMG1urIW^`J;k`)ms1|Pr&R748yRn`_w=E#6e zD>YdIkC=ybBF$=k8S(2Z$x7`5Pa`g)&{E+96qFE`7LS<|m+$J6I7~(eh<3?SBm_i>bz$rn*5D1Lks6pXS_X;7m`*fr_lfC@=l zBGq+LsKJD&!mtf}QIfi>-}jO05WI#VYz*qaq{D`3>(0jo#Y#btkBp&OI@0_nt3?Oh zXU=@2-CaBzhKKa(Yuc6*0;k1r6dwbnz&S2|gx-!c14011rBSD7S;J(cqa6LC}_%T z>#JysQw|&jFvFlqt2MiRm8qz}+{-%>)QpIVs@<-0$@j6%Ht)$6Tb zWGNoGUZ=k{fGDfHwmrP_-{o{)YYzp_002abePepAu zxl)uk84tw&E}u!M1c+)vsSW+HQWFtibIfRQLYIDiq{|>C*~b^ySBobJNHp`-vyul}Z<1a5XG9%4HjjlSl@yWMFieOW za;1Q1{UC_UrWP~D4vm?s`}1c754T0Tu0Ya~{J`FpR?}&bbpqeHAo0rKzzRhxM%5@; z8LB()_vta@Yh$xvMg3(DB+AD=fd#+Zqc~N{{P}E_?Jf=NfsSq~Dm{NnP|=0>w!Mt< z`tJ&)>o3~P*_}HQ5aV(DLe+7p8lw&ohk}(hi1M%raNkF8{(%?7(Y0eeQl_8+XZhW~ z-;N6ixZdxDXP=(#4Y{Zts`Eu30g#;!_??OO(hXEL zW(GCn`VvF=ihvw;@T!$_qC%@$$ik}8i=@;81Ca?P5p03#Mh-NUV1%rq8sp$u4N0_b zarr1u1jV{6veTHSs60CGIfzJ|#3JA`+7#e4PvrcFN`PjSo~mY=WnUQ-kQghW9KH!6 zIw=oc7TW1t$b5D&xhTjt_9b=DPQw;!j}p0Ff|i{&z*mrW)xXks$W+i39TVFt+LW+b z9RD(Bf&#dRHSkrM2G?*0jqvYSN^b=lbEkQsvBApCd_|j{d@*g1N=g=Tpad*Zp_2F0 zddgTmVFPIjX(}~B)q^6Ou4^04unN??&8*&q0tutfr?ytLjZtY8yTpREpq?Y&WK1Xy8WHpc4;QD;x+0;TfGt2oOD# zNH09wrufnq^KC1M77pJQ;*E!KMK*t*pyFO0y+B@~8~RuK1}MpIqG_!bocK^ru^KtZ zF7!(4zeZ@P?2FvPDt%Z8Irqo%l7<3H<2gGwMD#fHipKF<1n7on{?X9)> zz1N-0xU(YfC%R8+`b=xXK6)%XiP%C-#(KESpAePgPCzLW{I@hlAi5$CT~EiwWskfUo z#-5X_E3N{611J4=O3-c7nJ!x59};UMALH`6rF3LkoQJC0}W_@@pg)AWmK2AVD;{1BY zYNE=b+Eg2N)|qJke` zDxnVIiB^m->1T}T>R-n};XKUv@R0mF^LbIN-jqzSz{x4kZ^$u_@*s}SDy7D5{@Spq z9nNLRosR_7y$N`8b~1yp{eHBd`9Msn#7%Z{bZ~Rk)uQ15s7xyZQDWnx+=Lamc7$~q z)Hi@BO<6;l1q`}c#@?d32LTJ7?19%v5G`~6Bt1BPu4*;%mz9Z>ZyD38OXz9AbB zAZVg|ax4F=)EpwGdORB{gt6V>*~@H&D@eV3KpOY%a?;&_T%brtC~DsxRb9{Mt=ze~%S|7Uw#OzSZ_zz+_mK)(Mgyq12`HsI2_MKPm0NdAdrnVYBV+SdL@ z07K~f9kkCvw5MlK(AVVS$`96Aord$ruGm9L7qmPsqj@c91kkcYNXYmn>KPp={H-B4& zig28d=$Tu3>(1BR)^W9aYZ^b+6sOH4PX(V3%+=;tWo`t1$y4|G-U=4xrp44kJ%j8YGCoTV5U-Z4A`})&fzs7Bv(+*ICRtxi3EJZx$0 zqL?RRLzAwX?~ipJw-wvQ8&kX7Yu~rB{7hKGtL--O zl_C#lNhmOd+~}5<`omz?)i1+RY_Dtk7d1ufc3D5p(5JqG25GsxzMjjg@e^=8ozDJo z(0~c{!^J7~%!aBTn#_kI!UK0tO0ye++%-uVP4&jGqJxjDA6ngM`hn^`A~363jxjvU zhsA9cR9`+x5?ET!)JYsc$iIec$s<+9LfK0RQ-d=8Z5$WvmgoAd*;O^?_GHU(V_15i zt!}r=VXs^Et2M>ac;o7R0zN`lP3xLw?`xb~_I1s`o$x+#!p)x)Y z8ZBovcjT2W7+LUsWSu{?{JVzZx=VxIC#DrTN{)x?+V6faHuQ>nnf>Dt0XfH)COSUd z)HSLs*BCH;(-rp`?MNDx80ciUUvxb_Dl-#P6{2H_u<2hUzib?kcT*blqn5Fw0&L^- z&DIg&P4#Zq+?t{TA7~co?57RIsj2rQ1i+}iP8j%Nn*DFH(*}Jq(>gc&w=4BAQM0Qh z(hgRx5Nm^LoeQw#ZqZ$S{g`1|r(KhZDmgTJ{hqz1uYQA2X!e!oxGm`aS+ZFb3QAK| zD-BI{umBdqR)VkGxQIm)7$;LpB)>Q%Ze*@;a>o*ku#r?$?DWAX<>`aSl;(n-(~G`v zpER!v!)=z8A~qAnxv zY7U`o&@<1p*nTM}<+r=fCpq&C#m$rIYqU{X-KDuL)W50E+P8#>unW4$EO z(zfWwHaBW-e4AD4u-46YZAr5q1r)jF3NLo_2)+_OCc}WZY-yfT{M*Xg)Wjo3Tr~dej#qKibTTZSsIxnm%c7KyUE&MHe!q&0P8zRC|u4lCTHs>Qp zS@2fpx8~yNG;?k0C2Pi~?wYf!^zTlv?~6$N`J273l@gA6Kbsq~&lHm1 zGq2{xw!y{Cop-1H^3o4xyR-P(kFJmBCAex0#mTzMujnrwayM!+?wu0f9HDXl(mty$ zqkddMVSsV6&e}O+Y@-wZrDp37<;IpHhQ|1?+7Y_O9x=KLx6VQd(QO*39-5{lgu7EY81Nd#5PMjl|&RxR7aJPmnZD7V<~ zQ#2zG90oO`(w25xvogUnL!5!3H9T+{Ja6mjWHQ4F0x2mmqoCp|FfcR!`+1)CdCxP9 zt?jqpcU|9gvB?bY`#jHm-@p5J|L*%qPY?akr7q04`a<3!bIpU6<<4%M`=^A6>1rNLuFG@R>RU6~KWla6n7Uh< zR2A1X(XNfT^T#m_an323Y-&KDivlm;Y>ot^LKNL-+C%;b<{z}z3}avlb9UbiBb{mM z1Z#nE&Toa5SrTYC(GcBXJdqHqt!}7rsLiV^UuzuS>07l|+jP!^Y1K;ttZmO&{On2N zJ1=$x3~#KOnCpIDZ+dg`rm)W6LZ@cJqsi(@M>o_QlLL#Qbxd~dny>rBCQKg-BV}wW zDogszZ^?@8Qx*`vVA~JNQyD{;&Wp>1w7-@YvOy-Cn$8CaBYsrglV zuLTd?xim3wR!X2DTjRWB?}}&{yfxZ!1>xG;Djh$KUL6I4dP0U2aXf;IF!7qi))OcnR&D#>v$p4BzOgs2P#gW@Ka4JQ^gU~?_(Nq{ zd)tZFHt+uc=#OW zzu*n`gk@Zf%t>=+FK{-!KhxB`P~D)bXb;)}qoVTtV^!DAwl8?d0xrjWsrOW8gv!<7 zXpAd6v-88LwU56*=eSyoaq1S316* zQsH)(8rr`xM=zMW*>OHQs=~GR)M~g8V(k#KbGtsQvnsRT%6dQh$Nf)4Uk)j(AHP5q zQBctla^%5_U4$09%Ll#FY}`R?{E-PKc( znYMMRPI#pyBfhahF6ijYUUK>8HM%C7y4RdRH5C*0_R^ouYXYyB;;boiZ#OMx_P4HR zceHnn2_0)sHfF{WTQ2 z6!u-%6|r*m<%GhFpMMzDIBLY>hDTmqyvkmp30<_a-F-@LUi_nN?T3dydBwKO92eF- zFCw$>1*oLm6JJ*|#dYb$vZi*y4_*;4xMPF5Ys8fw=wJmcyPEWK4 zx_`E9j9Hp&TY16q`V+}Zqhbp)$5pSFL0NN?2IlA@g#P1pXM zo3`@OdUZ@!zCI*3xASD~gVXA2@=pAGE$Hjrsm!;Uy^encn=ijLH>Ny&abT?5oZDqL zN825rB|JTP(|e;=!PNgDsmU!D?zBJe|2Ob|e))@+7c(by-z7lR*nQ5{JkhqzeQwN_ zw8t`;li7`qM%Ryhmcqf;bgAN;8)0%?PQWLBro674%!PgFfy_nj1N6;ka-alM07cUP z%qSV!i)#N01oJLPD-P#&*2X;3)SdWaoAbcTdmTTgS}vdWvl^Y(5B4^=JCC;eTiYIG zKJc?U-4|_x_Ov%#YK{z<_SZD`8!r`4(B~bm?x%W-IW_Oo!Oo^IUBkNE^s8Nk%WMbT zEh|1vt1*>2z6{UY-juUBfw{O|U!38vWt~`h?QCxSma2UKV&@IY-M1Jx*4kTQ!#X3) zbteB=348bX&yGm!7wdklY+%P4b#7{VLqw1vyE{Abd)w>o#*T5Wqk6bJcHZ>5@r|}g zVC5?=SQ=KgC*;o8-41s$*^b-Q?od_xyP39EbN9jao%r#c^OlS6%xW7y%uuuh<^i>b z&#k+tS)lzx+x!KZy8pS?7Txu;`?Eieo`=3_L+-d-En~`wZcU$d3)8T`d7$@>5&qWE z6YKiMMzpM-p4<8JJv&^rdh@Mcl)#4f-YW>fp8c?l*n{C_F9ds1yv_RvR9S363p1<& zTK2?rCZO_%S1v~SFA6P+){I*Da%S?x9i@TRd%kYzi)e>u{fF1<_T-Hm?Pu@Ob#Lq) zyLhlMI47rc)r{IB3DGG7t$v1MP4k=$p{ACU00S~R_iJZ1CA=J!9<{VQp@UhSYzcQ; zKkgb^rtM{%wpjDPN~GYbfMY(?&$`a4&K{DrOZQ`##@Ub*Z`qCF*)FE4f92RlutorB zX=hUJyAtTWeBnT%!Fa+L9A~>HuDtfcTxUqcA-z3kM6v6fB}?`2S0&#Ma=T`Jl;&2s zdmXeOa_8cFy=fVf|Gql>Q%G^PGL}Y2>CNUvg(0qTeMCh%h&vvdA7vl>RmHWG!syE_ z3H8qyT3z+}-0mORmL@OhQ*@*$ss4d@_kpvIoqz79QRdqyFaie_Z8JUY~~Sx z){t||{vjhr7rntS`v--7pV4yX+vQzjz`ZVLEp>!ttXOSni;DVD+b_0p^fQ~&ljrT( zoW!)6D-R#6Z4HPub+nio(yY}v=FDX1?KM}{)HfWcI@QZEG`_quyU=v}@)Otp2`9Z{ z;DJHc-g>RLmtj@>*!INQ+bRbeL*l}|&*)PGVX&}X=e*eG%jkuW#5Y2WT50PuE8l;% z>FUQ#2kT9(wuPFsx1EE(&IusjbJv0Bj`qTeYb~bt@u0e#h0jwNLRZ zdpyMe3%Pdp%IBJlXm~51#r8xJZ>dbK3#E>mcU6pK#>Yn;+>tO$Z@T8FhPI z9rdqmnd@&DHMwq3JhaL5i+7%|Y*bx2o3~|V$g)9G%LiAE)%H7kp}zEaD_0mAynLtP6EafF%a zo9z0d;C3Lr8I)<+4TtB@A^o^O>+w;AIrr7OoNAS8SmZabLuF}uHYA0XonCnB&P?Xw zreM4e-CMyl{R|1g#i~F<-pJ&H%Cu7;PXa~X?>EsEezeZ~ z;@+_-eTv|DQ;+Di05Fg;)N{AGSQmTx&<>(nz3%SZmRr$Bu_#OnyikJ288na}ilzeB zsr~qcKF>aEb9Qk^Q4TN{$(1z+s-TI6nFcFz2lJiYwBp^*+uesO?y?Cx2R-~z+L^)e zQInAuT>dn(xVNEUd_msZ{?^8%foFna2i^XL&f(Y+7qRk|OlJIa$o)4p%zt#2g(WLt=HN)QNj}>={rA&yf^O0HQkA7LT&)wX;!Z3;r;Mi`?nXV;gfv|ZF!-&V9cH5quCR$F*QN6YB) zu^qqNUzM2RZ@4tBNmmdHv+y*^vle4%^@jABgpgKu-y zxSMUJ@%u6Ychnd7$(vLF+VacF4|74cWW4?Ej?!CfFi zy7ac9oQYKt?p^xU^sDLp;(?;|AGUcFv%2t=#6j^<`*$qsJ!i}z=OfRZHR$sa7M6Eh zetY4Dn!woJJBO49Ri^E(Z~(t@{cAATQ@JMp*qEh>{zait&66@mn=4D+dJhJ=?Hg6( zr_O`BoIrZ-t{s%C)!!eotO0VVO<8-B;)e!UNB3P209|4Kv+kvf0r7Q*@-uo{m&4Ky zsVwoIl{W%nA!FH<4u|&nMPT5)VK`4>fK0c6bT5>CLbY`1AV&edS%R2WP60)J3eEV9H?1cwcCB|t@GDl<*pqwH>jSiiuhA? z=Ny0gFw?I7MHx*f%lNLfJ1M$*RM5`al6UjR1z4MM493v*Q)<<TF*1ZFrHWIZ`GwR{bCC1 zD)a5pE0_pZ&ERLs)n7u9w zZXV1V=(i}eCgbesqjw~ai+9@0Uz}~Oi@vtJx_U%`G1OggEHl}0;hFtaqbk#`jLHMn zuqAN#s;b5#FURJ^6&5~9kU6*+K0?%rjsk4^g!P*pNanJ=$>Fn+V2P37+B z${AmJ{l$0Oe;D6(*Viq##zs!ConG`#>vf=dmc`U%>hq$~U%MQfaW3zuE?4u|cr}=c zsr;7McM)qpBz0|9f5YzbyNga3TOfxIDSxFh?YiT~{890@Z7JzhG!5F`4rh+mJhXC8 zc6Zf>>3Qqb+39T#rsAkdKfnsl0!Y+yG&;FIB=+lPnd2T>xqb$KRd-XweC@?GF*#i= zt)r#`dEQy;*Ds#=Ho{q_4?AqnG`H(294Ak;?rjO0Wp3HL%=UwQ)vg3{RNO1S*>S>Y zYuM-L$Xn+8Vc6W!8h9$*T*S zCfHs!&r!EVr#Q#d0yOe1)i;djNY^;VnR8Y| z78nbMK*m%(JKN|!cKkv6Om&$1$B&yTn(ACNu&J6o&oC&pPf|_#`cipHt$%E!NqhM% z-MOk==PkkRnwooJ)1|^`Mb!Z*akwtZ8!Vwh-f$%oZh4?Z+8+jPOO)=nT=QQF$%NKn zGpZ(Sg}mO~u`kcOx$=$l+^3m|m1%o4CHsofDnP-Izx2JXb=9+z;+$)ar(Fc5<*~BK zJ95j%#lQGoN!6}r>(o~ltv0!isWKM6!mLC@|G5WRCg$fo3X@|sXD?0OIf${Af(hbIbohq|xUwqLSa6~53+1f~bL;LsS21BHJk_m6 ztDpVIs6im2jrlUMY--1R^E1t}{0;A`CmRN}PlW3TZix-w0*x;d^(O~{J+4hHUYkFp zdBcHl%sV_=H^u`+Y(}#C;jZp%Q(?fdlZ zv)z|gZ5naT@^Mb+1=pFgmr~%K^?NHzT8`XlKhwDUn`uo8>Ux3ASr(R+teQCp{-FDc z?P$~1#B1G-#8ZRZ`z>8dy8k(9`p#qenE{5Q&I91!E??Z05WaV8#NXZlmeYKMh5?yjOuffusDw>ioa#Pb}{7kW13$7$yE8Ya- z#n|`Y zDyJdSx8pw1H~P&OePGsmj}9pDpONyMgcJ~U6XP->KUqsrk5&GMN|XvIyKvtW{CJqz zD}mO-yNm;|%xQZ=$(7@4%HN+A@jqn~Q{02*X@@=Nobgd3#)*#_ z64vZUn*jXqHP-9ws<^(ARPUqKGaK*>9B8H!s_29pRa_G$R<5y?R_=mOQhFBZM**ph z3qZ;@S~CNf#FMSRKr@0~M0jz<8xLCg!Shz5TLgU_L?05z2{}rQg7KEol9DAw`um>GbL~hf<*)LJ+V$)K3 ze3AkS9ufZtXN@;3bV(?OdV!3Cc7XcjnTQQiW_SL*_Vds09zA%SKB4@dNueu$@Z1Z{ z^=^?y5HI}!5&<+fFAXQV@V4J40(Mm3O}Oou$R=^*5-R!lZeHfZQVTGfEWnzj8*B!# zsuL{~#7G$11^yF|KkY%?;=@IYp#AkpyUw}2i9yccgT;XJIJ1dl}7x&8l zL3w=l4G&HOyA$7?M$Zix3swip=v~>`gE7MuUU2c@I^c@}M0f7e?IB8>4EsrKfOSus zWk1py!3KD5Oux}fsL>xIIxv`$i!yxiU;l-bW+fw}&4=<~>0Emwb~GYKiG5~qjeJx$ z_n7*sJNdQ@2sO%)5hpje({ePL!V6uhJx<|;k3jqa04E>(gzIayNziqhkKQFzb!OKGH_6JZB( z&mvsKhvsnc^I*OLs-c%awDnJsXp3rvz4mR$TSNhjzI|?Xz;JxS>5@g<$aI;yT|ik< zsDyfEa9n^4A4fg}ap}$Rz|IR+bjAW3?Sv-QAO~LP+x{DGk|ut@-?F$o!Ji#*Es!0F z@?mL{S#iz(-p$bgJSR%#-;zAX%j!!xH6L*e>Pfn}qtZ_}Pm7Wy0>;zc0|jO4^St7m zxO3uidH*UW6XX`hm)}4-3t>2ev88z zD;7jUagV{R^~&4w-#P(}g(bkI3g|Uf8_pka-dB_BfTb65ajl>tM+R&A<*Z-p!f2sJ zv*3Dx+i`C^nb3v*swjIBj8o-?NAzrN038K`6@ht#uw07G0_Jf@%%UnWNz;DeJWEj1 zfkiZFAtZqaM|GV_D#KUYiYS5^+moEtx~^M!5u~`8k^_e9cM+BWPU?-0+<|xv#{SFZ zbj!vJU`7HI9ij`g*=wWGtIxe;cK-|u|J1LRh61#NZrhu9Uo>{uK`ch|eHbpGY`nD& zaV7y(z!207N5Z3*!Xkg1rIL8~7jWqKqdxfPq^OrQw{f_S7gKrZOlPG!1WNKQfcLp* z-GA-Qbc{gWI-)#fsg8gWjn0OZ$N5>8WG9uQu!;E@rV`uW|EKx33ql#OPyMy! z8?A6}T=^zq3xooZVn1o(L?(Q&S<@numKzTVVHX$z6&f(?Y*D~BWX3svXsD9ibDoJo zPk-avfm{KbfIe*2QjY`HOaLCf1ob!?<_gWMbe^rJk6g;A#@AsaMcEgb6L1;13f)%Z zJ=g3y0++ZZ#z+YSnM7tz*M;QVz1$VHY{zH8afi)s_!i*)1n!^mPuO(4>dCqAFQGfY zbN>4GMB^=?OTYLENG7=Q0{tgtlmSltKRfo!s~!psOXo;*(P%1sbI~~U>g#J7qjWT_75pIwHWXS|{*{7%PMDvBlt<*9zE|U~U?jx1opF=<5+quP_dj95P zth^r_lc#e}FuLzly7oM}fT6DI#9R%kGQ{;ogA}B&PL%Cv+~wTORos+i!D}I*NOFr= z>5icE6(H%J;BS?2RwOXdattpuY4V#Rnl>PDLa+|_0HYMA(?0AV8Ya0I+WhROfi{ud z17FinU3V98v!w6=P!ipC0OLptUKdyb^+p&5SSlEe=0=7L2}BogZ32CK9#WwC4@Xf^ zqkt;{K&db#5*iG>mN-1~xS}io^>X2)jlUh5EI$Ezb816x^*Xa3S6)Te;3*gaN6`W! zT%E3EEcSQUi_%_(#*HWDm%qwapQyKkU?ez3=tpV90%i;$JOMrdii})H!21&IqE|q7 zg1h%6Ux%sJbsBb%D2);DXMrUU6Kq^kMMv_qe!*y?7d^DdPU;QIAs{uV&5Q!>!$;_icz z1QEdqT+e}>TJ8Tz@+qlROQ}*pts36aeG;xVw^a=b9zlF6+;ZL);$dL1*@&=DC} z)WYA`!ch!x`b2S6sGwP>WsZ>Xlw7$Ho)J6|X1nnfZv>7j)06b8h!qK^SJ3z<^|2^I zgRUeP%3!SOln$I$#yYH}@?M$K5TWyp(-*Hf%jVtCDe!q9osYvd(t@D~OR_zaYH(O7 z-b2GZ;0=!6w5ey5-r7=9Fo-2qg{zHgDy#>Qb^i$x3F+Blr&pm@=f|X>=iv+UJV9X+_?Ks&{Uh(w2bvQ zh)E*)8K)tX3IVzFip}L`Pce@AJrt*r)Z8ltOI-z#b;_>fLLdeM@&mwwkjc(HE2zP8 z@VF3ssU9iR)0a~Uq`%h1T$0Kr3wYlLQJFu*p@_X>Fc|LU1!3G167+5Ch3M{y#7ct>RR*+TGxGB*leQI()1s z&%Ly+8U`FvZGb#wglfKEm^lMEKAC<+1AG$Kg{2UgM3n& z(O8kTd8uu8&QbkL6quvl)vIqsL*ck-uAZU+mtb?Jq-Qxe;?fH??yV^4ZOC;NtHNog zqI`c)yy1pl`?5cH3{!DZlY<;EUW(h({>UrwiWl+XrS&SfCinyTRa4|wgNn_Xa6A2~ zA(6zyH=g?}yM195>so-2M72VEMe%ZN@nL

s?Hwu?AVQPVgo43U$1e|ApCqC45Z3tX00+@r4F$8B*6$OkNis1PbI8iib6dW$X zuL$)Tf{cfrbGi~FFMW6Zw6muGr$wKm02$5>IwvbkD-S)4zGYnqLE7FT2Z^1m@D!B9 z!>|SbjcfDaN-ZL|1AP!@O*qPUgD4zd(q$b6Nlr;HS4*DL^$S(YD5Hl)%sor47dNH9 z3)CJ)L9Okaie^Yr*l4bNe7?=)7t$WViy~-2`#UiP9UU zR217hkH;0BcQ49iTuQ1;ti?qF!TQ0E3KlE8h0cqV2m^Otvr9?4s9bO-HcMQnl$O0# z)PW6S6$BC1K`*9%Y(JSB#^2;yiGuUTe;tZQX=9;rAaw37DJ#J~`D>+|{QX(JrkuD# z)kjARj}Eg;q^D2_4}`-4$e|rrssG*)KQB! zp9cY4`w(~2FGr|63o}}_^H-(FT@ZahX@Vuv(m~MqQkXP)zB!9~YmY>_%H`xBoH)^k zKOsu=?0!FlA*f#+J+YM`pd`kYKwEZq#Pt6ME_wFb->^e5rOwrKwB{P zQjF<}aQvkpUIXvGJ%F80ghNw+9OU^*#>h+>CpgK2K-co~8#JN!e6@>kB_-|WD6k?I z3CBoWKtMO)((!gMmKu$Sbhw?>?&e40No3@YAj;X5{G0C}hj=FF6p2Ed9l9jda{??0 zU+CS~-fD7v6BLPL{ju~}Bau!`s0H<%myzzCV&#~UQc&3NLC?b$z~JF}D8!W0IW2qQ z)-aYA@K`1UDPVBYfBA*YqL7PMUtD+<1OWEO;+iI1+vs4JQNd2y+^^WU9yXo)$2T-+^q_kRs=bIf~93NxR`VaYR!e#FrTRkY^;$R<)g02d zrYaKBQ1bQ$DmlqA$5a8I<)w^SntTISYmx6DZ#Y@M6PY9O|H z(=21BOy;DJTc$mFC63Err?X10N}~~-gu#VcM4FCUq-E~)DbdtA+rjzfh(m!O7U8qM zPgUyirb9$m8@RB{)fh$Ljshsov3rRg*sw{#0Q$9t1)=ZEH8_l|V=z&zoL6Fpz+E~o&QWPd5xOFQZ-9Hv#=z{!eo)_sA*er325{;MJ>a~lS98>R~ z-GA=!2j%w|`j~jq5J824zd*4plq8a;6U-c7;T0|gZsz(ZR*4{ok8@E-5~SHHGB$BO z!7mPkK%ZM@Y4wZm%!4|XPq+GZSp#D=oBQINy%DL zj%lKZhy?;TB=SJl6e7zwBDajDssHi zN4yE-8OcN|j;_MHz#X~#5y`*dO+aYLz$x+Vu$(}L!(#jvp)9*0?JX#P!X$tZ;W$$# zC37!HzWfV&kSvoR4s%fI&|RPw1Vt$OBn5b0?M2gdiR1k_QpE>pGPhzvn2dx=1NJ9^ zMSD2K0*SasvYz8|Ftvz5O-0Y3rX@v-*)1zSwGZ__0a682MUI(4E*wcJgPPBEU{i$8 zE3YGiur99sed%Sh4QZKPXP|gp_j1b6{0b7~QIO#FD6fR;`TT*~;(WX^Orxa&&QsG= zN*M>jTakd}bsiQSCe5EAZ(fIna#?yQPG?p_L-F#=WLcirL$Xgc9$cXSLIPL%YhKzc za24^1#c&CTU>AfQ2Z*hVM#z%LUJ`@%1gnsE3Uz|RZD=sdc$IPi(-+~6!g;`uP1LX_ zBLYDDo7*YUfYH0(WK4@*d!+3O!GonjVOK@q-z? z)>z*8tL0<&%A>C& zt~+V#v)Zy@YnAl{qw%hp(2>JzeTc(LgUW~VaR%Cu&*FUzcGPsm2l)#W#$*EUQ4ApNjO`2}QC z@gL((7<)FCep=)BTeaGhI%S#d*^Il5q0zI;(mEFGb^O!6-~L2$R?(T-IUPRE9b)=p zqFwe{uy`f#S9|5JKAAYt<+SHAfvI&%Ei*OYYabxuHrQ^UPz zLv#oj3>Nwl4d$23B>%e%n;A zG}cbg^T95ock;GC0^=ncb7q0&iP#r!nWKg?6E%jqWzNk+X{y}*x+9GO5zJb_myZj7>QqlV>AKzEVQKX11zb)aD>NXx9jSl z1Z{E_KCI^DU)02c2g|-SHmSbx>0)ph3y|~SeTONnDbM^Y-4?Q+f)^x5U1Y)|=SEE^ z`aCWTohM>5WC@VX9A2;96Uf;RSslSP@!x}eS5&91T7Ao$kEzXh{C#j>VMF7zDCNXgGqtF;x+j4gt ze_0a+J^{zUf4n3-8RrHYm-HIWeG7eWxyQgZm-c3g79ZUOVE>!3o8z}k@Z4)*o#FLE zz;_S1bxwaA5=J7+L$8UCzVdnJJUV!O6#cL@!mqAI$4q?Qz(t}kju)03Gx+7Ws4w7B zZ*cK~Lp5-X<*h!*W{fa+AUp`u#jNLK*OK0)_YHzft?=89m8QkucLI|%z zIbVZU7*^VdFEu8hbR&4%=Bv zFN4Z)ng(H(P2Ys`92B6%ITO4|R(`>zD?@t?58fHF>C4~#Jf?~D(;l4w5#?x`^J-W}10XlfHv=2$OEYxoa{ z0&$|fR5#wPABXc{#|0FB3FJWetzZl0CApcSaB@2?`ipR~D3Ifj@3_u?2~HRk_L0La zkco~zn*ZS>NW^~Yf9#dSH79XOr5s7UEP~*j2=Ka^+X^n@351w|IbfLx?7Z2{+SxRI z2n06IFolNjy9;vek45-R@X#F7K#wVMPp9pAP1&}mnFmzs)gigzaoQu9YvV_kyBJGM zZWbIxQEDq%Ty%QDPYeB0U&Om090os790n(Ot3`~%nCnw^!=@M%H-(y^Wsit8;18=X z6EYY{roS-Ix~UyrN9qr_#6|e91O}3Kk%u2_Hq1{gjt_ukz}w*X(a{&fY6$r_`l2Vm zKY|f$aeUN(wgmn~h}?KE!0v2sSw;_FOs)Mr&hbkI75VI)5i101gr0-V@X2w+ z89qG~*p=f`SChtN_z0ez?RPt3V9=UC?&R)KG zn_C=ruqN^MOy)uqFfZ%poO&1?K@N%23<{7q(<9V{M@3I<`%dPYmvAn3ps^YDUFzLoKAt2NwQSl4lG3C`VmT8{b`D zaT%rg9sh#3+$VO6Akcxa5h@9y078ctX9!tL(XtBy;nO%#Jc%(LqMreW71SEF?BEgh(G^WHGabrA)w&tY+y9_)+*p=)dCajsXmd&oH97LJEz9 zLsbk3bfr*?-c{lF6o|VE>2xcAZS*Cz6f6$eUwA$oq(vw}Y$4O38hi&t0S&K*AKp9*6(P1no}*&R(yHLi{r}04_)i ziB5EIAP1d-oJ*Pt*ugp9ZE76@kU_@KVJMIO21nIG?SUn#6yt{!b$}Z!Z~K_{yu_DI z1#al@%reJ=3h6icHvk?yg(am_lwceLY>nNKc#ko33Em2*>)^Bjo%vwjm{RnLe8xor z5m0G8=OB6WHQK*qlERgO?*}gmtTskapA!H~Y(&vqfR$(hlA(=+d{RVUYi2^MN8N_- z)bRceQQU|OApQ<50X`p)P)aIe|4e1hDJ1s_+>9`RqV+|T(BM}Af4Ss{{;nc6efb3Y zoV0zJJ|P=8ia09<;D7@na&CfB4F6W*_qPF!8$4Xe;e2d@MI0vsi$cegXm8S`Sk+;z zqX9JMJ@tvdykC zG8L0qGb%a&%Pe^ zZlo3lvx#7C3hQq zAoTXQ)2wQf8CZ0W-$zzk$_tVwkt{sVYqNaTgHYQ3LuFc9XjcAh-l8B^R*rL1QUksk zA1 z0#p@W@5c5~pmKH6<0{GIgr4-&c z$ZtQN!Do`54lnj^Io+qnLC{@4Cx(=axX1>6P!idYeY~K$ue*ee$sSY5grvoZYbUEMoKIWCu;AS6eKYq6P%9n%yr2F!6sak?ei|0!4WI<6?!#&C4S_ z$51{rp;Av0n?4G-WWhpzKmqQY?}j3%E%+yodqBB4L{~_|2d5nZvlo|yY317sMJ|W8 z=z)Ag#7P<_cFAAb+B+}$+p{y?7D%h0P$(|Ac?C8;B$EoTMq#-C|B_e2zT#`*ez4<{ zg{2)1ZS>s;RKUORZXcTvixgB&d&Ky7PDz5PE9JilRZ56!%DJS(K@l63n`RGeiQx4g zv@DNaR4ESW^fhE$6c@%|i6!Yfr1D{?IOcSUoU8n-@n8p**Xf}!Da#dSnO8g&4=kkM ze1AZXKKe$Yz8HWr*k%Bk8O(SNvnj@RBs_Rq%RB*yKf&F&QS}qR=x3NO5TNkR+&M4s zv;`;(DV~u7UsHYOcRcVVQ4T)(kLY=o91h{!FNPt%1iAptW=z%=1MN?W@en@{lETPu zjeQtd1RUAOnUx1Qa)u~exB%eDChF`-4Cp}EvNY8-7T zVCTO>YokPQP0C03ZAwLjk zxVM3eZz6{|nUxdCYmqYH7lG}7t#dQ9=a(XX3W-42)q_w2SaHsha4V`vAgR?g&r&OP zKNf4-A(KFQMr0<>4Par{bEpbM==)eAhvqYdDr>DS3g{D;sllmg2#FP$6J|eniVS!v zU+R+7uOMn`%nj&VA$5K9uDs}YGFg!?t>G zmQGs0m zvw~kG#dpavi@OKt1_`~Oyh;e)j=ejN{z!F(6Grer8kIG-LZp95ki3(%0Xy!I2l!tX z8*jz_2J^_Q2#0s;9FjH||0~qSZstTxPL@P&3|j5S`>JE!R7fsy&gacDd2$`BwW7RU z%iKUR>1fyaP|4cUpiM@?JEScX9z&Ke!6@qHN{tnCtvzEk84Dtp8k3}2|5z`fccEof zul$!-rGmZ2S_dDBn{b2Rgw##JzzkFe$PAar9vhB~aaKF%zzXlr;s5 zc33eH4i3t0j#X5?R+$zdtIXrdl*h0ptIT7cL7k{Pd*qQh>hdH$5H`V3X!U2?BqU6n zEKKZnb#Df-0p-DEJQaYmy7lVE;tAHw>VBY+O<+7I*(3*;%Z4RwlhSyI9zD?x#oW+| z^8UPEz{&h7J*>jyLWn`h{y^wMqeh zq(vs|o}&6P=#Tle)Wx1TFCzEmvwD$YN|x0tI3kXrLYm6*3diAbAF$bcKIx52xSiJ+ zdcx##%nFIDZ|OYX>PdgLjajGQmZ%Vpgk$h6m=5sdm5_WSYyORlJQ+Dy^4~|ED(eH}r_3v0patfuy(;1)U8#bM4`+E0@y?GfGVYfstHqzask5YGYI0S8=x z*oXdOR%pzH3*{p((CU-G$Axv_Ir?{W#aw+-#$5W=vWgGDfhwzdP?6xA{g*ba68rf5 zV(0yAU07-`wDf{Gd4=-6z+YiQ16~&>SyenyTVg^4o!6e`bSRfRye>G$iy;1L(n~vm z{dAr*vuz4Ef0kC;xN!prO+~n_Q@#obw-;K~s`Q2k-OoUE29xdv=;PzEc+b9-oIe)W zp{WNxdRMo-!P{gKp{|(i6vJi%PDZ-q$YO4i#27Vj^w`OPbNEMRcN=wGy6zZ0ll(VW z7sN`4yH*=ajU$V(;ujYwl@);^{v*-e;07UJ4<)REhKzO?YqAl;1?82*nGO*fb1sJP zRU#KdKzR$&BS1D$pREr0)9kBk7Xw~H?|i9u7b@L`Vn);2{S(KS5_5r#r^f*gB2lKW zWtpk4J}{;;oJs+FNyS+I zeu#mu;gSpDTGtWF!NZkk{{uZs9Q(+V9$Yyx9F1J^+M_26zkPWop8zs!hop2OnN=lo zWbC^w`%t|nKr-+JzJ&*WJ`*<7D;{o7b(2_++`%#v%D{}6d5T4hiX#;`r>|O>A7jVGKCxhYj zI{$frG0I$u8I$W0s0YB^lQVI1m>52@!BRkWLbX=Ob`#S}&`RP77`|yGix8By|}ILCPHJxnqoP^)`BK^+vl?Wj$7CL`swH zi@NTsS@%M#^NyV5R<(JiwDXC?9m+m5tV}17j|(^=4B=B!Nj^#t0?|`HH(Q8Gd^aKi zkhy8?tKtmChXHiX>!o4U5Yu+`R@PqXli1NGG%X@74DxCPn*quR;wczI#to`oxm36( z^&}o2E9)^N;f&hnDi{u$MB13R5wQUOVHk)vm5a>JN@|zGxqM&2F31CTOz^2&+KMII zo@OI)-ZczT!j9EkAtn4VHMX{R4KxG+dO}}E>|IO6;m1ZcL`2;9$&Y0F^ ziEI41IY;7+!H}b;q@Jvl9=t0?qQqg* z(MQ;$QW~xEc8CB$a{5kT;|{CZ!DU5E%NfNX$FgM?}Xk!V2bI zWc4o^fk#jP#ldFu(kztL?F26`+^rj|8*|P_Hd;_7sFA5#nf*SMxQN`9qg=3~Ahv;j zl+}^tJIwAPVq3&%{w$Cmr<}Eo!U9lgJg_i5t`UF_-&!jp#WS*<4eskEG(*F)@>mf7 z{9@&?qF6#g5U~YcA6zpLfrFiq1FHNz;=-|v{SjCxirkLoAFInNFn=o)e~V5hrM);w z3-k^8RP$n>4721V6x%+SQs#J7GBCrZF-hEXDmWAb?`7ZS+fZYrUSZ+p?shds5|k_p zpVF)c1jwvJP1G1c7wLvO#VP#2*$)~Lmdds|`QCda;Q^%ZZ#E_1P~P0&ev=czl%_J1 zf_Mr5A3C2*iHj$6R$E5dSbzIMF7V4Ls!AXW;1yQl0G<(43W(1a?hT@8fnL)&Yr}7k zZRiejAEwjw`+CYXd**EL)JOpR|0(S;=bQH!p&N!DkPOqZBcRKPpQP=ZEz!ga;!E`F zT4qJJ{S@F_9-jI`N=FpNPJaBl=p!X@5Q{lfCAf zichFi2JlL*D42c4j!en$7%*_J7Bfi4CR+(w!m$93@L|p|bqFkB&kF5fr?Gsq=Lzy! zxasiV6vfdnq+GzWG%y^sCzgi!n_Zv%zB)wJ6=v#w09q<R!fD59Qw0#x91_8>qL(MMAWZP#Be=BK!zh=A3>d8Ac}B_2N+-Q{OL1Pq wBckJKye#yRi}nSJvR2>F+2Cq*hiw}(=jV?@|MaCF{NMAFrv0vPV*Kj=4}AKs0{{R3 literal 0 HcmV?d00001 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..fdf3d912 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,6 @@ +User-agent: * +Allow: / +Disallow: /dashboard +Disallow: /admin + +Sitemap: https://nexafx.io/sitemap.xml