diff --git a/package-lock.json b/package-lock.json index e562cdb..9d2ab87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "jose": "^6.2.2", "next": "16.2.2", "react": "19.0.0", + "react-country-phone-input": "^1.0.2", "react-dom": "19.0.0", "react-i18next": "^17.0.2", "stripe": "^22.0.1", @@ -3935,6 +3936,12 @@ "consola": "^3.2.3" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -5830,6 +5837,30 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "license": "MIT" + }, + "node_modules/lodash.reduce": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz", + "integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw==", + "license": "MIT" + }, + "node_modules/lodash.startswith": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz", + "integrity": "sha512-XClYR1h4/fJ7H+mmCKppbiBmljN/nGs73iq2SjCT9SF4CBPoUHzLvWmH1GtZMhMBZSiRkHXfeA2RY1eIlJ75ww==", + "license": "MIT" + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -6851,6 +6882,24 @@ "node": ">=0.10.0" } }, + "node_modules/react-country-phone-input": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/react-country-phone-input/-/react-country-phone-input-1.0.2.tgz", + "integrity": "sha512-VkCkO0O2W1fvR6xMQv2K/2rayuyr6WWGnoRX2RUFZdYWTDqh67gkEGWy1O2V1n6nJVWaqfUUPWR9o9RrQFlgaw==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.6", + "lodash.debounce": "^4.0.8", + "lodash.memoize": "^4.1.2", + "lodash.reduce": "^4.6.0", + "lodash.startswith": "^4.2.1", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0", + "react-dom": "^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0" + } + }, "node_modules/react-dom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", diff --git a/package.json b/package.json index aacdd91..51c2e01 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "jose": "^6.2.2", "next": "16.2.2", "react": "19.0.0", + "react-country-phone-input": "^1.0.2", "react-dom": "19.0.0", "react-i18next": "^17.0.2", "stripe": "^22.0.1", diff --git a/prisma/lootah.db b/prisma/lootah.db index 863eaa4..9ca8d19 100644 Binary files a/prisma/lootah.db and b/prisma/lootah.db differ diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1511b68..f7c10ac 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -56,3 +56,12 @@ model PricingItem { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model ContactRequest { + id String @id @default(cuid()) + name String + email String + phone String? + message String + createdAt DateTime @default(now()) +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index dd60b76..52fb41d 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -15,7 +15,7 @@ interface Order { metadata: Record; } -type Tab = 'pricing' | 'personas' | 'orders' | 'settings'; +type Tab = 'pricing' | 'personas' | 'orders' | 'contacts' | 'settings'; export default function AdminPage() { const router = useRouter(); @@ -369,6 +369,29 @@ export default function AdminPage() { if (activeTab === 'orders') loadOrders(); }, [activeTab, loadOrders]); + // --------------- CONTACTS --------------- + const [contacts, setContacts] = useState<{ id: string; name: string; email: string; phone: string | null; message: string; createdAt: string }[]>([]); + const [contactsLoading, setContactsLoading] = useState(false); + const [contactsError, setContactsError] = useState(''); + + const loadContacts = useCallback(async () => { + setContactsLoading(true); + try { + const res = await fetch('/api/admin/contacts/'); + if (!res.ok) throw new Error('Failed to load contacts'); + const data = await res.json(); + setContacts(data.contacts || []); + } catch { + setContactsError('Failed to load contacts'); + } finally { + setContactsLoading(false); + } + }, []); + + useEffect(() => { + if (activeTab === 'contacts') loadContacts(); + }, [activeTab, loadContacts]); + // --------------- SETTINGS --------------- const [settings, setSettings] = useState<{ key: string; value: string }[]>([]); const [settingsLoading, setSettingsLoading] = useState(false); @@ -495,7 +518,7 @@ export default function AdminPage() { {/* TABS */}
- {(['pricing', 'personas', 'orders', 'settings'] as Tab[]).map((t) => ( + {(['pricing', 'personas', 'orders', 'contacts', 'settings'] as Tab[]).map((t) => (
)} + {/* ===== CONTACTS TAB ===== */} + {activeTab === 'contacts' && ( +
+
+ +
+ {contactsError &&

{contactsError}

} + {!contactsLoading && contacts.length === 0 && !contactsError && ( +

No contact inquiries yet.

+ )} + {contacts.length > 0 && ( + + + {contacts.map((c, i) => ( +
+
{c.name}
+ +
{c.phone ? {c.phone} : '-'}
+
{c.message}
+
{new Date(c.createdAt).toLocaleDateString('en-AE')}
+
+ ))} +
+ )} +
+ )} + {/* SETTINGS TAB */} {activeTab === 'settings' && (
diff --git a/src/app/api/admin/contacts/route.ts b/src/app/api/admin/contacts/route.ts new file mode 100644 index 0000000..4118e79 --- /dev/null +++ b/src/app/api/admin/contacts/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import { cookies } from 'next/headers'; +import * as jose from 'jose'; + +export async function GET(request: Request) { + try { + const token = cookies().get('admin_token')?.value; + if (!token) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const secret = new TextEncoder().encode(process.env.JWT_SECRET || 'fallback-secret'); + await jose.jwtVerify(token, secret); + + const contacts = await prisma.contactRequest.findMany({ + orderBy: { createdAt: 'desc' }, + }); + + return NextResponse.json({ contacts }); + } catch (error) { + console.error('Failed to load contacts:', error); + return NextResponse.json({ error: 'Failed to load contacts' }, { status: 500 }); + } +} diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts new file mode 100644 index 0000000..dcd31c9 --- /dev/null +++ b/src/app/api/contact/route.ts @@ -0,0 +1,34 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; // Check if this is the correct export path + +export async function POST(request: Request) { + try { + const data = await request.json(); + + // Validate required fields + if (!data.name || !data.email || !data.message) { + return NextResponse.json( + { error: 'Name, Email, and Message are required' }, + { status: 400 } + ); + } + + // Save to database + const contact = await prisma.contactRequest.create({ + data: { + name: data.name, + email: data.email, + phone: data.phone || null, + message: data.message, + }, + }); + + return NextResponse.json({ success: true, contact }); + } catch (error) { + console.error('Failed to submit contact form:', error); + return NextResponse.json( + { error: 'Failed to submit contact form' }, + { status: 500 } + ); + } +} diff --git a/src/app/configure/page.tsx b/src/app/configure/page.tsx index 709e446..9d3e37f 100644 --- a/src/app/configure/page.tsx +++ b/src/app/configure/page.tsx @@ -1,43 +1,20 @@ 'use client'; -import Link from 'next/link'; import { ConfiguratorSection } from '@/components/ConfiguratorSection'; +import { Navbar } from '@/components/Navbar'; +import { FooterAndContact } from '@/components/FooterAndContact'; export default function ConfigurePage() { return ( <> - {/* Back button */} - - - - - Back - + + + {/* Configurator section takes up full height minus navbar height roughly, or we just let it take its normal height */} +
+ +
- + ); } diff --git a/src/app/globals.css b/src/app/globals.css index 0811fb8..cb435dc 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -480,6 +480,27 @@ html { } } +/* Hide overlay CTA on desktop since it takes over the screen */ +@media (min-width: 769px) { + .overlay-cta-section { + display: none !important; + } +} + +/* Specific class to ensure configurator shows only on desktop */ +.desktop-configurator { + display: block !important; + position: relative; + z-index: 10; + background-color: #ffffff; +} + +@media (max-width: 768px) { + .desktop-configurator { + display: none !important; + } +} + /* Small phones */ @media (max-width: 480px) { .overlay-section-left, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index adf562e..c2b5767 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -28,7 +28,18 @@ export default function RootLayout({ - {children} + + {children} + {/* WhatsApp Floating Button */} + + + + + + + + + diff --git a/src/app/page.tsx b/src/app/page.tsx index f00e3e2..fda02ff 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,12 +4,16 @@ import { useRef } from "react"; import { ClientOnly } from "@/components/ClientOnly"; import { ScrollScene } from "@/components/ScrollScene"; import { ScrollOverlays } from "@/components/ScrollOverlays"; +import { FooterAndContact } from "@/components/FooterAndContact"; +import { Navbar } from "@/components/Navbar"; export default function HomePage() { const scrollContainerRef = useRef(null); return ( <> + + {/* Fixed 3D scene behind everything */} @@ -28,6 +32,10 @@ export default function HomePage() {
+ +
+ +
); } diff --git a/src/app/privacy-policy/page.tsx b/src/app/privacy-policy/page.tsx new file mode 100644 index 0000000..e00aae1 --- /dev/null +++ b/src/app/privacy-policy/page.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Navbar } from '@/components/Navbar'; +import { FooterAndContact } from '@/components/FooterAndContact'; + +export default function PrivacyPolicyPage() { + return ( + <> + +
+
+

Privacy Policy

+

Effective Date: {new Date().toLocaleDateString('en-AE')}

+ +
+

1. Information We Collect

+

+ At YS Lootah Robotics, we collect information you provide directly to us when you request information, use the G1 Customizer, or contact us. This includes your name, email address, phone number, and any other information you choose to provide in your message. +

+
+ +
+

2. How We Use Your Information

+

+ We use the information we collect to respond to your inquiries, deliver our robotics enterprise solutions, maintain our dashboard, and communicate with you about your custom humanoid configurations. +

+
+ +
+

3. Data Security

+

+ We implement robust security measures designed to protect your personal information. Your contact data is stored securely in our private databases strictly for administrative and operational purposes. +

+
+ +
+

4. Contact Us

+

+ If you have questions or concerns about this Privacy Policy, please reach out to us at: +

+ YS Lootah Robotics
+ Dubai Design District (D3)
+ Dubai, United Arab Emirates
+ Email: info@yslootahtech.com +

+
+
+
+ + + ); +} diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 0000000..6300561 --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,32 @@ +import { MetadataRoute } from 'next'; + +export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = 'https://lootahrobotics.com'; // Adjust to your actual production domain + + return [ + { + url: `${baseUrl}`, + lastModified: new Date(), + changeFrequency: 'weekly', + priority: 1, + }, + { + url: `${baseUrl}/configure`, + lastModified: new Date(), + changeFrequency: 'monthly', + priority: 0.8, + }, + { + url: `${baseUrl}/privacy-policy`, + lastModified: new Date(), + changeFrequency: 'yearly', + priority: 0.5, + }, + { + url: `${baseUrl}/terms-of-service`, + lastModified: new Date(), + changeFrequency: 'yearly', + priority: 0.5, + }, + ]; +} diff --git a/src/app/terms-of-service/page.tsx b/src/app/terms-of-service/page.tsx new file mode 100644 index 0000000..98ac221 --- /dev/null +++ b/src/app/terms-of-service/page.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Navbar } from '@/components/Navbar'; +import { FooterAndContact } from '@/components/FooterAndContact'; + +export default function TermsOfServicePage() { + return ( + <> + +
+
+

Terms of Service

+

Effective Date: {new Date().toLocaleDateString('en-AE')}

+ +
+

1. Acceptance of Terms

+

+ By accessing and utilizing the YS Lootah Robotics web platform and the G1 Configurator, you accept and agree to be bound by the terms and provisions of this agreement. +

+
+ +
+

2. Use of the Site & Configurator

+

+ The 3D G1 Configurator is provided for informational and demonstrative purposes to showcase the capabilities of YS Lootah technologies. You agree to use this site strictly for lawful purposes resulting in enterprise robotics inquiries and configurations. +

+
+ +
+

3. Intellectual Property Rights

+

+ All original content on this website, including but not limited to text, graphics, 3D models (GLB files), logos, and software, is the exclusive property of YS Lootah Robotics and is protected by United Arab Emirates and international copyright laws. +

+
+ +
+

4. Disclaimer of Warranties

+

+ The materials on our platform are provided "as is". We make no warranties, expressed or implied, and hereby disclaim to the fullest extent permitted by law all warranties regarding the immediate enterprise availability of the rendered concepts displayed in the Configurator. +

+
+ +
+
+ + + ); +} diff --git a/src/components/FooterAndContact.tsx b/src/components/FooterAndContact.tsx new file mode 100644 index 0000000..3b97fbc --- /dev/null +++ b/src/components/FooterAndContact.tsx @@ -0,0 +1,378 @@ +'use client'; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import PhoneInput from 'react-country-phone-input'; +import 'react-country-phone-input/lib/style.css'; + +export function FooterAndContact() { + const [formData, setFormData] = useState({ name: '', email: '', phone: '', message: '' }); + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setStatus('loading'); + try { + const res = await fetch('/api/contact', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(formData) + }); + if (res.ok) { + setStatus('success'); + setFormData({ name: '', email: '', phone: '', message: '' }); + setTimeout(() => setStatus('idle'), 4000); + } else { + setStatus('error'); + setTimeout(() => setStatus('idle'), 4000); + } + } catch { + setStatus('error'); + setTimeout(() => setStatus('idle'), 4000); + } + }; + + return ( +
+ + {/* Premium Desktop CTA section */} +
+ {/* Subtle background glow */} +
+ +

+ Ready to Build Your G1? +

+

+ Customize every detail. From intelligent locomotion to identity and purpose. Your enterprise-grade humanoid is just a few clicks away. +

+ { + e.currentTarget.style.background = 'var(--color-gold)'; + e.currentTarget.style.color = '#ffffff'; + e.currentTarget.style.boxShadow = '0 0 30px rgba(196, 162, 101, 0.4)'; + e.currentTarget.style.transform = 'translateY(-2px)'; + }} + onMouseOut={(e) => { + e.currentTarget.style.background = 'transparent'; + e.currentTarget.style.color = 'var(--color-gold)'; + e.currentTarget.style.boxShadow = 'none'; + e.currentTarget.style.transform = 'translateY(0)'; + }} + > + Configure Your G1 + + + + + +
+ + {/* Contact Section */} +
+
+ + {/* Contact Info */} +
+
+

