first
This commit is contained in:
@@ -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',
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user