first
CI/CD / test-and-build (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled

This commit is contained in:
Yjoz
2026-04-10 15:31:59 +04:00
parent 87d2688eeb
commit ec0f991a30
55 changed files with 10218 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
'use client';
import { useEffect, useState } from 'react';
import { pricingStore, usePricingStore } from '@/store/usePricingStore';
import Link from 'next/link';
export default function AdminPage() {
const items = usePricingStore((s) => s.items);
const isHydrated = usePricingStore((s) => s.isHydrated);
const [editedPrices, setEditedPrices] = useState<Record<string, number>>({});
const [saved, setSaved] = useState(false);
useEffect(() => {
pricingStore.getState().hydrate();
}, []);
useEffect(() => {
const map: Record<string, number> = {};
items.forEach((item) => {
map[item.id] = item.price;
});
setEditedPrices(map);
}, [items]);
const handlePriceChange = (id: string, value: string) => {
const num = parseInt(value.replace(/[^0-9]/g, ''), 10);
if (!isNaN(num)) {
setEditedPrices((prev) => ({ ...prev, [id]: num }));
} else if (value === '') {
setEditedPrices((prev) => ({ ...prev, [id]: 0 }));
}
};
const handleSave = () => {
Object.entries(editedPrices).forEach(([id, price]) => {
pricingStore.getState().updatePrice(id, price);
});
setSaved(true);
setTimeout(() => setSaved(false), 2000);
};
const handleReset = () => {
pricingStore.getState().resetPrices();
setSaved(false);
};
const formatPrice = (price: number) => {
return new Intl.NumberFormat('en-AE', { style: 'decimal' }).format(price);
};
if (!isHydrated) {
return (
<div style={pageStyle}>
<p style={{ color: '#64748b' }}>Loading...</p>
</div>
);
}
return (
<div style={pageStyle}>
<div style={containerStyle}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '2rem' }}>
<div>
<h1 style={{ fontSize: '1.5rem', fontWeight: 700, color: '#1a1a2e', margin: 0, marginBottom: '0.25rem' }}>
Pricing Dashboard
</h1>
<p style={{ fontSize: '0.8rem', color: '#94a3b8', margin: 0 }}>
Edit prices for the G1 Robot Configurator
</p>
</div>
<Link
href="/"
style={{
padding: '0.5rem 1rem',
borderRadius: '0.375rem',
border: '1px solid rgba(59, 130, 246, 0.2)',
background: 'rgba(59, 130, 246, 0.06)',
color: '#2563eb',
fontSize: '0.8rem',
textDecoration: 'none',
transition: 'all 0.2s ease',
}}
>
Back to Configurator
</Link>
</div>
{/* Pricing Table */}
<div style={{
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(20px)',
border: '1px solid rgba(0, 0, 0, 0.06)',
borderRadius: '0.75rem',
overflow: 'hidden',
}}>
{/* Table Header */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 200px',
padding: '0.75rem 1.25rem',
borderBottom: '1px solid rgba(0, 0, 0, 0.04)',
background: 'rgba(248, 248, 246, 0.5)',
}}>
<span style={{ fontSize: '0.7rem', fontWeight: 600, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
Item
</span>
<span style={{ fontSize: '0.7rem', fontWeight: 600, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.05em', textAlign: 'right' }}>
Price (AED)
</span>
</div>
{/* Rows */}
{items.map((item, index) => (
<div
key={item.id}
style={{
display: 'grid',
gridTemplateColumns: '1fr 200px',
padding: '1rem 1.25rem',
alignItems: 'center',
borderBottom: index < items.length - 1 ? '1px solid rgba(0, 0, 0, 0.04)' : 'none',
transition: 'background 0.15s ease',
}}
>
<div>
<div style={{ fontSize: '0.875rem', color: '#374151', fontWeight: 500 }}>
{item.label}
</div>
<div style={{ fontSize: '0.7rem', color: '#94a3b8', fontFamily: 'monospace' }}>
{item.id}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '0.5rem' }}>
<span style={{ fontSize: '0.75rem', color: '#94a3b8' }}>AED</span>
<input
type="text"
value={formatPrice(editedPrices[item.id] ?? item.price)}
onChange={(e) => handlePriceChange(item.id, e.target.value)}
style={{
width: '130px',
padding: '0.5rem 0.75rem',
borderRadius: '0.375rem',
border: '1px solid rgba(0, 0, 0, 0.1)',
background: 'rgba(255, 255, 255, 1)',
color: '#1a1a2e',
fontSize: '0.875rem',
fontFamily: 'monospace',
textAlign: 'right',
outline: 'none',
transition: 'border-color 0.2s ease',
}}
onFocus={(e) => { e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)'; }}
onBlur={(e) => { e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)'; }}
aria-label={`Price for ${item.label}`}
/>
</div>
</div>
))}
</div>
{/* Actions */}
<div style={{ display: 'flex', gap: '0.75rem', marginTop: '1.5rem', justifyContent: 'flex-end' }}>
<button
onClick={handleReset}
style={{
padding: '0.6rem 1.25rem',
borderRadius: '0.375rem',
border: '1px solid rgba(239, 68, 68, 0.2)',
background: 'rgba(239, 68, 68, 0.05)',
color: '#ef4444',
cursor: 'pointer',
fontSize: '0.8rem',
transition: 'all 0.2s ease',
}}
>
Reset to Defaults
</button>
<button
onClick={handleSave}
style={{
padding: '0.6rem 1.5rem',
borderRadius: '0.375rem',
border: '1px solid rgba(59, 130, 246, 0.3)',
background: saved ? 'rgba(34, 197, 94, 0.08)' : 'rgba(59, 130, 246, 0.08)',
color: saved ? '#16a34a' : '#2563eb',
cursor: 'pointer',
fontSize: '0.8rem',
fontWeight: 600,
transition: 'all 0.2s ease',
}}
>
{saved ? 'Saved!' : 'Save Prices'}
</button>
</div>
</div>
</div>
);
}
const pageStyle: React.CSSProperties = {
minHeight: '100vh',
background: '#ffffff',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '2rem',
fontFamily: 'system-ui, -apple-system, sans-serif',
};
const containerStyle: React.CSSProperties = {
width: '100%',
maxWidth: '640px',
};
+228
View File
@@ -0,0 +1,228 @@
@import "tailwindcss";
@theme {
/* Light Mode Color Palette */
--color-primary: #ffffff;
--color-secondary: #f8f8f6;
--color-accent: #3b82f6;
--color-accent-hover: #2563eb;
--color-gold: #c4a265;
--color-text-primary: #1a1a2e;
--color-text-secondary: #64748b;
--color-text-muted: #94a3b8;
--color-border: #e2e8f0;
--color-border-light: #cbd5e1;
/* Glassmorphism Colors - Light */
--color-glass-bg: rgba(255, 255, 255, 0.85);
--color-glass-border: rgba(0, 0, 0, 0.08);
--color-glass-highlight: rgba(255, 255, 255, 0.5);
/* Spacing & Sizing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border Radius */
--radius-sm: 0.375rem;
--radius-md: 0.5rem;
--radius-lg: 0.75rem;
--radius-xl: 1rem;
/* Shadows */
--shadow-glow: 0 2px 20px rgba(0, 0, 0, 0.06);
--shadow-glow-lg: 0 4px 40px rgba(0, 0, 0, 0.08);
/* Transitions */
--transition-fast: 150ms ease;
--transition-base: 200ms ease;
--transition-slow: 300ms ease;
}
/* Base Styles */
html,
body {
margin: 0;
padding: 0;
min-height: 100vh;
background-color: var(--color-primary);
color: var(--color-text-primary);
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
overflow-x: hidden;
}
/* Scroll Snap */
html {
scroll-behavior: smooth;
scroll-snap-type: y mandatory;
}
/* Focus Styles for Accessibility */
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Selection Styles */
::selection {
background-color: var(--color-accent);
color: #ffffff;
}
/* Glassmorphism Utilities - Light */
.glass-panel {
background: var(--color-glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--color-glass-border);
box-shadow: var(--shadow-glow);
}
.glass-panel-highlight {
background: linear-gradient(
135deg,
var(--color-glass-highlight) 0%,
transparent 50%
);
}
/* Hero animated gradient - Light */
.hero-gradient {
position: absolute;
inset: 0;
background: radial-gradient(ellipse 80% 60% at 50% 40%, rgba(59, 130, 246, 0.04) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 30% 70%, rgba(196, 162, 101, 0.03) 0%, transparent 50%),
linear-gradient(180deg, #f0f0ec 0%, #e8e8e4 50%, #f0f0ec 100%);
animation: heroShift 12s ease-in-out infinite alternate;
}
@keyframes heroShift {
0% {
background-position: 0% 0%, 0% 0%, 0% 0%;
opacity: 1;
}
50% {
opacity: 0.9;
}
100% {
background-position: 100% 100%, 100% 0%, 0% 0%;
opacity: 1;
}
}
/* Fade in up animation */
.fade-in-up {
animation: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Scroll indicator - dark for light mode */
.scroll-indicator {
width: 1px;
height: 40px;
position: relative;
overflow: hidden;
}
.scroll-indicator::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 100%;
background: linear-gradient(180deg, #c4a265, transparent);
animation: scrollPulse 2s ease-in-out infinite;
}
@keyframes scrollPulse {
0%, 100% {
transform: translateY(-100%);
opacity: 0;
}
50% {
transform: translateY(0);
opacity: 1;
}
}
/* Spin animation for loaders */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Scrollbar Styling - Light */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--color-secondary);
}
::-webkit-scrollbar-thumb {
background: var(--color-border-light);
border-radius: var(--radius-sm);
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-text-muted);
}
/* Snap children */
.snap-section {
scroll-snap-align: start;
height: 100vh;
}
/* Responsive Layout Styles */
@media (max-width: 1024px) {
.glass-panel-responsive {
width: 360px !important;
}
}
@media (max-width: 768px) {
.layout-container {
flex-direction: column !important;
}
.glass-panel-responsive {
position: fixed !important;
bottom: 0 !important;
left: 0 !important;
right: 0 !important;
width: 100% !important;
height: 50vh !important;
border-left: none !important;
border-right: none !important;
border-top: 1px solid var(--color-border) !important;
box-shadow: 0 -4px 40px rgba(0, 0, 0, 0.08) !important;
border-radius: 1rem 1rem 0 0 !important;
z-index: 50;
}
.mobile-handle {
display: flex !important;
}
}
.mobile-handle {
display: none;
}
+36
View File
@@ -0,0 +1,36 @@
import type { Metadata, Viewport } from "next";
import { ErrorBoundary } from "@/components/ErrorBoundary";
import { I18nProvider } from "@/components/I18nProvider";
import "./globals.css";
export const metadata: Metadata = {
title: "G1 Configurator | Lootah Robotics",
description: "3D Configurator for the G1 Robot by Lootah Robotics",
};
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
themeColor: "#ffffff",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" dir="ltr">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body suppressHydrationWarning>
<ErrorBoundary>
<I18nProvider>{children}</I18nProvider>
</ErrorBoundary>
</body>
</html>
);
}
+39
View File
@@ -0,0 +1,39 @@
"use client";
import { useRef } from "react";
import { ClientOnly } from "@/components/ClientOnly";
import { ScrollScene } from "@/components/ScrollScene";
import { ScrollOverlays } from "@/components/ScrollOverlays";
import { ConfiguratorSection } from "@/components/ConfiguratorSection";
export default function HomePage() {
const scrollContainerRef = useRef<HTMLDivElement>(null);
return (
<>
{/* Fixed 3D scene behind everything */}
<ClientOnly>
<ScrollScene scrollContainerRef={scrollContainerRef} />
</ClientOnly>
{/* Text overlays that fade based on scroll */}
<ScrollOverlays />
{/* Scroll spacer with snap sections -- 7 sections for the landing */}
<div ref={scrollContainerRef} style={{ position: "relative", zIndex: 1, pointerEvents: "none" }}>
<div className="snap-section" />
<div className="snap-section" />
<div className="snap-section" />
<div className="snap-section" />
<div className="snap-section" />
<div className="snap-section" />
<div className="snap-section" />
</div>
{/* Configurator section */}
<div style={{ position: "relative", zIndex: 20 }}>
<ConfiguratorSection />
</div>
</>
);
}