+ Connect With Us +

+

+ Start your
Robotics Journey +

+

+ Whether you are looking to integrate the G1 into your enterprise workflows or have questions about custom developments, our team in the UAE is here to help. +

+ + +
+ + {/* Contact Form */} +
+
+ {status === 'success' && ( +
+ Thank you! Your message has been sent successfully. +
+ )} + {status === 'error' && ( +
+ An error occurred. Please try again. +
+ )} +
+ + setFormData({...formData, name: e.target.value})} + style={{ width: '100%', padding: '1rem 0', border: 'none', borderBottom: '1px solid rgba(255,255,255,0.1)', fontSize: '1rem', background: 'transparent', color: '#ffffff', outline: 'none', transition: 'border-color 0.3s', boxSizing: 'border-box' }} + onFocus={(e) => e.target.style.borderBottom = '1px solid var(--color-gold)'} + onBlur={(e) => e.target.style.borderBottom = '1px solid rgba(255,255,255,0.1)'} + /> +
+ +
+ + setFormData({...formData, email: e.target.value})} + style={{ width: '100%', padding: '1rem 0', border: 'none', borderBottom: '1px solid rgba(255,255,255,0.1)', fontSize: '1rem', background: 'transparent', color: '#ffffff', outline: 'none', transition: 'border-color 0.3s', boxSizing: 'border-box' }} + onFocus={(e) => e.target.style.borderBottom = '1px solid var(--color-gold)'} + onBlur={(e) => e.target.style.borderBottom = '1px solid rgba(255,255,255,0.1)'} + /> +
+ + {/* Mobile Number with Country Code */} +
+ +
+ setFormData({...formData, phone})} + containerStyle={{ width: '100%' }} + inputStyle={{ + width: '100%', + padding: '1rem 0 1rem 3.5rem', + border: 'none', + borderBottom: '1px solid rgba(255,255,255,0.1)', + fontSize: '1rem', + background: 'transparent', + color: '#ffffff', + outline: 'none', + transition: 'border-color 0.3s', + boxSizing: 'border-box' + }} + buttonStyle={{ + background: 'transparent', + border: 'none', + borderBottom: '1px solid rgba(255,255,255,0.1)', + padding: '0 0.5rem', + }} + dropdownStyle={{ background: '#11111a', color: '#fff', border: '1px solid rgba(255,255,255,0.1)' }} + /> +
+
+ +
+ +