feat: initialize Lootah Robotics web application with 3D configurator and scroll-driven UI components
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions

This commit is contained in:
Najjar\NajjarV02
2026-04-20 11:36:04 +04:00
parent 6c5f1f7f15
commit 9394b58b0e
18 changed files with 981 additions and 125 deletions
+54 -2
View File
@@ -15,7 +15,7 @@ interface Order {
metadata: Record<string, string>;
}
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 */}
<div style={{ display: 'flex', gap: '0.25rem', marginBottom: '1.25rem', borderBottom: '1px solid rgba(0,0,0,0.06)', paddingBottom: '0.5rem' }}>
{(['pricing', 'personas', 'orders', 'settings'] as Tab[]).map((t) => (
{(['pricing', 'personas', 'orders', 'contacts', 'settings'] as Tab[]).map((t) => (
<button
key={t}
onClick={() => setActiveTab(t)}
@@ -758,6 +781,35 @@ export default function AdminPage() {
</div>
)}
{/* ===== CONTACTS TAB ===== */}
{activeTab === 'contacts' && (
<div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '0.5rem', marginBottom: '0.75rem', alignItems: 'center' }}>
<button onClick={loadContacts} disabled={contactsLoading} style={secondaryBtnStyle}>
{contactsLoading ? 'Loading…' : 'Refresh'}
</button>
</div>
{contactsError && <p style={errorTextStyle}>{contactsError}</p>}
{!contactsLoading && contacts.length === 0 && !contactsError && (
<p style={{ color: '#94a3b8', fontSize: '0.85rem', textAlign: 'center', padding: '2rem' }}>No contact inquiries yet.</p>
)}
{contacts.length > 0 && (
<TableCard>
<TableHeader cols="1.5fr 1fr 1fr 2.5fr 1fr" labels={['Name', 'Email', 'Phone', 'Message', 'Date']} />
{contacts.map((c, i) => (
<div key={c.id} style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 2.5fr 1fr', padding: '1rem 1.25rem', alignItems: 'flex-start', borderBottom: i < contacts.length - 1 ? '1px solid rgba(0,0,0,0.04)' : 'none', gap: '1rem' }}>
<div style={{ fontSize: '0.85rem', fontWeight: 500, color: '#1e293b' }}>{c.name}</div>
<div style={{ fontSize: '0.8rem', color: '#64748b' }}><a href={`mailto:${c.email}`} style={{ color: '#3b82f6', textDecoration: 'none' }}>{c.email}</a></div>
<div style={{ fontSize: '0.8rem', color: '#64748b' }}>{c.phone ? <a href={`tel:${c.phone}`} style={{ color: '#64748b', textDecoration: 'none' }}>{c.phone}</a> : '-'}</div>
<div style={{ fontSize: '0.8rem', color: '#475569', whiteSpace: 'pre-wrap', lineHeight: 1.5 }}>{c.message}</div>
<div style={{ fontSize: '0.75rem', color: '#94a3b8' }}>{new Date(c.createdAt).toLocaleDateString('en-AE')}</div>
</div>
))}
</TableCard>
)}
</div>
)}
{/* SETTINGS TAB */}
{activeTab === 'settings' && (
<div>
+25
View File
@@ -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 });
}
}
+34
View File
@@ -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 }
);
}
}
+9 -32
View File
@@ -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 */}
<Link
href="/"
style={{
position: 'fixed',
top: '1rem',
right: '1rem',
zIndex: 100,
display: 'inline-flex',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 1rem',
borderRadius: '2rem',
background: 'rgba(255, 255, 255, 0.9)',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
border: '1px solid rgba(0, 0, 0, 0.08)',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.06)',
color: '#374151',
fontSize: '0.8rem',
fontWeight: 500,
textDecoration: 'none',
transition: 'all 0.2s ease',
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="19" y1="12" x2="5" y2="12" /><polyline points="12 19 5 12 12 5" />
</svg>
Back
</Link>
<Navbar />
{/* Configurator section takes up full height minus navbar height roughly, or we just let it take its normal height */}
<div style={{ minHeight: '100vh', paddingTop: '80px' }}>
<ConfiguratorSection />
</div>
<ConfiguratorSection />
<FooterAndContact />
</>
);
}
+21
View File
@@ -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,
+12 -1
View File
@@ -28,7 +28,18 @@ export default function RootLayout({
</head>
<body suppressHydrationWarning>
<ErrorBoundary>
<I18nProvider>{children}</I18nProvider>
<I18nProvider>
{children}
{/* WhatsApp Floating Button */}
<a href="https://wa.me/971559482728" target="_blank" rel="noopener noreferrer" aria-label="Contact us on WhatsApp" className="fixed bottom-6 z-50 flex items-center justify-center h-14 w-14 rounded-full bg-gradient-to-r from-[#25D366] to-[#128C7E] text-white shadow-lg shadow-[#25D366]/30 transition-all duration-300 ease-out hover:scale-110 hover:shadow-[#25D366]/50 hover:shadow-xl active:scale-95 group" style={{ insetInlineEnd: '1.5rem' }}>
<span className="absolute inset-0 -z-10 rounded-full bg-gradient-to-r from-[#25D366] to-[#128C7E] opacity-0 blur-xl transition-opacity duration-300 group-hover:opacity-100"></span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="tabler-icon tabler-icon-brand-whatsapp relative z-10 h-8 w-8">
<path d="M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9"></path>
<path d="M9 10a.5 .5 0 0 0 1 0v-1a.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a.5 .5 0 0 0 0 -1h-1a.5 .5 0 0 0 0 1"></path>
</svg>
<span aria-hidden="true" className="absolute inset-0 rounded-full border border-[#25D366] bg-[#25D366]/50 animate-[ping_2.2s_cubic-bezier(0,0,0.2,1)_infinite]"></span>
</a>
</I18nProvider>
</ErrorBoundary>
</body>
</html>
+8
View File
@@ -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<HTMLDivElement>(null);
return (
<>
<Navbar />
{/* Fixed 3D scene behind everything */}
<ClientOnly>
<ScrollScene scrollContainerRef={scrollContainerRef} />
@@ -28,6 +32,10 @@ export default function HomePage() {
<div className="snap-section" />
<div className="snap-section" />
</div>
<div className="snap-section" style={{ scrollSnapAlign: 'start' }}>
<FooterAndContact />
</div>
</>
);
}
+51
View File
@@ -0,0 +1,51 @@
import React from 'react';
import { Navbar } from '@/components/Navbar';
import { FooterAndContact } from '@/components/FooterAndContact';
export default function PrivacyPolicyPage() {
return (
<>
<Navbar />
<div style={{ background: '#050508', minHeight: '100vh', color: '#ffffff', fontFamily: 'Inter, sans-serif' }}>
<main style={{ maxWidth: '800px', margin: '0 auto', padding: '12rem 1.5rem 6rem', lineHeight: 1.8 }}>
<h1 style={{ fontSize: '3rem', fontWeight: 200, marginBottom: '1rem', letterSpacing: '-0.03em' }}>Privacy <span style={{ color: 'var(--color-gold)', fontWeight: 500 }}>Policy</span></h1>
<p style={{ color: '#94a3b8', fontSize: '1rem', marginBottom: '4rem' }}>Effective Date: {new Date().toLocaleDateString('en-AE')}</p>
<section style={{ marginBottom: '3rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>1. Information We Collect</h2>
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
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.
</p>
</section>
<section style={{ marginBottom: '3rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>2. How We Use Your Information</h2>
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
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.
</p>
</section>
<section style={{ marginBottom: '3rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>3. Data Security</h2>
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
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.
</p>
</section>
<section style={{ marginBottom: '3rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>4. Contact Us</h2>
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
If you have questions or concerns about this Privacy Policy, please reach out to us at:
<br/><br/>
<strong>YS Lootah Robotics</strong><br/>
Dubai Design District (D3)<br/>
Dubai, United Arab Emirates<br/>
Email: info@yslootahtech.com
</p>
</section>
</main>
</div>
<FooterAndContact />
</>
);
}
+32
View File
@@ -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,
},
];
}
+47
View File
@@ -0,0 +1,47 @@
import React from 'react';
import { Navbar } from '@/components/Navbar';
import { FooterAndContact } from '@/components/FooterAndContact';
export default function TermsOfServicePage() {
return (
<>
<Navbar />
<div style={{ background: '#050508', minHeight: '100vh', color: '#ffffff', fontFamily: 'Inter, sans-serif' }}>
<main style={{ maxWidth: '800px', margin: '0 auto', padding: '12rem 1.5rem 6rem', lineHeight: 1.8 }}>
<h1 style={{ fontSize: '3rem', fontWeight: 200, marginBottom: '1rem', letterSpacing: '-0.03em' }}>Terms of <span style={{ color: 'var(--color-gold)', fontWeight: 500 }}>Service</span></h1>
<p style={{ color: '#94a3b8', fontSize: '1rem', marginBottom: '4rem' }}>Effective Date: {new Date().toLocaleDateString('en-AE')}</p>
<section style={{ marginBottom: '3rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>1. Acceptance of Terms</h2>
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
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.
</p>
</section>
<section style={{ marginBottom: '3rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>2. Use of the Site & Configurator</h2>
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
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.
</p>
</section>
<section style={{ marginBottom: '3rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>3. Intellectual Property Rights</h2>
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
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.
</p>
</section>
<section style={{ marginBottom: '3rem' }}>
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>4. Disclaimer of Warranties</h2>
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
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.
</p>
</section>
</main>
</div>
<FooterAndContact />
</>
);
}