44 Commits
Author SHA1 Message Date
Najjar\NajjarV02andClaude Opus 5.5 8382501598 feat(seo): fix titles, canonicals, share tags and soft 404s
CI/CD / test-and-build (pull_request) Canceled after 0s
CI/CD / deploy (pull_request) Canceled after 0s
- Titles carried the brand twice (page title plus layout template) and Pudu
  items read "Pudu PUDU". Pages now go through pageMetadata() and displayName().
- /configure, /privacy-policy and /terms-of-service reused the homepage title.
- Eight pages had no canonical and inherited the homepage og:url.
- Unknown /robots, /accessories and /industries slugs returned 200; they are
  now a real 404 (dynamicParams = false) with a not-found page.
- Events, Smart Buildings and Government have no page content yet and rendered
  not-found: moved to UPCOMING_INDUSTRIES until it is written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:57:18 +04:00
Najjar\NajjarV02andClaude Opus 4.8 6f6536db7c chore(contact): update address to DIFC Trade Center Second showroom
CI/CD / test-and-build (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-24 15:47:36 +04:00
Najjar 8bc90b0db9 feat(shop): add AgiBot brand (A2 + X2 flagship) to catalog
CI/CD / test-and-build (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled
2026-07-06 16:12:00 +04:00
Najjar 0cb5693ca3 chore(seo): point shop canonical/sitemap/llms to shop.yslootahrobotics.com
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-07-06 14:16:54 +04:00
NajjarandClaude Opus 4.8 60d50e15e4 feat(seo): robots.txt, richer sitemap, llms.txt, manifest, OG image, JSON-LD
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- Add app/robots.ts (allow crawl, disallow admin/api, sitemap URL)
- Expand sitemap to industries/[slug], accessories/[slug] and more static routes
- Add public/llms.txt (AI summary), site.webmanifest, 1200x630 og.jpg
- Layout: default OG/Twitter image, manifest, robots meta, Organization + WebSite JSON-LD
- Robot pages: canonical, OG image, Product + BreadcrumbList JSON-LD; home canonical

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:45:38 +04:00
NajjarandClaude Opus 4.8 873026a4a9 feat: monochrome black & white rebrand + new Pudu robots
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- Convert site from royal-blue theme to black/white monochrome (desaturate
  all blue-hued colors; keep WhatsApp green and error red)
- Swap blue circular brand logo for mono YS mark; regenerate favicon/PWA icons
- Add 6 Pudu robots (CC1 Pro, MT1 Max, MT1 Vac, T600, T600 Underride, BG1 Pro)
- Fix favicon.ico to RGBA (Next 16 Turbopack decode) and Stripe apiVersion type

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 13:20:26 +04:00
Najjar\NajjarV02andClaude Opus 4.8 c2490947d2 fix: favicon uses official Lootah Robotics logo instead of gold icon
CI/CD / test-and-build (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled
Regenerate favicon.ico (16/32/48px) and apple-touch-icon.png from
public/images/brands/ys-lootah-robotics-logo.webp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:44:27 +04:00
Najjar\NajjarV02andClaude Opus 4.8 7bb71dc707 fix: mobile configurator shows robot above price panel
CI/CD / test-and-build (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled
On mobile (<768px) the flex column put the config aside (price
breakdown + Proceed/Reset) above the robot canvas. Add explicit
order so the robot renders first and the panel flows below it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:27:20 +04:00
Najjar\NajjarV02andClaude Opus 4.8 60e1ea8407 fix: mobile configurator panel flows in-page so Proceed button is reachable
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
On mobile the configurator was a 100vh frame with an inner scrolling
bottom-sheet embedded inside the scrolling /configure page. Page scroll
hijacked the gesture and skipped the frame, trapping the Proceed to Order
and Reset buttons in the inner scroll. Drop the nested scroll on <=768px:
canvas stays fixed on top, the full config panel flows in normal page
scroll so the CTAs are always visible. Landscape side-by-side preserved.

Also pin outputFileTracingRoot to the project dir to silence the
multiple-lockfile workspace-root warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 10:35:24 +04:00
Najjar\NajjarV02 e500fee42f fix: enhance layout and styling for footer and body, add portrait image for Bu Sunaidah
CI/CD / test-and-build (push) Has been cancelled
CI/CD / deploy (push) Has been cancelled
2026-05-22 10:46:46 +04:00
Najjar\NajjarV02 78649c6f3b fix: prevent horizontal overflow and adjust main width for better layout
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-22 10:25:19 +04:00
Najjar\NajjarV02andClaude Opus 4.7 c667736ee6 fix: kill horizontal overflow on mobile
Add `overflow-x: clip` to main/section/header/footer/nav so decorative
absolutely-positioned glow elements (dcta-glow, brand-card-glow,
cap-tile-glow) can't bubble their bounds up to body.scrollWidth.
Without this, body extended ~168px past viewport at 360px width,
producing right-side empty gap and horizontal scroll on Android Chrome.

Also stack 2-col grids on narrow viewports in ExclusiveAccessSection,
FounderSection, and IndustryUseCases stats row to prevent cramped
or overflowing content under 640px.

Verified via Playwright at 320/360/375/390/411/414/480 — body.scrollWidth
now equals viewport at all widths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:10:09 +04:00
Najjar\NajjarV02 e8f3bf8686 fix: enforce LTR layout across the application regardless of language
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-22 09:39:44 +04:00
Najjar\NajjarV02andClaude Opus 4.7 4c415cd5ea fix: bento tiles full-width below 900px (no two-up on mobile)
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:54:20 +04:00
Najjar\NajjarV02andClaude Opus 4.7 8d4fe60f28 fix: BentoGrid flagship tile mobile overlap
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
Robot image and text overlapped on mobile because image was
absolutely positioned over the full card while text was max-width 50%.
Refactored to stacked flex layout below 900px (copy on top, image
below in own region), original side-by-side layout preserved at
desktop via media query.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:41:06 +04:00
Najjar\NajjarV02andClaude Opus 4.7 5a40f6b733 perf: aggressive webp re-compression + max-width 1920 resize
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- recompress every webp at q=70-82 depending on size band
- resize anything wider than 1920px to 1920 max
- convert remaining png/jpg holdouts (mine, thermal, dex3-1 etc) to webp
- restore apple-touch-icon.png (iOS requires png)
- 86 files recompressed, saved 4.36 MB
- public/ images: 11.7 MB -> 6.69 MB (-43%)
- total trajectory: 23.3 MB -> 6.69 MB (-71%)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:31:56 +04:00
Najjar\NajjarV02andClaude Opus 4.7 dda358b59d perf: convert all images to webp, update references
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- 77 jpg/png/gif → webp (kept 7 where webp larger)
- public/ assets: 23.3 MB → ~12 MB (~50% smaller)
- 110 image references updated across data files + components
- scripts/convert-to-webp.mjs + scripts/fix-image-refs.mjs added

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:26:39 +04:00
Najjar\NajjarV02andClaude Opus 4.7 de25d774af content: page copy refinements + accessory rich data extensions
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- about, brands, contact, industries, bu-sunaidah page copy polish
- BuSunaidahSection content updates
- accessories.ts: rich-content interfaces (highlights, featureSections,
  specGroups, paramsImage, footnotes, safetyNotice) + full Dex2-5,
  Dex3-1, Dex1-1, Dex5-1, Z1, D1-T, L2 data sourced from vendor pages

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 17:17:05 +04:00
Najjar\NajjarV02 fb41977b18 feat: enhance UI components and update Instagram post data
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 17:08:24 +04:00
Najjar\NajjarV02 e7fde29a21 Add Instagram post fetching functionality and local JSON fallback
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- Introduced a new TypeScript file `bu-sunaidah-fetch.ts` to handle fetching the latest Instagram posts for Bu Sunaidah.
- Implemented a scraping method to attempt to retrieve posts directly from Instagram, with a fallback to a local JSON file `bu-sunaidah-posts.json`.
- Added a new JSON file containing recent Instagram post URLs and captions.
- Included a new image asset `unitree-l2.jpg` for accessories.
2026-05-21 16:50:22 +04:00
Najjar\NajjarV02 2d32a1e722 feat: add accessory detail and index pages with dynamic routing and metadata
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 16:22:05 +04:00
Najjar\NajjarV02 5271914b16 feat: update RobotCanvas lighting and background styles; enhance button designs
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
fix: update accessory images and compatibility details in accessories data

refactor: modify industry page hero image for education section

feat: add Bu Sunaidah page with sections for media, Instagram feed, and press coverage

feat: create InstagramGlyph icon component for consistent styling

chore: initialize Bu Sunaidah press and Instagram data structures
2026-05-21 16:15:40 +04:00
Najjar\NajjarV02 e500409305 Refactor code structure and remove redundant changes
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 16:00:57 +04:00
Najjar\NajjarV02 1967ae5402 Refactor code structure for improved readability and maintainability
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 15:53:21 +04:00
Najjar\NajjarV02 d8502292bc feat: add AccessoriesShowcase component and accessories data
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- Implemented the AccessoriesShowcase component for displaying various robotic accessories with filtering options.
- Created a new data file for accessories, including types, groups, and detailed information for each accessory.
- Introduced FilterChip and AccessoryCard components for better UI representation of accessories.
2026-05-21 15:45:45 +04:00
Najjar\NajjarV02 37fab3fcaf Refactor code structure for improved readability and maintainability
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 15:36:32 +04:00
Najjar\NajjarV02 574d43adb4 feat: enhance industry detail and showcase components with responsive images and improved styles
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 15:24:31 +04:00
Najjar\NajjarV02 7c3b42e651 Refactor code structure for improved readability and maintainability
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 14:54:20 +04:00
Najjar\NajjarV02 b5876aead5 refactor: update icon imports to individual paths and enhance industry page content
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 14:38:45 +04:00
Najjar\NajjarV02 329967df60 feat: add industry pages data structure for robotics solutions across various sectors
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 14:27:04 +04:00
Najjar\NajjarV02 fa5f4f7a3e feat: add global responsive guards and enhance layout for mobile and touch interactions
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 13:46:17 +04:00
Najjar\NajjarV02 de9f358582 refactor: remove ServicesGrid component and its associated styles 2026-05-21 13:28:38 +04:00
Najjar\NajjarV02 7458818c62 feat: add CTAButton component with customizable variants, sizes, and arrows
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-21 12:57:56 +04:00
Najjar\NajjarV02 79f7f9e579 feat: enhance CategoryShowcaseScroll with manual scroll locking and improved layout adjustments
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-20 18:21:20 +04:00
Najjar\NajjarV02 8457493f49 refactor: replace RobotCategoryGrid with CategoryShowcaseScroll and update styles across components
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- Updated HomePage to use CategoryShowcaseScroll instead of RobotCategoryGrid for better category display.
- Modified BrandShowcase component to adjust background gradients and text colors for improved aesthetics.
- Changed CompanyStory component to enhance visual consistency with updated accent colors.
- Refined ExclusiveAccessSection to align with new design standards, including gradient adjustments.
- Enhanced Hero3DRobotics with new gradient backgrounds and updated text colors for better readability.
- Updated RoboticsScrollShowcase to reflect new color scheme and improved text visibility.
- Adjusted RoboticsSplineShowcase to maintain design consistency with updated colors and gradients.
- Improved MotionSection to enhance scroll behavior and visibility transitions.
- Updated container-scroll-animation styles for better visual effects.
- Introduced new CategoryShowcaseScroll component for a more dynamic category display experience.
2026-05-20 18:15:43 +04:00
Najjar\NajjarV02 729ab71c2c Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-20 18:03:42 +04:00
Najjar\NajjarV02 bfe589be27 feat: enhance BrandShowcase component with improved layout and visual elements
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-20 17:58:58 +04:00
Najjar\NajjarV02 d7c343ac7d refactor: update Hero3DRobotics and RoboticsSplineShowcase components for improved styling and layout
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- Adjusted padding, opacity, and transform properties for robot images in Hero3DRobotics.
- Updated sizes for images to enhance responsiveness.
- Modified floor reflection styles for better visual consistency.
- Refactored RoboticsSplineShowcase to use inline styles for layout and design consistency.
- Enhanced text styles and button designs for better user experience.
- Improved grid layout for better responsiveness across different screen sizes.
2026-05-20 17:56:07 +04:00
Najjar\NajjarV02 461a00384c feat: add robotics showcase components and UI enhancements
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- Introduced FounderSection component to highlight leadership and vision.
- Created ServicesGrid component to display various robotics services offered.
- Developed RoboticsScrollShowcase for showcasing robots with interactive elements.
- Implemented RoboticsSplineShowcase featuring a 3D Spline scene for enhanced user experience.
- Added reusable Card component for consistent styling across sections.
- Integrated ContainerScroll for animated scrolling effects in the showcase.
- Built SplineScene component for lazy loading Spline 3D scenes.
- Added Spotlight component for interactive hover effects.
- Created utility function for class name merging to streamline styling.
2026-05-20 17:46:36 +04:00
Najjar\NajjarV02 92cf4aba3b feat: add robotics components and data structures
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
- Introduced RobotProductCard component for displaying robot details.
- Added WhyUs component highlighting key reasons for choosing our robotics solutions.
- Implemented CursorSpotlight for enhanced user interaction.
- Created GlassPanel for a stylish UI element.
- Developed MotionSection for animated section visibility.
- Added PremiumButton for versatile button options.
- Established data structures for industries and robots, including detailed specifications and use cases.
- Included utility functions for retrieving robots by slug and category.
2026-05-20 17:22:47 +04:00
Najjar\NajjarV02 2eac53dac3 feat(seed): update pricing item seeding to upsert on every deploy
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-19 17:46:22 +04:00
Najjar\NajjarV02 fe5f1ce25f Refactor code structure for improved readability and maintainability
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions
2026-05-19 17:44:20 +04:00
Najjar\NajjarV02andClaude Opus 4.7 52b910ed93 feat(pricing): switch base+EDU prices to AED and unify attire at 5000 AED
- `base` (Basic body) → 77,125 AED  (Unitree G1 retail $16k + $5k markup)
- new `edu` row     → 146,900 AED  ($40k flat)
- all persona attire (kandura, vest, suit, robot-doctor, security-guard) → 5,000 AED
- `custom-color` unchanged at 3,500 AED
- PricingEngine now swaps the base line when EDU body is selected
- localStorage key bumped to `lootah-pricing-v2` to invalidate stale client caches
- Robot Body buttons now describe the variants as `Standard consumer` vs `Open-source education / research` (same chassis GLB, differs in licensing)
- New `prisma/update-prices.ts` idempotent script applies the new prices to an existing DB; seed.ts updated for fresh installs
- Pricing tests updated to match new defaults

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:40:04 +04:00
Najjar\NajjarV02andClaude Opus 4.7 03dbc4ac98 feat(configure): add Basic vs EDU robot body selector under Persona Attire
- New `activeBody` field in config store with `setBody` action and URL sync (`b` key, defaults to `basic` for backward compat)
- Robot Body section in ConfigPanel between Persona Attire and Pricing
- RobotModel base mesh extracted into BaseBodyMesh subcomponent wrapped in error boundary so a missing EDU GLB silently falls back to basic
- Tests cover defaults, setBody, reset, and round-trip serialization

Note: drop `/public/Unitree_G1_EDU.glb` to enable the EDU variant — until then EDU selection falls back to basic with a console warning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 17:11:14 +04:00
273 changed files with 22179 additions and 1916 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"permissions": {
"allow": [
"Bash(npx next *)",
"WebFetch(domain:www.unitree.com)",
"WebFetch(domain:www.pudurobotics.com)",
"WebFetch(domain:cdn.unitree.com)",
"Bash(curl -s \"https://www.unitree.com/\")",
"Bash(curl -sL \"https://www.unitree.com/\" -A \"Mozilla/5.0 \\(Windows NT 10.0; Win64; x64\\) AppleWebKit/537.36\")",
"Bash(curl -sL \"https://www.unitree.com/\" -A \"Mozilla/5.0 \\(Windows NT 10.0; Win64; x64\\) AppleWebKit/537.36 \\(KHTML, like Gecko\\) Chrome/120.0.0.0 Safari/537.36\")",
"Bash(curl -sL \"https://www.unitree.com/_nuxt/entry.DnivrlYq.css\" -A \"Mozilla/5.0\")",
"Bash(curl -sL \"https://www.unitree.com/_nuxt/default.awSnd8AZ.css\" -A \"Mozilla/5.0\")",
"Bash(curl -sL \"https://www.unitree.com/images/37d3d40c1272402a9c363a50b405276d.svg\" -A \"Mozilla/5.0\" -o \"C:\\\\Users\\\\NajjarV02\\\\Desktop\\\\yslootahrobotics\\\\public\\\\brands\\\\unitree-logo-white.svg\")",
"Bash(curl -sL \"https://www.unitree.com/images/0079f8938336436e955ea3a98c4e1e59.svg\" -A \"Mozilla/5.0\" -o \"C:\\\\Users\\\\NajjarV02\\\\Desktop\\\\yslootahrobotics\\\\public\\\\brands\\\\unitree-logo-dark.svg\")",
"PowerShell(Copy-Item *)"
]
}
}
@@ -0,0 +1,236 @@
# Component Rules
Per-element rules. Apply when designing or reviewing the corresponding component.
---
## Buttons
**Primary**
- Gradient gold: `bg-gradient-to-r from-gold-light via-gold to-gold-deep`
- Text: `text-obsidian` (dark on gold for contrast)
- Shape: `rounded-full px-7 py-3.5 text-sm font-medium tracking-wide`
- Shadow: `shadow-[0_10px_40px_-12px_rgba(212,164,55,0.6)]`
- Hover shadow only: `hover:shadow-[0_18px_50px_-12px_rgba(242,194,91,0.8)]`
- Optional `magnetic` wrapper for cursor pull effect on desktop.
**Outline**
- `border border-bone/20 text-bone hover:border-gold/60 hover:text-gold-light`
- Same shape as primary.
**Ghost**
- `text-bone hover:text-gold-light` no border, no fill.
**Forbidden**
- Bright red CTA buttons (unless brand color is red).
- Square corners on action buttons.
- Solid `#000` or `#fff` backgrounds.
- Loading text inside button that breaks layout use a spinner icon + word swap with fixed min-width.
- Success/error message **inside** button render outside as a banner.
---
## Cards
**Standard card**
```tsx
<div className="group relative isolate h-full overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 p-6 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/30 hover:bg-graphite/60 md:p-7">
{/* hover amber glow (decorative) */}
{/* content */}
{/* gold underline sweep at bottom */}
</div>
```
**Premium / featured card** (used for cert card, sale card, hero CTA)
- `border-gold/25`
- `bg-gradient-to-br from-[#1a1407] via-[#0c0a06] to-[#1e1409]`
- `shadow-[0_30px_120px_-30px_rgba(212,164,55,0.45)]`
- Often has animated `shimmer` hairline on top edge.
**Grid layout**
- `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-N gap-4 md:gap-5`
- Apply `cardGrid` + `cardCell` variants for staggered entry.
**Inside the card**
- Top row: small icon disc (`size-11 glass-gold`) + optional mono micro-label.
- Heading: `font-display text-2xl md:text-3xl`.
- Body: `text-sm text-mist leading-relaxed`.
- Bottom: optional "Learn more" link or gold underline sweep.
---
## Section heading
```tsx
<div className="flex flex-col items-center gap-6 text-center"> {/* or items-start for left-aligned */}
<SectionLabel index="01">Section name</SectionLabel>
<Reveal>
<h2 className="font-display text-5xl leading-[1.02] text-bone text-balance md:text-6xl">
Plain words and a{" "}
<span className="gold-text">gold accent phrase.</span>
</h2>
</Reveal>
<Reveal delay={0.1}>
<p className="max-w-2xl text-mist text-pretty md:text-lg">
One refined sentence describing the section.
</p>
</Reveal>
</div>
```
**Rules**
- Always `text-balance` on h2 for clean wrapping.
- `text-pretty` on subtitle.
- Use SectionLabel with sequential index per page (01, 02, 03…). No mono font.
- One gold-accent phrase max per heading overdoing gold cheapens it.
---
## Navbar
- Fixed top: `fixed inset-x-0 top-0 z-50`.
- Logo left, nav center/right, primary CTA + WhatsApp icon right.
- Scrolled state: `backdrop-blur-xl bg-obsidian/70 border-b border-bone/5`.
- Nav links: `text-sm font-medium tracking-[0.005em] text-bone/85 hover:text-gold-light`.
- Active state via `usePathname()` → `text-gold-light` + full-width underline + `aria-current="page"`.
- Mobile: hamburger right, menu drawer below, links close on click.
---
## Footer
**Layout 4 columns, no duplication**
| Column 1 | Column 2 | Column 3 | Column 4 |
|---|---|---|---|
| Brand + contact | Navigation | Services | Legal & App |
| Logo, tagline, phone, email, WhatsApp, Instagram | Home + all top-level routes | Specific service items | Privacy Policy + 2 store badges |
**Rules**
- No page link appears in two columns.
- Privacy Policy lives only under Legal.
- App badges only in Legal & App column (footer) official SVGs at 132 px width, 8 px gap.
- External links open in new tab with rel attrs.
- Background: `bg-obsidian` with subtle "LUXAM"-style watermark behind content (translate-y-[18%], opacity 0.025).
- Bottom row: `© YYYY {brand}. All rights reserved.` + tagline + agency credit (subtle, gold-underlined on hover).
---
## Forms
- Field wrapper: `<label>` with small uppercase metadata label + input.
- Input class:
```
peer w-full rounded-xl border border-bone/12 bg-obsidian/60
px-4 py-3.5 text-sm text-bone placeholder-mist/40 outline-none
transition-colors duration-300
focus:border-gold/60 focus:bg-obsidian/80
focus:shadow-[0_0_0_3px_rgba(212,164,55,0.18)]
aria-[invalid=true]:border-[#a3261c]/60
```
- Honeypot field: hidden `<input name="website">` silently succeed if filled.
- Validation: server-side via `submitContact()` action; client gets `state.fieldErrors` + `state.message`.
- Submission states: `useActionState` + `useFormStatus`. Spinner inside button, success/error banner **outside** button.
---
## Icons
- Source: `lucide-react` (already installed).
- Size: `size-4` inline, `size-5` standalone, `size-[18px]` inside discs.
- StrokeWidth: `1.4` for elegance (not the default 2).
- Wrapper for emphasis: `glass-gold size-11 grid place-items-center rounded-full`.
- **Forbidden**: oversized icons (>32 px) standalone, multi-color icon sets, cartoonish flat icons.
**No lucide WhatsApp glyph** use custom inline SVG from `snippets/WhatsAppLink.tsx`.
**No lucide Instagram glyph in v1.16** use custom inline SVG (see footer).
---
## App store badges
- Use **official SVG badges**, not custom-drawn pills with text.
- Sources: svgrepo.com `/show/<id>/<slug>.svg` (downloads clean SVG).
- Store locally in `public/images/app-store-badge.svg` + `public/images/google-play-badge.svg`.
- Render via plain `<img>` (next/image blocks SVG by default in Next 16).
- Width: `132 px` footer, `140-160 px` hero CTA section.
- Hover: `opacity-85` + `-translate-y-0.5`. No gold circles around them.
- Stack with `gap-2` (8 px) in footer.
```tsx
<a href={app.appStore} target="_blank" rel="noopener noreferrer" aria-label="Download Luxam on the App Store"
className="inline-flex transition duration-300 hover:-translate-y-0.5 hover:opacity-85">
<img src="/images/app-store-badge.svg" alt="Download on the App Store"
className="h-auto w-[132px] max-w-full" loading="lazy" />
</a>
```
---
## WhatsApp link
- URL form: `https://api.whatsapp.com/send/?phone=<digits-only>&text&type=phone_number&app_absent=0`
- Phone in URL: country code + number, **no `+` or spaces**.
- Display phone format: `+CC XX XXX XXXX`.
- Anchor: `target="_blank" rel="noopener noreferrer" aria-label="Chat with [Brand] on WhatsApp"`.
- Icon: inline SVG (lucide has no WhatsApp glyph). Refined gold tint, not bright green.
- Two surfaces: nav (circular icon button) + footer (icon + display number).
- Optional third surface: contact page row.
---
## Client logo section
- Horizontal marquee, `animation: marquee 50s linear infinite`.
- Hover pauses via `onMouseEnter/Leave` setting `animationPlayState`.
- Duplicate the data array `[...clients, ...clients]` for seamless loop.
- Tiles: `h-24 w-44 sm:h-28 sm:w-52 rounded-2xl border border-bone/10 bg-graphite/40 backdrop-blur-sm`.
- Logos: `object-contain max-h-14 sm:max-h-16`. Default `grayscale opacity-70`. Hover: `grayscale-0 opacity-100 scale-105`.
- Edge fade via `mask-image: linear-gradient(90deg, transparent, black 8%, black 92%, transparent)`.
- Reduced-motion fallback: render a static `grid-cols-2 sm:grid-cols-3 lg:grid-cols-6`.
---
## Certificate gallery
- 4-col on desktop, 2-col tablet, 1-col mobile.
- Each tile: portrait `aspect-[3/4]` with `next/image fill object-cover object-top`.
- Top-left badge: page indicator (`Page 1`, `Page 2 · Annex`).
- Top-right: ArrowUpRight disc (links open in new tab).
- Click anywhere on tile opens the PDF.
- Hover: scale 1.04 (1.2s ease-out), gold ring border, amber radial glow, caption title → gold-light.
- Caption below image: `font-mono` micro-label (document name in gold) + title + "Open PDF" CTA.
- Optional "Need the full PDFs?" pill row below grid for direct downloads.
---
## Final CTA section
- Wraps inside a premium gold-bordered glass card.
- Animated halo (`!reduce && motion.div opacity pulse`).
- Grid: `lg:grid-cols-12` (8/4 split).
- Left col: SectionLabel + h2 with one gold-accent phrase + paragraph.
- Right col: 2 buttons (`primary` + `outline`).
- Place at the end of every long page, before the footer.
---
## Privacy / Legal pages
- Single-column max-w-3xl reading layout.
- Each section gets `id="..."` + `scroll-mt-28` for anchored navigation.
- `font-display` h2 per section, `text-mist` body, `text-pretty` on every `<p>`.
- Effective date + `metallic-divider` at top.
- Bottom disclaimer: `text-sm text-mist/70` with mailto link.
---
## Don't
- Don't add hover effects without `transition-*` for smoothness.
- Don't use `transform-origin` defaults on rotated icon discs keep them small (8-12°).
- Don't use `whileHover={{ scale: 1.1 }}` on cards too theatrical. Use `-translate-y-1` instead.
- Don't add box-shadows with multiple layers and bright colors. One subtle amber glow max.
- Don't introduce a new font family per page stick to display + sans.
- Don't render placeholder images (`via.placeholder.com`, gray boxes) in production code. Always use real assets the user provided.
@@ -0,0 +1,45 @@
# Component Snippets
Overview of the reusable TSX snippets in `snippets/`. Each is a copy-paste base for a new project adapt brand tokens + content via your own `lib/content.ts`.
| File | Purpose | Depends on |
|---|---|---|
| `PremiumButton.tsx` | Gold-gradient primary + outline + ghost variants, optional magnetic wrapper | `lib/cn`, lucide ArrowUpRight |
| `SectionHeading.tsx` | Premium section heading w/ eyebrow + serif h2 + sub paragraph | `lib/cn`, framer Reveal |
| `GlassCard.tsx` | Dark glass card w/ hover lift, amber glow, gold underline sweep | lucide LucideIcon |
| `Footer.tsx` | 4-column footer w/ no duplication, watermark, agency credit | brand contact data |
| `AppBadges.tsx` | Official App Store + Google Play badge anchors (plain `<img>`) | `/public/images/*-badge.svg` |
| `WhatsAppLink.tsx` | Refined gold WhatsApp icon button + label variant | inline SVG (no lucide glyph) |
| `ClientLogoMarquee.tsx` | Auto-scroll logo marquee w/ hover pause + reduced-motion grid fallback | logos array, marquee keyframe in globals.css |
| `CertificateGallery.tsx` | 4-image bento gallery linking to PDFs in new tab | `next/image`, certs array |
## How to use
1. Copy the file you need into your project's `src/components/` (or wherever your component layer lives).
2. Update import paths (`@/lib/cn`, `@/components/ui/...`) to match your project alias.
3. Wire data: the snippets accept props or read from a `lib/content.ts` you control.
4. Confirm Tailwind v4 `@theme` tokens (`obsidian`, `gold`, `bone`, `mist`, etc.) exist see `DESIGN_SYSTEM.md`.
5. Confirm Framer Motion + lucide-react installed:
```bash
npm i framer-motion lucide-react
```
## Adapt patterns
The snippets show the **shape**. Your brand may need:
- Different palette (warm copper instead of gold? `--color-gold` → your token, keep the same usage rules).
- Different copy hierarchy (more / fewer columns in footer).
- Different surfaces (e.g., add a Resources column).
Keep the **interaction grammar** consistent:
- Hover lift `-translate-y-1`.
- Amber/gold glow bottom-right.
- Underline sweep on cards.
- 1.4 strokeWidth on icons.
- Serif on h1/h2 only.
That's what makes any palette feel "Luxam-class" premium.
## Don't fork without reason
If a snippet does 95% of what you need, **wrap** it with project-specific props rather than forking. Forks drift.
@@ -0,0 +1,204 @@
# Design System
Reusable design tokens + scales extracted from the Luxam reference build. Drop into `globals.css` (Tailwind v4 `@theme`) of any new project to start with the same DNA.
---
## Color palette
```css
@theme {
/* Backgrounds obsidian → graphite layering */
--color-obsidian: #08080A; /* page background */
--color-graphite: #15161A; /* dark glass card */
--color-graphite-2: #1C1D22; /* slightly lifted card variant */
/* Foreground */
--color-bone: #F4F0E6; /* primary text on dark */
--color-mist: #C7C2B5; /* secondary/muted text */
/* Brand gold spectrum (use sparingly + structurally) */
--color-gold: #D4A437; /* core brand gold */
--color-gold-light: #F2C25B; /* hover, highlight, gradient stops */
--color-gold-deep: #7A4A12; /* gradient base */
--color-ember: #B5731A; /* warm secondary */
/* Sustainability accent (use only where ecology theme applies) */
--color-emerald: #1F8F6B;
--color-emerald-glow: #3FD8A4;
}
```
**Usage rules**
- **Background:** `bg-obsidian` body, `bg-graphite/40 backdrop-blur-sm` cards.
- **Borders:** `border-bone/10` default, `border-gold/30` on hover, `border-gold/40` active.
- **Text:** `text-bone` titles, `text-mist` body/secondary, `text-gold-light` brand accents.
- **Never use** raw `#fff` or pure `#000`. Always `bone` or `obsidian`.
- **Gradients:** single-axis brand gradient `from-gold-light via-gold to-gold-deep`. No rainbow.
---
## Typography
```ts
// app/layout.tsx
import { Cormorant_Garamond, Geist, Geist_Mono } from "next/font/google";
const display = Cormorant_Garamond({
subsets: ["latin"],
variable: "--font-display",
weight: ["400", "500", "600"],
display: "swap",
});
const sans = Geist({ subsets: ["latin"], variable: "--font-sans", display: "swap" });
const mono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono", display: "swap" });
```
**Hierarchy**
| Role | Font | Size |
|---|---|---|
| Hero H1 | `font-display` (serif) | `text-[clamp(2.6rem,7vw,6.4rem)] leading-[1.0] tracking-[-0.02em]` |
| Section H2 | `font-display` | `text-[clamp(2.4rem,5vw,4.6rem)] leading-[1.02] tracking-[-0.02em]` |
| Card H3/H4 | `font-display` | `text-2xl md:text-3xl` |
| Body | `font-sans` (inherited default) | `text-base md:text-lg leading-[1.7]` |
| UI label | `font-sans` | `text-[10px] md:text-[11px] font-medium uppercase tracking-[0.22em]` |
| Nav link | `font-sans` | `text-sm font-medium tracking-[0.005em]` |
| Stats number | `font-display` | `text-5xl md:text-6xl` (responsive smaller on mobile) |
**Avoid**
- `font-mono` on long values or body text.
- `tracking-[0.3em]+` on small labels.
- `leading-[0.95]` on serif headlines (clips ascenders).
**Helper class** for one-color brand text:
```css
.gold-text {
background: linear-gradient(90deg, #FFEFC5, #F2C25B 40%, #D4A437 80%, #7A4A12);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
```
---
## Spacing scale
Section padding lives **only** on the `Section` primitive:
```tsx
// components/ui/Section.tsx
className={cn("relative scroll-mt-24 py-16 md:scroll-mt-28 md:py-20 lg:py-24", className)}
```
- Mobile: `py-16` (64 px)
- Tablet: `md:py-20` (80 px)
- Desktop: `lg:py-24` (96 px)
- All anchored sections get `scroll-mt-24 md:scroll-mt-28` for sticky-nav clearance.
**Don't** override per-section unless there's a structural reason. Centralize.
Card padding: `p-6 md:p-7` standard, `p-7 md:p-10` for hero/feature cards.
Grid gaps: `gap-4 md:gap-5` standard, `gap-6 md:gap-8` for large cards.
---
## Border radius scale
```css
--radius-xs: 4px;
--radius-sm: 8px;
--radius-md: 14px;
--radius-lg: 22px;
--radius-xl: 32px;
```
Tailwind shortcuts to match: `rounded-2xl` (cards), `rounded-3xl` (hero panels), `rounded-full` (pills + buttons).
---
## Motion
```ts
// shared variants
const cardGrid: Variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.15 } },
};
const cardCell: Variants = {
hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
visible: {
opacity: 1, y: 0, filter: "blur(0px)",
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] },
},
};
```
- Entrance ease: `[0.16, 1, 0.3, 1]` (cubic-bezier "out-quart-ish").
- Duration: hero 1.3s, cards 0.8s, hover 0.3-0.5s.
- Hover lift: `-translate-y-1` max. No `-translate-y-2`+ (jumpy).
- Halo pulse: `[0.5, 0.85, 0.5]` opacity, 6s loop. Wrap in `!reduce && (...)`.
- `useReducedMotion()` must be read in any animated component.
**Forbidden**
- Bouncy springs (`stiffness > 200`) on entrance.
- Scale jumps > 1.05 on hover for cards.
- Parallax > 200 px translate range.
- Marquees without `mask-image` edge fade.
---
## Visual textures
| Class | Purpose |
|---|---|
| `noise-bg` | Subtle film-grain background overlay |
| `grain` | Animated grain (use sparingly on cards) |
| `metallic-divider` | Thin gold-tinted divider line |
| `radial-gold` | Faint amber radial wash (positioned absolute, blur-3xl) |
| `radial-emerald` | Same, emerald variant |
| `hairline-t`, `hairline-b` | 1 px top/bottom hairline borders |
| `mask-fade-y`, `mask-fade-x` | Edge fade masks |
Define these as utilities in `globals.css` per the Luxam reference.
---
## Hover state pattern
Every card/button uses the same hover vocabulary:
1. `-translate-y-1` lift (cards) or `-translate-y-0.5` (buttons).
2. Border step up: `border-bone/10 → border-gold/30 (or /35 /40)`.
3. Background step up: `bg-graphite/40 → bg-graphite/60`.
4. Amber radial glow bottom-right: `radial-gradient(circle, rgba(212,164,55,0.28), transparent 70%)` blurred.
5. Gold underline sweep (cards): `absolute bottom-0 left-0 h-px w-0 ... group-hover:w-full`.
6. Optional: top hairline gold gradient fade-in.
7. Icon disc gets `rotate-[8deg]` + amber halo shadow.
Apply via `group` + `group-hover:` Tailwind variants never per-element JS listeners.
---
## Container widths
```tsx
// components/ui/Container.tsx three variants
size="default" → max-w-7xl
size="wide" → max-w-[1480px]
size="narrow" → max-w-3xl
```
All variants: `mx-auto px-6 md:px-10`.
---
## When in doubt
- More dark, less gold.
- More space, less content.
- Less motion, more restraint.
- Real photography > illustration.
- Editorial typography > corporate sans.
@@ -0,0 +1,274 @@
# Prompt Templates
Copy-paste prompts for common tasks. Replace `{{placeholders}}` with project specifics.
---
## 1. Create homepage from scratch
```
Act as a senior frontend architect.
Build a premium, cinematic, dark/gold luxury homepage for {{BRAND}} ({{INDUSTRY}}) using the design DNA in .claude/skills/premium-frontend-designer/.
Stack: Next.js 16 App Router · TypeScript · Tailwind v4 · Framer Motion · lucide-react.
Composition (top → bottom):
1. Nav (fixed, scroll-aware)
2. Hero (min-h-100svh, parallax halos, 2 CTAs, eyebrow + serif h1 + sub + body)
3. BrandStrip (marquee of capability keywords)
4. About / Story (split 5/7, copy + visual)
5. WhyUCO-style problem→solution comparison
6. Services overview (6 cards)
7. Trusted Clients marquee (use ONLY logos I provide)
8. Stats / Impact metrics (animated counters)
9. Cinematic reel (lazy video)
10. Process / Closed loop (timeline)
11. Industries served
12. Certifications
13. Gallery (4-image bento)
14. Final CTA section
15. Footer (4 columns, no duplication)
Brand details I will provide:
{{BRAND_LEGAL_NAME, PHONE, EMAIL, WHATSAPP_URL, INSTAGRAM, APP_STORE, GOOGLE_PLAY, AGENCY_CREDIT}}
Do NOT invent links. Ask if missing.
```
---
## 2. Redesign an existing page
```
Inspect {{PAGE_ROUTE}}. Keep the content + intent but rebuild it to match the design DNA in .claude/skills/premium-frontend-designer/.
Requirements:
- Reuse existing primitives (Section, Container, Button, Reveal, SectionLabel, etc.) don't duplicate.
- Apply the cardGrid + cardCell motion variants for grid entrances.
- Add a single gold-accent phrase per h2.
- Centralize section padding via the Section primitive (no per-page overrides).
- Run typecheck + grep for placeholder hrefs before reporting done.
Return a clear file change list with NEW / MODIFIED markers.
```
---
## 3. Add a polished footer
```
Build a 4-column premium footer for {{BRAND}}.
Columns:
1. Brand + contact (logo, tagline, phone, email, WhatsApp, Instagram)
2. Navigation (top-level pages)
3. Services (specific service items)
4. Legal & App (Privacy Policy + 2 store badges)
No duplicate link labels across columns. External links open in new tab.
Footer bottom row: copyright + tagline + subtle agency credit (gold underline on hover).
Watermark: project name in serif, large, opacity-0.025, contained inside footer overflow.
Brand details:
{{PHONE, EMAIL, WHATSAPP_URL, INSTAGRAM_URL, APP_STORE_URL, GOOGLE_PLAY_URL, AGENCY_CREDIT_URL}}
```
---
## 4. Add WhatsApp button
```
Add a refined WhatsApp button to:
1. Footer contact stack (icon + label "WhatsApp · {{DISPLAY_NUMBER}}").
2. Nav (circular icon-only button next to primary CTA, mobile menu pill).
3. Contact page info card (full-width tile w/ icon disc).
URL: {{WHATSAPP_API_URL}}
Display number: {{+CC XX XXX XXXX}}
Use inline SVG (lucide has no WhatsApp glyph). Refined gold tint, not bright green. Hover: gold border + soft glow + scale 1.04. aria-label="Chat with {{BRAND}} on WhatsApp". target="_blank" rel="noopener noreferrer".
```
---
## 5. Add app store badges
```
Add official App Store + Google Play badge SVGs.
URLs:
- App Store: {{APP_STORE_URL}}
- Google Play: {{GOOGLE_PLAY_URL}}
Tasks:
1. Download from svgrepo.com (use /show/ path not /download/):
- https://www.svgrepo.com/show/303128/download-on-the-app-store-apple-logo.svg
- https://www.svgrepo.com/show/303139/google-play-badge-logo.svg
2. Save to public/images/app-store-badge.svg + public/images/google-play-badge.svg.
3. Render via plain <img> (next/image blocks SVG in Next 16).
4. Width: 132 px footer / 140-160 px in hero CTA section.
5. Hover: opacity-85 + -translate-y-0.5. No gold circles.
6. Stack gap-2 (8 px) in footer.
7. aria-label on every anchor. target="_blank" rel="noopener noreferrer".
Use the snippet from snippets/AppBadges.tsx as a starting point.
```
---
## 6. Add client logo section
```
Add a premium "Trusted by leading {{INDUSTRY}} partners" client logo marquee to the homepage (after Services) and About page (after process).
Use ONLY these 12 logos I provide:
{{LIST OF LOGO URLS}}
Tasks:
1. Download each into public/images/clients/ with clean filenames.
2. Create src/lib/clients.ts with typed array { src, alt }.
3. Use recognisable brand names for alt text; for unidentified logos use "{{BRAND}} client logo".
4. Marquee: 50s linear infinite, hover-pause, mask-image edge fade.
5. Tile: h-24 w-44 mobile / h-28 w-52 tablet+. Default grayscale + opacity 70%, hover full color + scale 1.05.
6. Reduced-motion fallback: render a static grid-cols-2 sm:grid-cols-3 lg:grid-cols-6.
Use snippets/ClientLogoMarquee.tsx as the base.
```
---
## 7. Add certificate gallery
```
Replace the document-card list on /certifications with a 4-image gallery showing the actual certificate pages.
Use these exact images (download to public/images/certifications/):
{{IMG1_URL, IMG2_URL, IMG3_URL, IMG4_URL}}
Each image links to the corresponding PDF (already in public/documents/certifications/).
Layout: sm:grid-cols-2 lg:grid-cols-4 with aspect-[3/4] portrait tiles.
Tile chrome:
- Top-left page badge (Page 1 / Page 2 · Annex).
- Top-right ArrowUpRight disc.
- Bottom caption: mono category in gold + serif title + "Open PDF" CTA.
- Hover: scale 1.04, gold ring border, amber radial glow, gold underline sweep.
External: target="_blank" rel="noopener noreferrer" on every anchor.
Optional: secondary "Need the full PDFs?" pill row below grid with direct download links.
Use snippets/CertificateGallery.tsx as a starting point.
```
---
## 8. Fix mobile responsiveness
```
The {{PAGE}} layout breaks on mobile. Specific issues:
{{ISSUES_LIST}}
Walk through the responsive checklist in .claude/skills/premium-frontend-designer/RESPONSIVE_CHECKLIST.md and fix every applicable item.
Test at 320 / 375 / 390 / 414 / 768 / 1024 viewports.
Don't redesign only fix layout, spacing, overflow, clipping. Preserve the visual identity.
Return a table of issues + fixes (Before / After columns).
```
---
## 9. Polish typography sitewide
```
Audit and refine typography across all pages:
1. Hero h1: clamp(2.4rem, 7vw, 6.4rem), leading-[1.0], tracking-[-0.02em]. Add pt buffer for sticky-nav clearance.
2. Two-font system: serif for h1/h2 only, clean sans (Geist/Inter) for everything else.
3. Remove font-mono from long values + body text. Keep only on tiny brand-pattern eyebrow tags if at all.
4. Tracking: reduce all tracking-[0.32em+] on small labels to tracking-[0.22em].
5. Nav links: text-sm font-medium tracking-[0.005em] (slightly larger + medium weight, near-zero tracking).
6. BrandStrip marquee: smaller (text-xs md:text-sm uppercase) so it doesn't compete with hero.
7. Section labels via SectionLabel primitive never inline.
8. text-balance on h1/h2, text-pretty on body paragraphs.
Confirm:
- typecheck clean
- HTTP 200 on every route
- zero tracking-[0.32em] remaining
- no font-mono on user-facing body content
```
---
## 10. Add Privacy Policy
```
Create /privacy-policy route matching the premium dark/gold style.
9 sections with id="..." anchors + scroll-mt-28:
- introduction
- information-we-collect
- how-we-use-information
- contact-form-data
- cookies-and-analytics
- data-protection
- third-party-services
- user-rights
- contact-information
Use brand contact details from lib/content.ts.
Effective date stamp at top + metallic-divider. Bottom disclaimer with mailto link in gold underline.
Single column max-w-3xl. font-display h2 per section. text-pretty body.
Don't write fake legal claims. General professional language only. Tell users they can contact for privacy questions.
Add to footer Legal column.
```
---
## 11. Coolify deploy via Dockerfile
```
Deploy {{PROJECT}} to Coolify with a Dockerfile build pack.
Tasks:
1. Add `output: "standalone"` to next.config.ts.
2. Create multi-stage Dockerfile (node:22-alpine, 3 stages: deps → build → run).
3. Add .dockerignore (skip .git, node_modules, .next cache, env files, .claude).
4. Run-stage as non-root user (UID 1001).
5. EXPOSE 3000, CMD ["node", "server.js"] using standalone output.
Then via Coolify API:
- PATCH application: build_pack="dockerfile", dockerfile_location="/Dockerfile", ports_exposes="3000", domains="https://{{DOMAIN}}"
- POST /api/v1/deploy?uuid={{APP_UUID}}&force=true
- Poll deployment until terminal status.
If "no available server" appears on the domain after deploy: verify DNS resolves to Coolify host IP, then redeploy to trigger Let's Encrypt cert issuance.
```
---
## 12. General "make this premium" polish
```
Inspect {{COMPONENT_OR_PAGE}}. Identify what doesn't match the premium dark/gold luxury direction in .claude/skills/premium-frontend-designer/DESIGN_SYSTEM.md.
Specifically check:
- Are buttons using premium variants (gold gradient primary, outline secondary)?
- Are cards using the hover lift + amber glow + gold underline sweep pattern?
- Is there any cheap iconography or stock-photo placeholder?
- Are headings using the serif font with one gold-accent phrase?
- Is spacing centralized via Section primitive?
- Is motion subtle (no bounces, no scale > 1.05 on hover)?
Apply only the changes needed don't rewrite working sections. Return a list of refinements made.
```
@@ -0,0 +1,108 @@
# Responsive Checklist
Run through every item before declaring a page done.
## Viewports to verify
- **320 px** (iPhone SE 1st gen, smallest practical)
- **375 px** (iPhone SE 2/3, iPhone 13 mini)
- **390 px** (iPhone 14/15/16 default)
- **414 px** (iPhone Plus)
- **768 px** (iPad portrait, `md:` breakpoint)
- **1024 px** (iPad landscape / small laptop, `lg:` breakpoint)
- **1440 px** (full laptop)
## Global guards
- [ ] `body { overflow-x: hidden }` in `globals.css`.
- [ ] No `min-w-[...]` on root containers.
- [ ] All decorative absolute elements live inside `relative isolate overflow-hidden` parents.
- [ ] `max-w-*` containers always paired with `mx-auto px-6 md:px-10`.
## Header / Nav
- [ ] Logo left, menu/CTA right on mobile.
- [ ] Hamburger button is `size-10` minimum (touch target).
- [ ] Mobile menu drawer closes when a link is clicked.
- [ ] No sticky-nav overlap on anchored sections `scroll-mt-24 md:scroll-mt-28` on `<Section>`.
- [ ] Logo height responsive: `h-11 sm:h-14 md:h-16`.
## Hero
- [ ] H1 clamp: `clamp(2.4rem, 7vw, 6.4rem)` never larger upper bound.
- [ ] `leading-[1.0]` on serif H1 (not `0.95` clips ascenders).
- [ ] Top padding generous: `pt-32 sm:pt-36 md:pt-44` (clears sticky nav + breathing room).
- [ ] CTAs use `flex flex-wrap items-center gap-4` so they stack on narrow screens.
- [ ] Stats panel: collapse 1-col-tall mobile layouts to a horizontal 3-col compact row.
## Cards & grids
- [ ] Mobile: `grid-cols-1`. Tablet: `sm:grid-cols-2`. Desktop: `lg:grid-cols-N` matched to card count.
- [ ] Avoid `lg:grid-cols-3` for a 5-card list (leaves orphan) use 5 or 2-row layout matched to count.
- [ ] Aspect-ratio used over fixed heights: `aspect-[4/3]` or `aspect-[3/4]`.
## Typography
- [ ] No tracking > `0.22em` on small labels.
- [ ] `text-balance` on h1/h2.
- [ ] `text-pretty` on body paragraphs.
- [ ] Long tokens (cert numbers, emails) use `break-all` or `break-words` to wrap.
## Images
- [ ] `next/image` with `sizes` attribute matched to layout.
- [ ] Hero LCP image gets `priority`.
- [ ] `object-cover` only when crop is visually intentional.
- [ ] `object-contain` for logos.
- [ ] Alt text meaningful never "image" or filename.
## Forms
- [ ] Field grid: `grid gap-5 sm:grid-cols-2` for first/last + email/phone.
- [ ] Inputs `text-base` (16 px+) on mobile to prevent iOS zoom-on-focus.
- [ ] Submit button respects pending state w/ spinner.
- [ ] Field errors render below field, not in a popup.
- [ ] Success/error banner renders outside button.
## Motion
- [ ] `useReducedMotion()` checked in every component using motion props.
- [ ] Heavy halos / parallax skipped when reduced-motion is on.
- [ ] Marquee paused when reduced-motion is on (or replaced with grid).
## External links
- [ ] All `<a target="_blank">` carry `rel="noopener noreferrer"`.
- [ ] WhatsApp / Instagram / store badges have `aria-label`.
- [ ] App badges sized 132 px (footer) / 140-160 px (CTA).
## Footer
- [ ] No duplicated link labels across columns.
- [ ] Privacy Policy only in Legal column.
- [ ] Agency credit row sits below copyright, subtle.
- [ ] Watermark contained via `overflow-hidden` on footer root.
## Accessibility
- [ ] Every interactive element keyboard-reachable.
- [ ] `focus-visible:` rings on buttons, inputs, anchors.
- [ ] Modal/lightbox: scroll-locked body + `Escape` closes + arrows navigate.
- [ ] Color contrast checked (`text-bone` on obsidian = 16:1).
## Final smoke test
```bash
npx tsc --noEmit # clean
curl -s -o NUL -w "%{http_code}" http://localhost:3000/... # 200 on every route
```
Grep for placeholder leaks:
```bash
grep -rn 'href="#"' src/
grep -rn 'href=""' src/
grep -rn 'href="/#' src/
```
Should all return zero (unless intentional same-page anchors with matching IDs).
@@ -0,0 +1,143 @@
---
name: premium-frontend-designer
description: Build, redesign, and polish premium dark/gold luxury websites with React/Next.js/Tailwind. Use when the user asks to create a website, redesign a page, polish a section, fix mobile responsiveness, add premium components (hero, footer, CTA, client logos, certificates, app badges, WhatsApp button), or apply the Luxam-style luxury design DNA to another project.
---
# Premium Frontend Designer & Implementation Agent
You are a senior frontend architect + UI/UX designer + implementation engineer.
Your job: create production-ready, premium, cinematic, dark/gold luxury websites that feel high-end, trustworthy, and conversion-focused the way the Luxam reference site does.
This skill captures the design DNA, component patterns, prompt templates, and snippets so any future project can match the same quality bar.
---
## When to use this skill
Auto-trigger whenever the user:
- asks for a "premium", "luxury", "cinematic", "dark/gold", or "Luxam-style" design
- creates or redesigns a page (homepage, about, services, contact, gallery, certifications, purchase-sale)
- polishes typography, spacing, or motion
- fixes mobile responsiveness
- adds: WhatsApp link, app store buttons, client logos, certificate gallery, premium hero, premium footer
- imports brand contact details into a layout
- wants reusable React/Tailwind components in the Luxam style
Refuse silently if the project explicitly uses a different design language (e.g. bright corporate, neobrutalist, retro 8-bit). In that case ask before applying this skill's DNA.
---
## Visual principles
1. **Dark cinematic background** obsidian-class blacks (`#08080A`), graphite layering (`#15161A` / `#1C1D22`). Never pure white. Never bright primary colors as backgrounds.
2. **Gold/amber accents** restrained, structural, single-tone gradient. Use on numbers, labels, dividers, hover states, hero highlights. Never bright yellow. Never multi-color rainbow gradients.
3. **Editorial typography pairing**
- Serif (Cormorant Garamond / Playfair / Libre Baskerville) for hero H1 + section H2 only.
- Sans (Geist / Inter / Manrope / Satoshi) for nav, body, buttons, labels, metadata.
- No monospace on user-facing copy or long values.
4. **Glass + grain** semi-transparent `bg-bone/[0.03]` cards with `backdrop-blur`. Subtle noise/grain overlays to break flat surfaces.
5. **Cinematic motion** Framer Motion: slow, restrained. `cubic-bezier(0.16, 1, 0.3, 1)`, 0.8-1.2s entrance, `useReducedMotion()` respected.
6. **Responsive from the start** every layout works at 320 / 375 / 768 / 1024 / 1440 viewports.
7. **No corporate basic** no boxy stock layouts, no flat colorful cards, no generic stock-photo hero, no cheap icon dumps.
Read `DESIGN_SYSTEM.md` for the full token palette + typography clamps.
---
## Coding principles
1. **Inspect before changing.** Always read the existing files, lockfile, framework version, and existing primitives. Reuse don't duplicate.
2. **Preserve project identity.** Don't rewrite the whole brand for a small request.
3. **Real content only.** Never invent links, social handles, certifications, addresses, phone numbers, or legal claims. Ask if missing.
4. **External links** carry `target="_blank"` + `rel="noopener noreferrer"`.
5. **Reusable primitives first.** Look for existing `Section`, `Container`, `Button`, `Reveal`, `SectionLabel`, `GlassCard` before writing new ones.
6. **Clean imports.** Remove unused icons / helpers after edits.
7. **One source of truth** for spacing push to a shared `Section` primitive instead of overriding per page.
8. **Typecheck after every change.** `npx tsc --noEmit` must pass.
9. **No backwards-compat shims.** Don't leave `// removed` comments, dead aliases, or "for future" stubs.
10. **No commented-out code** in committed work.
11. **No emojis** in code or files unless user asks.
Read `COMPONENT_RULES.md` for per-element rules (buttons, cards, footer, forms, icons).
---
## Accessibility requirements
- All anchors have meaningful text or `aria-label`.
- All interactive elements reachable by keyboard.
- Focus-visible states: `focus-visible:border-gold/60 focus-visible:ring-2 focus-visible:ring-gold/40`.
- Modal/lightbox: `role="dialog" aria-modal="true"`, `Escape` closes, scroll-lock on body.
- Decorative SVGs: `aria-hidden`.
- Color contrast: body text `text-bone` (≈ #F4F0E6) on obsidian = 16:1 ratio, well above WCAG AA.
- `prefers-reduced-motion` respected via Framer's `useReducedMotion()` disable transforms/parallax, keep opacity fades.
---
## Responsive requirements
Test mentally (or in browser) at: **320, 375, 390, 414, 768, 1024, 1440**.
- Hero h1 clamp: `clamp(2.4rem, 7vw, 6.4rem)` with `leading-[1.0]`.
- Section padding: `py-16 md:py-20 lg:py-24` (uniform).
- Anchored sections: `scroll-mt-24 md:scroll-mt-28` for sticky-nav clearance.
- `body { overflow-x: hidden }` in `globals.css` as a global guard.
- Sticky decorations (halos, watermarks, blurs) must not introduce horizontal scroll use `overflow-hidden` on parent + `translate` instead of fixed widths.
- Mobile cards stack `grid-cols-1 sm:grid-cols-2 lg:grid-cols-N`.
Read `RESPONSIVE_CHECKLIST.md` before shipping any page.
---
## Output requirements
Every meaningful change returns:
1. **Files changed** explicit list with `NEW` / `MODIFIED` / `DELETED` markers.
2. **What was kept** vs **what was replaced** when redesigning.
3. **Receipts** `HTTP 200`, typecheck clean, key content rendered (grep output).
4. **Responsive breakdown** desktop / tablet / mobile behavior.
5. **Confirmations** explicit yes/no on every checklist item the user asked.
6. **No placeholder leaks** confirm zero `href="#"`, `href=""`, `href="/#"` remain in modified files.
---
## QA checklist (run before reporting done)
```
[ ] npx tsc --noEmit clean
[ ] dev server HTTP 200 on every modified route
[ ] grep zero placeholder hrefs in modified files
[ ] grep zero hardcoded brand values that should be in lib/content.ts
[ ] mobile (<640px) no horizontal overflow, hero not clipped, CTAs not cut
[ ] tablet (≥640 <1024) grids transition cleanly to 2-col
[ ] desktop (≥1024) spacing balanced, no orphan cards
[ ] external links target="_blank" + rel="noopener noreferrer"
[ ] focus-visible visible on every button/anchor
[ ] reduced-motion heavy parallax/scale animations disabled
[ ] no leftover console.log or commented-out code
```
---
## Brand-specific config (Luxam reference)
`examples/luxam-config.md` holds the exact Luxam contact details, WhatsApp URL, app store links, agency credit, etc. **Do not copy these values into other projects** use them only as the shape pattern. Each new project gets its own `lib/content.ts` populated from real values the user provides.
---
## File map of this skill
```
SKILL.md this file (entry point, principles, when to use)
DESIGN_SYSTEM.md color tokens, typography, spacing scale, motion
COMPONENT_RULES.md per-component rules (buttons, cards, footer, forms, icons)
RESPONSIVE_CHECKLIST.md pre-ship checklist for every viewport
PROMPT_TEMPLATES.md copy-paste prompts for common tasks
COMPONENT_SNIPPETS.md high-level component overview
snippets/ 8 reusable TSX component files
examples/luxam-config.md Luxam-specific values (reference, not boilerplate)
```
Read the relevant section(s) for the task at hand. Don't dump the whole skill into the user's reply surface only the rules + snippets relevant to the change being made.
@@ -0,0 +1,118 @@
# Luxam Reference Config
Concrete values used in the Luxam project. **Reference only** do not paste into other projects unless the user explicitly says these are also their values.
## `src/lib/content.ts`
```ts
export const brand = {
name: "LUXAM",
legalName: "Luxam Oils Trading LLC",
tagline: "Sustainable Energy. Refined.",
promise: "Converting waste into the fuel of tomorrow.",
phone: "04 262 3314",
email: "info@luxam.ae",
location: "United Arab Emirates",
instagram: "@luxamtrading",
};
export const nav = [
{ label: "Home", href: "/" },
{ label: "About us", href: "/about-us" },
{ label: "Services", href: "/services" },
{ label: "Purchase & Sale", href: "/purchase-sale" },
{ label: "Gallery", href: "/gallery" },
{ label: "Certifications", href: "/certifications" },
{ label: "Contact us", href: "/contact-us" },
];
export const socials = {
instagram: "https://www.instagram.com/luxamtrading/",
whatsapp:
"https://api.whatsapp.com/send/?phone=971527312190&text&type=phone_number&app_absent=0",
whatsappDisplay: "+971 52 731 2190",
};
export const app = {
googlePlay: "https://play.google.com/store/apps/details?id=com.lootah.luxam",
appStore: "https://apps.apple.com/ae/app/luxam/id6740183015",
};
export const credit = {
agency: "YS Lootah Tech",
url: "https://yslootahtech.com/",
};
```
## Footer link cluster
```ts
const navigationLinks = [
{ label: "Home", href: "/" },
{ label: "About us", href: "/about-us" },
{ label: "Services", href: "/services" },
{ label: "Purchase & Sale", href: "/purchase-sale" },
{ label: "Gallery", href: "/gallery" },
{ label: "Certifications", href: "/certifications" },
{ label: "Contact us", href: "/contact-us" },
];
const serviceItems = [
{ label: "UCO Collection", href: "/services" },
{ label: "Logistics & Handling", href: "/services" },
{ label: "Recycling & Feedstock", href: "/services" },
{ label: "Compliance & Documentation", href: "/certifications" },
{ label: "Partner with us", href: "/contact-us" },
];
const legalLinks = [{ label: "Privacy Policy", href: "/privacy-policy" }];
```
## Deployment (Coolify)
```
Coolify project luxam (uuid h0840c84kgo0g4cgwg0os88c)
env production uuid s4ow44cwgo08g88g04gwkkcs
Application uuid zc4s4k4k0c44c8cs8oc8so4k
build_pack dockerfile
dockerfile_location /Dockerfile
ports_exposes 3000
fqdn https://luxamnew.yslootahtech.com
DNS A record luxamnew → 51.255.193.254 (DNS only)
```
Git remote:
```
origin https://git.devxsupport.com/mohammad/luxam-web.git (main)
```
Token policy: never commit. Use `COOLIFY_TOKEN` env var when scripting deploys.
## Dockerfile pattern
Multi-stage:
1. `deps` (node:22-alpine, `apk add libc6-compat`, `npm ci`)
2. `build` (`next build` w/ `output: "standalone"`)
3. `run` (non-root UID 1001, copies `.next/standalone` + `.next/static` + `public`, `CMD ["node","server.js"]`)
`.dockerignore` skips `.git`, `node_modules`, `.next/cache`, `.env*`, `.claude`.
## ISCC certificate facts (Luxam-specific)
Use only as a precedent for how to surface certificate metadata. Do not copy into other projects.
```
Certificate number EU-ISCC-Cert-DE100-27002125
Standard ISCC EU
Framework RED III (Directive (EU) 2023/2413)
Issuer SGS Germany GmbH (Europa Allee 12, D-49685 Emstek, Germany)
Certified entity Luxam Oils Trading LLC
Site Al Ttay – Al Khawaneej, Dubai, UAE
Scope Collecting point, warehouse
Material UCO (entirely of vegetable origin)
Valid from 11 August 2025
Valid to 10 August 2026
```
For any new project that surfaces compliance docs, follow the same data shape: number, standard, issuer, certified entity, site, scope, material, validity dates. Pull only from official documents never invent.
@@ -0,0 +1,71 @@
/**
* Official App Store + Google Play badge anchors.
*
* Setup steps:
* 1. Download official SVGs (use the /show/ path which returns clean SVG):
* https://www.svgrepo.com/show/303128/download-on-the-app-store-apple-logo.svg
* https://www.svgrepo.com/show/303139/google-play-badge-logo.svg
* 2. Save to public/images/app-store-badge.svg + public/images/google-play-badge.svg.
* 3. Add to lib/content.ts:
* export const app = {
* googlePlay: "https://play.google.com/store/apps/details?id=...",
* appStore: "https://apps.apple.com/ae/app/.../id...",
* };
*
* next/image blocks raw SVG by default in Next 16 keep plain <img>.
*/
import { app } from "@/lib/content";
type Props = {
store: "appstore" | "googleplay";
small?: boolean;
};
export function AppBadge({ store, small }: Props) {
const isApple = store === "appstore";
const href = isApple ? app.appStore : app.googlePlay;
const src = isApple
? "/images/app-store-badge.svg"
: "/images/google-play-badge.svg";
const alt = isApple ? "Download on the App Store" : "Get it on Google Play";
const label = isApple
? "Download on the App Store"
: "Get it on Google Play";
const width = small ? 132 : 160;
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={label}
className="group inline-flex transition duration-300 hover:-translate-y-0.5 hover:opacity-85"
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={src}
alt={alt}
width={width}
height={small ? 40 : 48}
className={
small
? "h-auto w-[132px] max-w-full select-none"
: "h-auto w-[140px] max-w-full select-none sm:w-[160px]"
}
loading="lazy"
decoding="async"
draggable={false}
/>
</a>
);
}
/** Both badges in a vertical stack. Use in footer Legal column. */
export function AppBadgeStack({ small }: { small?: boolean }) {
return (
<div className="flex flex-col items-start gap-2">
<AppBadge store="appstore" small={small} />
<AppBadge store="googleplay" small={small} />
</div>
);
}
@@ -0,0 +1,125 @@
"use client";
/**
* 4-image certificate gallery each tile links to a PDF in a new tab.
*
* Expected data shape:
* export type CertPage = {
* src: string; // /images/certifications/page.jpg
* alt: string;
* title: string; // ISCC Certificate 2025
* page: string; // Page 1 · Annex
* document: string; // ISCC Certificate
* href: string; // /documents/certifications/iscc.pdf OR upstream URL
* };
* export const certificatePages: CertPage[] = [ ... ];
*/
import Image from "next/image";
import { ArrowUpRight, FileText } from "lucide-react";
import { motion, type Variants } from "framer-motion";
export type CertPage = {
src: string;
alt: string;
title: string;
page: string;
document: string;
href: string;
};
const grid: Variants = {
hidden: {},
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.15 } },
};
const cell: Variants = {
hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] },
},
};
export function CertificateGallery({ pages }: { pages: CertPage[] }) {
return (
<motion.div
variants={grid}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4 lg:gap-6"
>
{pages.map((p) => (
<motion.div key={p.src} variants={cell}>
<CertCard {...p} />
</motion.div>
))}
</motion.div>
);
}
function CertCard({ src, alt, title, page, document, href }: CertPage) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={`${title} ${page}. Opens PDF in a new tab.`}
className="group relative isolate flex h-full flex-col overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/35 hover:bg-graphite/60 focus:outline-none focus-visible:border-gold/60 focus-visible:ring-2 focus-visible:ring-gold/40"
>
<div className="relative aspect-[3/4] w-full overflow-hidden bg-obsidian">
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
className="object-cover object-top transition-transform duration-[1200ms] ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:scale-[1.04]"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 h-20 bg-gradient-to-b from-obsidian/70 via-obsidian/20 to-transparent"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-obsidian/80 via-obsidian/30 to-transparent"
/>
<span className="absolute left-3 top-3 inline-flex items-center gap-2 rounded-full border border-gold/30 bg-obsidian/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.22em] text-gold-light backdrop-blur">
<span className="size-1 rounded-full bg-gold-light shadow-[0_0_8px_rgba(242,194,91,0.8)]" />
{page}
</span>
<span className="absolute right-3 top-3 grid size-9 place-items-center rounded-full border border-bone/20 bg-obsidian/70 text-mist backdrop-blur transition-all duration-500 group-hover:-translate-y-0.5 group-hover:border-gold/50 group-hover:text-gold-light">
<ArrowUpRight className="size-4" />
</span>
<div
aria-hidden
className="pointer-events-none absolute -bottom-16 -right-16 size-48 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100"
style={{
background:
"radial-gradient(circle, rgba(212,164,55,0.32), transparent 70%)",
}}
/>
<span
aria-hidden
className="pointer-events-none absolute inset-0 rounded-t-2xl border border-transparent transition-colors duration-500 group-hover:border-gold/40"
/>
</div>
<div className="relative flex flex-col gap-2 p-5 md:p-6">
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-gold-light">
{document}
</span>
<h4 className="font-display text-lg text-bone text-balance transition-colors duration-500 group-hover:text-gold-light md:text-xl">
{title}
</h4>
<div className="mt-1 inline-flex items-center gap-2 text-xs text-mist">
<FileText className="size-3.5 text-gold-light" />
<span>Open PDF</span>
</div>
</div>
<span
aria-hidden
className="pointer-events-none absolute bottom-0 left-0 h-px w-0 bg-gradient-to-r from-gold via-gold-light to-transparent transition-all duration-700 group-hover:w-full"
/>
</a>
);
}
@@ -0,0 +1,132 @@
"use client";
/**
* Premium horizontal logo marquee.
*
* Setup:
* 1. globals.css must have:
* @keyframes marquee {
* 0% { transform: translateX(0); }
* 100% { transform: translateX(-50%); }
* }
* 2. Pass clients prop or create lib/clients.ts:
* export const clients = [
* { src: "/images/clients/brand-a.png", alt: "Brand A" },
* ...
* ];
*/
import Image from "next/image";
import { useReducedMotion } from "framer-motion";
export type Client = { src: string; alt: string };
export function ClientLogoMarquee({
clients,
eyebrow = "Our Clients",
title,
subtitle,
}: {
clients: Client[];
eyebrow?: string;
title: React.ReactNode;
subtitle?: React.ReactNode;
}) {
const reduce = useReducedMotion();
const loop = [...clients, ...clients];
return (
<section className="relative overflow-hidden py-16 md:py-20 lg:py-24">
<div
aria-hidden
className="pointer-events-none absolute -left-32 top-1/3 size-[480px] rounded-full radial-gold blur-3xl opacity-30"
/>
<div className="mx-auto max-w-7xl px-6 md:px-10">
<div className="flex flex-col items-center gap-6 text-center">
<div className="flex items-center gap-3 text-[11px] font-medium uppercase tracking-[0.22em] text-mist">
<span className="h-px w-8 bg-gold/40" />
<span>{eyebrow}</span>
</div>
<h2 className="font-display text-4xl leading-[1.04] text-bone text-balance md:text-5xl">
{title}
</h2>
{subtitle ? (
<p className="max-w-2xl text-pretty text-mist md:text-lg">{subtitle}</p>
) : null}
</div>
<div className="relative mt-12 md:mt-14">
{/* edge fades */}
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r from-obsidian to-transparent md:w-32"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l from-obsidian to-transparent md:w-32"
/>
<div
className="flex overflow-hidden"
style={{
maskImage:
"linear-gradient(90deg, transparent, black 8%, black 92%, transparent)",
}}
>
{reduce ? (
<ClientGrid clients={clients} />
) : (
<div
className="flex shrink-0 gap-4 pr-4 md:gap-6 md:pr-6"
style={{
animation: "marquee 50s linear infinite",
animationPlayState: "running",
}}
onMouseEnter={(e) =>
(e.currentTarget.style.animationPlayState = "paused")
}
onMouseLeave={(e) =>
(e.currentTarget.style.animationPlayState = "running")
}
>
{loop.map((c, i) => (
<LogoTile key={`${c.src}-${i}`} client={c} />
))}
</div>
)}
</div>
</div>
</div>
</section>
);
}
function LogoTile({ client }: { client: Client }) {
return (
<div className="group/tile relative grid h-24 w-44 shrink-0 place-items-center overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 px-5 backdrop-blur-sm transition-all duration-500 hover:border-gold/30 hover:bg-graphite/60 md:h-28 md:w-52">
<div
aria-hidden
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-700 group-hover/tile:opacity-100"
style={{
background:
"radial-gradient(60% 80% at 50% 50%, rgba(212,164,55,0.18), transparent 70%)",
}}
/>
<Image
src={client.src}
alt={client.alt}
width={200}
height={100}
className="relative max-h-14 w-auto object-contain opacity-70 brightness-[1.05] contrast-[0.9] grayscale transition-all duration-500 group-hover/tile:scale-105 group-hover/tile:opacity-100 group-hover/tile:grayscale-0 md:max-h-16"
/>
</div>
);
}
function ClientGrid({ clients }: { clients: Client[] }) {
return (
<div className="grid w-full grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
{clients.map((c) => (
<LogoTile key={c.src} client={c} />
))}
</div>
);
}
@@ -0,0 +1,188 @@
/**
* Premium 4-column footer w/ no duplicated nav links.
*
* Expected lib/content.ts shape:
*
* export const brand = {
* name: "BRAND",
* legalName: "Brand Legal LLC",
* phone: "00 000 0000",
* email: "info@example.com",
* instagram: "@brand_handle",
* };
* export const socials = {
* instagram: "https://www.instagram.com/brand_handle/",
* whatsapp: "https://api.whatsapp.com/send/?phone=000000000000&text&type=phone_number&app_absent=0",
* whatsappDisplay: "+00 00 000 0000",
* };
* export const credit = {
* agency: "YS Lootah Tech",
* url: "https://yslootahtech.com/",
* };
*/
import { Mail, Phone } from "lucide-react";
import { brand, socials, credit } from "@/lib/content";
import { WhatsAppLink } from "./WhatsAppLink";
import { AppBadge } from "./AppBadges";
function InstagramIcon({ className }: { className?: string }) {
return (
<svg
aria-hidden
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
strokeLinejoin="round"
className={className}
>
<rect x="3" y="3" width="18" height="18" rx="5" />
<circle cx="12" cy="12" r="4" />
<circle cx="17.5" cy="6.5" r="1.1" fill="currentColor" stroke="none" />
</svg>
);
}
type Link = { label: string; href: string };
type Props = {
navigationLinks: Link[];
serviceItems: Link[];
legalLinks: Link[];
/** Brand logo node (provide your own LogoMark) */
logo?: React.ReactNode;
};
export function Footer({ navigationLinks, serviceItems, legalLinks, logo }: Props) {
return (
<footer className="relative overflow-hidden border-t border-bone/10 bg-obsidian pb-8 pt-11 md:pb-10 md:pt-14 lg:pb-12 lg:pt-[72px]">
<div className="relative z-10 mx-auto max-w-[1480px] px-6 md:px-10">
<div className="grid gap-10 lg:grid-cols-12 lg:gap-12">
{/* Col 1 Brand + contact */}
<div className="space-y-5 lg:col-span-4">
<a href="/" className="inline-flex items-center gap-4">
{logo}
<span className="hidden font-display text-2xl tracking-[0.22em] text-bone sm:inline">
{brand.name}
</span>
</a>
<p className="max-w-md text-sm text-mist text-pretty md:text-base">
{brand.legalName}.
</p>
<div className="metallic-divider" />
<div className="space-y-2 text-sm text-mist">
<a
href={`tel:${brand.phone.replace(/\s/g, "")}`}
className="flex items-center gap-3 hover:text-gold-light"
>
<Phone className="size-4" />
{brand.phone}
</a>
<a
href={`mailto:${brand.email}`}
className="flex items-center gap-3 hover:text-gold-light"
>
<Mail className="size-4" />
{brand.email}
</a>
<WhatsAppLink
href={socials.whatsapp}
display={socials.whatsappDisplay}
ariaLabel={`Chat with ${brand.name} on WhatsApp`}
inline
/>
<a
href={socials.instagram}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-3 hover:text-gold-light"
>
<InstagramIcon className="size-4" />
Instagram · {brand.instagram}
</a>
</div>
</div>
{/* Cols 2-4 */}
<div className="grid gap-8 sm:grid-cols-3 lg:col-span-8 lg:gap-10">
<FooterCol title="Navigation" links={navigationLinks} />
<FooterCol title="Services" links={serviceItems} />
<div>
<h4 className="text-[10px] font-medium uppercase tracking-[0.22em] text-gold-light">
Legal &amp; App
</h4>
<ul className="mt-4 space-y-2.5">
{legalLinks.map((l) => (
<li key={l.label}>
<a
href={l.href}
className="text-sm text-mist transition-colors hover:text-bone"
>
{l.label}
</a>
</li>
))}
</ul>
<div className="mt-5 flex flex-col items-start gap-2">
<AppBadge store="appstore" small />
<AppBadge store="googleplay" small />
</div>
</div>
</div>
</div>
<div className="mt-9 flex flex-col items-start justify-between gap-3 border-t border-bone/10 pt-5 text-xs uppercase tracking-[0.22em] text-mist md:mt-10 md:flex-row md:items-center md:gap-6">
<span>
© {new Date().getFullYear()} {brand.legalName}. All rights reserved.
</span>
</div>
<div className="mt-3 text-[11px] tracking-[0.04em] text-mist/60 md:text-xs">
Website designed &amp; developed by{" "}
<a
href={credit.url}
target="_blank"
rel="noopener noreferrer"
className="text-mist/80 underline decoration-gold/30 decoration-1 underline-offset-4 transition-colors duration-500 hover:text-gold-light hover:decoration-gold/70"
>
{credit.agency}
</a>
.
</div>
</div>
{/* Watermark contained inside footer */}
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex select-none justify-center overflow-hidden"
>
<span className="translate-y-[18%] font-display text-[14vw] leading-none tracking-[-0.02em] text-bone/[0.025] md:text-[12vw]">
{brand.name}
</span>
</div>
</footer>
);
}
function FooterCol({ title, links }: { title: string; links: Link[] }) {
return (
<div>
<h4 className="text-[10px] font-medium uppercase tracking-[0.22em] text-gold-light">
{title}
</h4>
<ul className="mt-4 space-y-2.5">
{links.map((l) => (
<li key={l.label}>
<a
href={l.href}
className="text-sm text-mist transition-colors hover:text-bone"
>
{l.label}
</a>
</li>
))}
</ul>
</div>
);
}
@@ -0,0 +1,75 @@
"use client";
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
type Props = {
Icon?: LucideIcon;
title: ReactNode;
body?: ReactNode;
href?: string;
external?: boolean;
className?: string;
/** Append custom inner content below the body */
children?: ReactNode;
};
export function GlassCard({
Icon,
title,
body,
href,
external,
className,
children,
}: Props) {
const inner = (
<>
{/* hover amber radial glow */}
<div
aria-hidden
className="pointer-events-none absolute -bottom-16 -right-16 size-48 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100"
style={{
background:
"radial-gradient(circle, rgba(212,164,55,0.28), transparent 70%)",
}}
/>
{Icon ? (
<div className="grid size-11 place-items-center rounded-full glass-gold transition-all duration-500 group-hover:rotate-[8deg] group-hover:shadow-[0_0_22px_-6px_rgba(212,164,55,0.6)]">
<Icon className="size-[18px] text-gold-light" strokeWidth={1.4} />
</div>
) : null}
<h4 className="mt-5 font-display text-xl text-bone transition-colors duration-500 group-hover:text-gold-light md:text-2xl">
{title}
</h4>
{body ? (
<p className="mt-2 text-sm text-mist text-pretty leading-relaxed">
{body}
</p>
) : null}
{children}
{/* gold underline sweep */}
<span
aria-hidden
className="pointer-events-none absolute bottom-0 left-0 h-px w-0 bg-gradient-to-r from-gold via-gold-light to-transparent transition-all duration-700 group-hover:w-full"
/>
</>
);
const base = `group relative isolate h-full overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 p-6 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/30 hover:bg-graphite/60 md:p-7 ${className ?? ""}`;
if (href) {
return (
<a
href={href}
{...(external
? { target: "_blank", rel: "noopener noreferrer" }
: {})}
className={base}
>
{inner}
</a>
);
}
return <div className={base}>{inner}</div>;
}
@@ -0,0 +1,40 @@
"use client";
import { cn } from "@/lib/cn";
import { ArrowUpRight } from "lucide-react";
import type { ComponentPropsWithoutRef } from "react";
type Variant = "primary" | "ghost" | "outline";
type Props = ComponentPropsWithoutRef<"a"> & {
variant?: Variant;
arrow?: boolean;
};
const base =
"relative inline-flex items-center gap-2 rounded-full px-7 py-3.5 text-sm font-medium tracking-wide transition-all duration-500";
const variants: Record<Variant, string> = {
primary:
"bg-gradient-to-r from-gold-light via-gold to-gold-deep text-obsidian shadow-[0_10px_40px_-12px_rgba(212,164,55,0.6)] hover:shadow-[0_18px_50px_-12px_rgba(242,194,91,0.8)]",
outline:
"border border-bone/20 text-bone hover:border-gold/60 hover:text-gold-light",
ghost: "text-bone hover:text-gold-light",
};
export function PremiumButton({
variant = "primary",
className,
children,
arrow = true,
...rest
}: Props) {
return (
<a className={cn(base, variants[variant], className)} {...rest}>
<span>{children}</span>
{arrow ? (
<ArrowUpRight className="size-4 transition-transform duration-500 group-hover:translate-x-0.5" />
) : null}
</a>
);
}
@@ -0,0 +1,76 @@
"use client";
import { cn } from "@/lib/cn";
import { motion, useReducedMotion, type Variants } from "framer-motion";
import type { ReactNode } from "react";
type Props = {
eyebrow?: string;
index?: string;
title: ReactNode;
/** The gold-accent phrase rendered inside the title pass as JSX or separate prop. */
accent?: ReactNode;
subtitle?: ReactNode;
align?: "left" | "center";
className?: string;
};
export function SectionHeading({
eyebrow,
index,
title,
accent,
subtitle,
align = "left",
className,
}: Props) {
const reduce = useReducedMotion();
const v: Variants = {
hidden: { opacity: 0, y: reduce ? 0 : 24, filter: reduce ? "blur(0px)" : "blur(6px)" },
visible: {
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: { duration: 0.9, ease: [0.16, 1, 0.3, 1] },
},
};
return (
<div
className={cn(
"flex flex-col gap-6",
align === "center" ? "items-center text-center" : "items-start",
className
)}
>
{eyebrow ? (
<div className="flex items-center gap-3 text-[11px] font-medium uppercase tracking-[0.22em] text-mist">
{index ? <span className="text-gold/85">{index}</span> : null}
<span className="h-px w-8 bg-gold/40" />
<span className="text-mist/85">{eyebrow}</span>
</div>
) : null}
<motion.h2
variants={v}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="font-display text-[clamp(2.4rem,5vw,4.6rem)] leading-[1.02] tracking-[-0.02em] text-bone text-balance"
>
{title}
{accent ? <> <span className="gold-text">{accent}</span></> : null}
</motion.h2>
{subtitle ? (
<motion.p
variants={v}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
transition={{ delay: 0.1 }}
className="max-w-2xl text-pretty text-mist md:text-lg"
>
{subtitle}
</motion.p>
) : null}
</div>
);
}
@@ -0,0 +1,88 @@
/**
* Refined gold WhatsApp link.
*
* Use 3 surfaces:
* - <WhatsAppLink href={...} display="+CC XX XXX XXXX" ariaLabel="..." inline /> (footer row)
* - <WhatsAppIconButton href={...} ariaLabel="..." /> (nav)
* - <WhatsAppLink href={...} display={...} ariaLabel={...} /> (contact card)
*
* URL format expected: https://api.whatsapp.com/send/?phone=COUNTRYCODE+DIGITS&text&type=phone_number&app_absent=0
* Phone in URL = digits only (no + or spaces). Display string = formatted "+CC XX XXX XXXX".
*
* lucide-react has no WhatsApp glyph → use this inline SVG.
*/
import type { SVGProps } from "react";
export function WhatsAppIcon({ className, ...rest }: SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden
className={className}
{...rest}
>
<path d="M19.11 4.86A9.94 9.94 0 0 0 12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.33 4.95L2 22l5.27-1.38a9.93 9.93 0 0 0 4.76 1.21h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.84-7.06ZM12.04 20.15c-1.5 0-2.96-.4-4.24-1.17l-.3-.18-3.13.82.83-3.05-.2-.31a8.21 8.21 0 0 1-1.26-4.35c0-4.53 3.69-8.22 8.22-8.22 2.2 0 4.27.86 5.82 2.41a8.16 8.16 0 0 1 2.41 5.81c0 4.53-3.69 8.24-8.15 8.24Zm4.72-6.16c-.26-.13-1.53-.75-1.76-.83-.24-.09-.41-.13-.58.13-.17.26-.66.83-.81 1-.15.17-.3.2-.55.07-.26-.13-1.1-.41-2.1-1.29-.78-.69-1.3-1.55-1.45-1.81-.15-.26-.02-.4.11-.53.11-.11.26-.3.39-.45.13-.15.17-.26.26-.43.09-.17.04-.32-.02-.45-.07-.13-.58-1.4-.79-1.92-.21-.5-.42-.43-.58-.44-.15-.01-.32-.01-.49-.01-.17 0-.45.06-.69.32-.24.26-.9.88-.9 2.15 0 1.27.92 2.5 1.05 2.67.13.17 1.82 2.78 4.42 3.9.62.27 1.1.43 1.48.55.62.2 1.18.17 1.63.1.5-.07 1.53-.62 1.74-1.23.21-.6.21-1.12.15-1.23-.06-.11-.23-.18-.49-.31Z" />
</svg>
);
}
type LinkProps = {
href: string;
display: string;
ariaLabel: string;
inline?: boolean; // true = footer row; false = larger contact-card row
};
export function WhatsAppLink({ href, display, ariaLabel, inline }: LinkProps) {
if (inline) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={ariaLabel}
className="flex items-center gap-3 text-sm text-mist hover:text-gold-light"
>
<WhatsAppIcon className="size-4" />
WhatsApp · {display}
</a>
);
}
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={ariaLabel}
className="group flex items-center gap-3 rounded-xl border border-bone/8 bg-bone/[0.03] p-3.5 transition-colors duration-500 hover:border-gold/30"
>
<span className="grid size-9 shrink-0 place-items-center rounded-full border border-gold/30 bg-gold/[0.08] text-gold-light shadow-[0_0_14px_-4px_rgba(212,164,55,0.6)]">
<WhatsAppIcon className="size-4" />
</span>
<span className="text-bone group-hover:text-gold-light">
WhatsApp · {display}
</span>
</a>
);
}
type ButtonProps = {
href: string;
ariaLabel: string;
className?: string;
};
export function WhatsAppIconButton({ href, ariaLabel, className }: ButtonProps) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={ariaLabel}
className={`group grid size-10 place-items-center rounded-full border border-bone/15 text-bone transition-all duration-500 hover:border-gold/40 hover:text-gold-light hover:shadow-[0_0_22px_-6px_rgba(212,164,55,0.6)] hover:scale-[1.04] ${className ?? ""}`}
>
<WhatsAppIcon className="size-[18px]" />
</a>
);
}
+2 -2
View File
@@ -3,12 +3,12 @@ node_modules
.git .git
.gitignore .gitignore
# Local env files — set these as environment variables in Coolify instead # Local env files set these as environment variables in Coolify instead
.env .env
.env.local .env.local
.env.*.local .env.*.local
# SQLite database files — mount /app/prisma as a persistent volume in Coolify # SQLite database files mount /app/prisma as a persistent volume in Coolify
prisma/*.db prisma/*.db
prisma/*.db-shm prisma/*.db-shm
prisma/*.db-wal prisma/*.db-wal
+2 -2
View File
@@ -1,5 +1,5 @@
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Stripe — https://dashboard.stripe.com/apikeys # Stripe https://dashboard.stripe.com/apikeys
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_... STRIPE_SECRET_KEY=sk_live_...
@@ -11,7 +11,7 @@ STRIPE_WEBHOOK_SECRET=whsec_...
ADMIN_JWT_SECRET=change_me_generate_with_openssl_rand_hex_32 ADMIN_JWT_SECRET=change_me_generate_with_openssl_rand_hex_32
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
# Database — SQLite via libsql # Database SQLite via libsql
# In Coolify: set to file:/app/prisma/lootah.db # In Coolify: set to file:/app/prisma/lootah.db
# and mount /app/prisma as a persistent volume # and mount /app/prisma as a persistent volume
# ───────────────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────────────
@@ -11,7 +11,7 @@ Add password authentication to the admin dashboard, enable full CRUD for persona
- Create `prisma/schema.prisma` with SQLite provider - Create `prisma/schema.prisma` with SQLite provider
- Models: `AdminUser` (id, username, passwordHash, createdAt), `AppSettings` (key, value) - Models: `AdminUser` (id, username, passwordHash, createdAt), `AppSettings` (key, value)
- Run `npx prisma db push` to create the database - Run `npx prisma db push` to create the database
- Create `src/lib/prisma.ts` — singleton Prisma client - Create `src/lib/prisma.ts` singleton Prisma client
2. **Create seed script** `prisma/seed.ts` 2. **Create seed script** `prisma/seed.ts`
- Creates default admin user with bcrypt-hashed password - Creates default admin user with bcrypt-hashed password
@@ -19,10 +19,10 @@ Add password authentication to the admin dashboard, enable full CRUD for persona
- Run with `npx prisma db seed` - Run with `npx prisma db seed`
3. **Create admin auth API routes** 3. **Create admin auth API routes**
- `src/app/api/admin/login/route.ts` — POST, accepts `{ username, password }`, verifies bcrypt hash from DB, returns JWT in httpOnly cookie (JWT secret from DB) - `src/app/api/admin/login/route.ts` POST, accepts `{ username, password }`, verifies bcrypt hash from DB, returns JWT in httpOnly cookie (JWT secret from DB)
- `src/app/api/admin/verify/route.ts` — GET, checks JWT cookie validity - `src/app/api/admin/verify/route.ts` GET, checks JWT cookie validity
- `src/app/api/admin/logout/route.ts` — POST, clears auth cookie - `src/app/api/admin/logout/route.ts` POST, clears auth cookie
- `src/app/api/admin/change-password/route.ts` — POST, accepts `{ currentPassword, newPassword }`, updates hash in DB - `src/app/api/admin/change-password/route.ts` POST, accepts `{ currentPassword, newPassword }`, updates hash in DB
4. **Create admin middleware** `src/middleware.ts` 4. **Create admin middleware** `src/middleware.ts`
- Protects all `/admin/*` routes (except `/admin/login/`) - Protects all `/admin/*` routes (except `/admin/login/`)
@@ -34,7 +34,7 @@ Add password authentication to the admin dashboard, enable full CRUD for persona
- Calls login API, redirects to `/admin/` on success - Calls login API, redirects to `/admin/` on success
- Shows error on wrong credentials - Shows error on wrong credentials
6. **Add JWT_SECRET to .env.local** (only this one — password is in DB) 6. **Add JWT_SECRET to .env.local** (only this one password is in DB)
- Alternative: store JWT secret in DB and load at startup into a module-level cache - Alternative: store JWT secret in DB and load at startup into a module-level cache
### Dependencies to install ### Dependencies to install
@@ -43,12 +43,12 @@ Add password authentication to the admin dashboard, enable full CRUD for persona
## Phase 2: Full CRUD for Pricing Items + Personas ## Phase 2: Full CRUD for Pricing Items + Personas
### Steps (depends on Phase 1) ### Steps (depends on Phase 1)
6. **Add/Remove pricing items in admin** — Update `src/app/admin/page.tsx` 6. **Add/Remove pricing items in admin** Update `src/app/admin/page.tsx`
- "Add Item" button with fields: id (auto-slug from label), label, price - "Add Item" button with fields: id (auto-slug from label), label, price
- Delete button per row (with confirmation) - Delete button per row (with confirmation)
- Update `usePricingStore` to support `addItem()` and `removeItem()` actions - Update `usePricingStore` to support `addItem()` and `removeItem()` actions
7. **Persona management section** — New section in admin page 7. **Persona management section** New section in admin page
- List current personas with edit capability (label, description, colors) - List current personas with edit capability (label, description, colors)
- Add new persona form - Add new persona form
- Delete persona button - Delete persona button
@@ -58,7 +58,7 @@ Add password authentication to the admin dashboard, enable full CRUD for persona
## Phase 3: Additional Admin Features ## Phase 3: Additional Admin Features
### Steps (parallel with Phase 2) ### Steps (parallel with Phase 2)
8. **Orders dashboard section** — New section in admin page 8. **Orders dashboard section** New section in admin page
- Show orders from Stripe API (list recent payments) - Show orders from Stripe API (list recent payments)
- Display: order ID, customer email, amount, status, date - Display: order ID, customer email, amount, status, date
- New API route `src/app/api/admin/orders/route.ts` to fetch from Stripe - New API route `src/app/api/admin/orders/route.ts` to fetch from Stripe
@@ -77,18 +77,18 @@ Add password authentication to the admin dashboard, enable full CRUD for persona
11. **Logout button** in admin header 11. **Logout button** in admin header
## Relevant Files ## Relevant Files
- `src/app/admin/page.tsx` — Main admin dashboard (add CRUD UI, analytics) - `src/app/admin/page.tsx` Main admin dashboard (add CRUD UI, analytics)
- `src/app/admin/login/page.tsx` — New login page - `src/app/admin/login/page.tsx` New login page
- `src/app/api/admin/login/route.ts` — New auth API - `src/app/api/admin/login/route.ts` New auth API
- `src/app/api/admin/verify/route.ts` — New verify API - `src/app/api/admin/verify/route.ts` New verify API
- `src/app/api/admin/orders/route.ts` — New orders API - `src/app/api/admin/orders/route.ts` New orders API
- `src/middleware.ts` — New, protects admin routes - `src/middleware.ts` New, protects admin routes
- `src/lib/prisma.ts` — New, Prisma client singleton - `src/lib/prisma.ts` New, Prisma client singleton
- `prisma/schema.prisma` — New, database schema (AdminUser, AppSettings) - `prisma/schema.prisma` New, database schema (AdminUser, AppSettings)
- `prisma/seed.ts` — New, seeds default admin user - `prisma/seed.ts` New, seeds default admin user
- `src/store/usePricingStore.ts` — Add `addItem()`, `removeItem()` actions - `src/store/usePricingStore.ts` Add `addItem()`, `removeItem()` actions
- `src/components/ConfigPanel.tsx` — Persona options currently hardcoded (PERSONA_OPTIONS const) - `src/components/ConfigPanel.tsx` Persona options currently hardcoded (PERSONA_OPTIONS const)
- `.env.local` — Add ADMIN_PASSWORD, ADMIN_JWT_SECRET - `.env.local` Add ADMIN_PASSWORD, ADMIN_JWT_SECRET
## Verification ## Verification
1. Visit `/admin/` without login → redirected to `/admin/login/` 1. Visit `/admin/` without login → redirected to `/admin/login/`
@@ -102,13 +102,13 @@ Add password authentication to the admin dashboard, enable full CRUD for persona
9. Run `npx vitest run` → all existing tests pass 9. Run `npx vitest run` → all existing tests pass
## Decisions ## Decisions
- **Database: Prisma + SQLite** — file-based, no external server needed, real ORM - **Database: Prisma + SQLite** file-based, no external server needed, real ORM
- Admin password stored as bcrypt hash in DB (not in env vars) - Admin password stored as bcrypt hash in DB (not in env vars)
- JWT secret stored in AppSettings table in DB - JWT secret stored in AppSettings table in DB
- JWT in httpOnly cookie — secure, no localStorage tokens - JWT in httpOnly cookie secure, no localStorage tokens
- jose library for JWT — lightweight, edge-compatible (works in Next.js middleware) - jose library for JWT lightweight, edge-compatible (works in Next.js middleware)
- Personas need to move from hardcoded array to store-driven — breaking change in ConfigPanel - Personas need to move from hardcoded array to store-driven breaking change in ConfigPanel
- Stripe orders fetched via Stripe API directly — no local DB needed for orders - Stripe orders fetched via Stripe API directly no local DB needed for orders
## Further Considerations ## Further Considerations
1. **Persona storage**: Currently hardcoded in ConfigPanel. Moving to a store means ConfigPanel reads from store dynamically. This is required for admin CRUD to work. **Recommended: create persona store with localStorage persistence (same pattern as pricing store)** 1. **Persona storage**: Currently hardcoded in ConfigPanel. Moving to a store means ConfigPanel reads from store dynamically. This is required for admin CRUD to work. **Recommended: create persona store with localStorage persistence (same pattern as pricing store)**
+4
View File
@@ -37,3 +37,7 @@ next-env.d.ts
.vscode .vscode
/src/generated/prisma /src/generated/prisma
# Local design references not part of project
/references
+66
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+72
View File
File diff suppressed because one or more lines are too long
+70
View File
File diff suppressed because one or more lines are too long
+11 -11
View File
@@ -1,9 +1,9 @@
# Changelog — Lootah Robotics G1 Configurator # Changelog Lootah Robotics G1 Configurator
> تاريخ التغييرات — 20 أبريل 2026 > تاريخ التغييرات 20 أبريل 2026
--- ---
## 2ff21c5 — perf: compress GLBs 75%, add Draco decoder, loading spinner for attire ## 2ff21c5 perf: compress GLBs 75%, add Draco decoder, loading spinner for attire
### المشكلة ### المشكلة
- الموبايلات القديمة كانت تعلّق لأوقات طويلة عند تحميل الأزياء الجديدة - الموبايلات القديمة كانت تعلّق لأوقات طويلة عند تحميل الأزياء الجديدة
@@ -25,7 +25,7 @@
--- ---
## b2a484f — fix: dynamic attire buttons in ScrollOverlays + mobile touch support ## b2a484f fix: dynamic attire buttons in ScrollOverlays + mobile touch support
### المشكلة ### المشكلة
- أزرار الأزياء في صفحة الـ Landing (Kandura, Vest, Suit) كانت مكتوبة بشكل ثابت (hardcoded) - أزرار الأزياء في صفحة الـ Landing (Kandura, Vest, Suit) كانت مكتوبة بشكل ثابت (hardcoded)
@@ -41,7 +41,7 @@
--- ---
## 320b77b — fix: contacts API - use ADMIN_JWT_SECRET env var ## 320b77b fix: contacts API - use ADMIN_JWT_SECRET env var
### المشكلة ### المشكلة
- صفحة Contacts في الأدمن كانت ترجع خطأ 500 - صفحة Contacts في الأدمن كانت ترجع خطأ 500
@@ -58,7 +58,7 @@
--- ---
## 25ffbf4 — feat: add favicon and app icons for PWA support ## 25ffbf4 feat: add favicon and app icons for PWA support
### التغييرات ### التغييرات
| الملف | التغيير | | الملف | التغيير |
@@ -73,7 +73,7 @@
--- ---
## e686d41 — fix: use configStore.getState().setPersonaAttire in ScrollOverlays ## e686d41 fix: use configStore.getState().setPersonaAttire in ScrollOverlays
### المشكلة ### المشكلة
- بناء Docker كان يفشل مع خطأ TypeScript: - بناء Docker كان يفشل مع خطأ TypeScript:
@@ -93,7 +93,7 @@
--- ---
## e159965 — feat: add GET endpoint to retrieve contact requests with admin authentication ## e159965 feat: add GET endpoint to retrieve contact requests with admin authentication
### التغييرات ### التغييرات
| الملف | التغيير | | الملف | التغيير |
@@ -114,10 +114,10 @@ STRIPE_SECRET_KEY= # للمدفوعات
### هيكل Stores ### هيكل Stores
| Store | الوصف | | Store | الوصف |
|---|---| |---|---|
| `configStore` (vanilla Zustand) | الألوان والزي النشط — يدعم `.getState()` | | `configStore` (vanilla Zustand) | الألوان والزي النشط يدعم `.getState()` |
| `useConfigStore` (React hook) | wrapper لـ `configStore` للاستخدام داخل components | | `useConfigStore` (React hook) | wrapper لـ `configStore` للاستخدام داخل components |
| `personaStore` (vanilla Zustand) | قائمة الأزياء — تُحمَّل من API عند التهيئة | | `personaStore` (vanilla Zustand) | قائمة الأزياء تُحمَّل من API عند التهيئة |
| `pricingStore` | أسعار العناصر — تُزامَن مع قاعدة البيانات | | `pricingStore` | أسعار العناصر تُزامَن مع قاعدة البيانات |
### تدفق الأزياء المرفوعة ### تدفق الأزياء المرفوعة
1. الأدمن يرفع `.glb` من لوحة التحكم 1. الأدمن يرفع `.glb` من لوحة التحكم
+1 -1
View File
@@ -15,7 +15,7 @@ ENV NEXT_TELEMETRY_DISABLED=1
# Generate Prisma client (reads prisma/schema.prisma + prisma.config.ts) # Generate Prisma client (reads prisma/schema.prisma + prisma.config.ts)
RUN npx prisma generate RUN npx prisma generate
# Build Next.js — produces .next/standalone (set in next.config.mjs) # Build Next.js produces .next/standalone (set in next.config.mjs)
RUN npm run build RUN npm run build
# ── Stage 3: production runner ───────────────────────────────────────────────── # ── Stage 3: production runner ─────────────────────────────────────────────────
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

+1 -1
View File
@@ -9,7 +9,7 @@ echo "→ Syncing database schema..."
# db push creates the SQLite file and syncs tables to match schema.prisma # db push creates the SQLite file and syncs tables to match schema.prisma
/app/node_modules/.bin/prisma db push /app/node_modules/.bin/prisma db push
echo "→ Seeding database (idempotent — skips existing records)..." echo "→ Seeding database (idempotent pricing rows are upserted every deploy)..."
/app/node_modules/.bin/tsx /app/prisma/seed.ts /app/node_modules/.bin/tsx /app/prisma/seed.ts
echo "→ Starting Next.js on port ${PORT:-3000}..." echo "→ Starting Next.js on port ${PORT:-3000}..."
Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

+7
View File
@@ -1,11 +1,18 @@
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
output: 'standalone', output: 'standalone',
outputFileTracingRoot: __dirname,
images: { images: {
unoptimized: true, unoptimized: true,
}, },
trailingSlash: true, trailingSlash: true,
reactStrictMode: true, reactStrictMode: true,
allowedDevOrigins: ['127.0.0.1', 'localhost', '10.255.254.66'],
}; };
export default nextConfig; export default nextConfig;
+104
View File
@@ -18,22 +18,27 @@
"@react-three/drei": "^10.7.7", "@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0", "@react-three/fiber": "^9.5.0",
"@react-three/postprocessing": "^2.16.0", "@react-three/postprocessing": "^2.16.0",
"@splinetool/react-spline": "^4.1.0",
"@splinetool/runtime": "^1.12.94",
"@stripe/react-stripe-js": "^6.1.0", "@stripe/react-stripe-js": "^6.1.0",
"@stripe/stripe-js": "^9.1.0", "@stripe/stripe-js": "^9.1.0",
"@types/three": "^0.183.1", "@types/three": "^0.183.1",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"clsx": "^2.1.1",
"draco3dgltf": "^1.5.7", "draco3dgltf": "^1.5.7",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"gsap": "^3.14.2", "gsap": "^3.14.2",
"i18next": "^26.0.3", "i18next": "^26.0.3",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"jose": "^6.2.2", "jose": "^6.2.2",
"lucide-react": "^1.16.0",
"next": "16.2.2", "next": "16.2.2",
"react": "19.0.0", "react": "19.0.0",
"react-country-phone-input": "^1.0.2", "react-country-phone-input": "^1.0.2",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-i18next": "^17.0.2", "react-i18next": "^17.0.2",
"stripe": "^22.0.1", "stripe": "^22.0.1",
"tailwind-merge": "^3.6.0",
"three": "^0.170.0", "three": "^0.170.0",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"zustand": "^5.0.12" "zustand": "^5.0.12"
@@ -2713,6 +2718,37 @@
"text-hex": "1.0.x" "text-hex": "1.0.x"
} }
}, },
"node_modules/@splinetool/react-spline": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@splinetool/react-spline/-/react-spline-4.1.0.tgz",
"integrity": "sha512-Y379gm17gw+1nxT/YXTCJnVIWuu7tsUH1tp/YxsYb0pZnc9Gljk7Om4Kpq7WPq0bZ4zidVCxf6xn6jgDcbHifQ==",
"dependencies": {
"blurhash": "2.0.5",
"lodash.debounce": "4.0.8",
"react-merge-refs": "2.1.1",
"thumbhash": "0.1.1"
},
"peerDependencies": {
"@splinetool/runtime": "*",
"next": ">=14.2.0",
"react": "*",
"react-dom": "*"
},
"peerDependenciesMeta": {
"next": {
"optional": true
}
}
},
"node_modules/@splinetool/runtime": {
"version": "1.12.94",
"resolved": "https://registry.npmjs.org/@splinetool/runtime/-/runtime-1.12.94.tgz",
"integrity": "sha512-Y49qppQkzVHhbeePpmdXBOeVOtgAcn8qObBujB8ePYnGYBxSAA2NCHo91pY1Sv1DCMXrowuTSAJexVeXf+t4Pw==",
"dependencies": {
"on-change": "4.0.0",
"semver-compare": "1.0.0"
}
},
"node_modules/@standard-schema/spec": { "node_modules/@standard-schema/spec": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -3714,6 +3750,12 @@
"require-from-string": "^2.0.2" "require-from-string": "^2.0.2"
} }
}, },
"node_modules/blurhash": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz",
"integrity": "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==",
"license": "MIT"
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
@@ -4022,6 +4064,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/color": { "node_modules/color": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
@@ -6043,6 +6094,15 @@
"url": "https://github.com/sponsors/wellwelwel" "url": "https://github.com/sponsors/wellwelwel"
} }
}, },
"node_modules/lucide-react": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
"integrity": "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/lz-string": { "node_modules/lz-string": {
"version": "1.5.0", "version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@@ -6515,6 +6575,18 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/on-change": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/on-change/-/on-change-4.0.0.tgz",
"integrity": "sha512-PTu7C9Jsz4b+sNMDpH0eZFTr7uxdOtoDWRnhaVNK50bgrrnW5nvbWI0jm5DG9qOoTnIhBzE9xoKVFPD9xgtbdg==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sindresorhus/on-change?sponsor=1"
}
},
"node_modules/one-time": { "node_modules/one-time": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
@@ -6947,6 +7019,16 @@
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
"node_modules/react-merge-refs": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/react-merge-refs/-/react-merge-refs-2.1.1.tgz",
"integrity": "sha512-jLQXJ/URln51zskhgppGJ2ub7b2WFKGq3cl3NYKtlHoTG+dN2q7EzWrn3hN3EgPsTMvpR9tpq5ijdp7YwFZkag==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/react-use-measure": { "node_modules/react-use-measure": {
"version": "2.1.7", "version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz", "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
@@ -7170,6 +7252,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/semver-compare": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
"integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
"license": "MIT"
},
"node_modules/seq-queue": { "node_modules/seq-queue": {
"version": "0.0.5", "version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
@@ -7713,6 +7801,16 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tailwind-merge": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
"integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.2.2", "version": "4.2.2",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz",
@@ -7779,6 +7877,12 @@
"integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==", "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/thumbhash": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/thumbhash/-/thumbhash-0.1.1.tgz",
"integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==",
"license": "MIT"
},
"node_modules/tinybench": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+6 -1
View File
@@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev --webpack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
@@ -21,22 +21,27 @@
"@react-three/drei": "^10.7.7", "@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0", "@react-three/fiber": "^9.5.0",
"@react-three/postprocessing": "^2.16.0", "@react-three/postprocessing": "^2.16.0",
"@splinetool/react-spline": "^4.1.0",
"@splinetool/runtime": "^1.12.94",
"@stripe/react-stripe-js": "^6.1.0", "@stripe/react-stripe-js": "^6.1.0",
"@stripe/stripe-js": "^9.1.0", "@stripe/stripe-js": "^9.1.0",
"@types/three": "^0.183.1", "@types/three": "^0.183.1",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"clsx": "^2.1.1",
"draco3dgltf": "^1.5.7", "draco3dgltf": "^1.5.7",
"framer-motion": "^12.38.0", "framer-motion": "^12.38.0",
"gsap": "^3.14.2", "gsap": "^3.14.2",
"i18next": "^26.0.3", "i18next": "^26.0.3",
"i18next-browser-languagedetector": "^8.2.1", "i18next-browser-languagedetector": "^8.2.1",
"jose": "^6.2.2", "jose": "^6.2.2",
"lucide-react": "^1.16.0",
"next": "16.2.2", "next": "16.2.2",
"react": "19.0.0", "react": "19.0.0",
"react-country-phone-input": "^1.0.2", "react-country-phone-input": "^1.0.2",
"react-dom": "19.0.0", "react-dom": "19.0.0",
"react-i18next": "^17.0.2", "react-i18next": "^17.0.2",
"stripe": "^22.0.1", "stripe": "^22.0.1",
"tailwind-merge": "^3.6.0",
"three": "^0.170.0", "three": "^0.170.0",
"tsx": "^4.19.2", "tsx": "^4.19.2",
"zustand": "^5.0.12" "zustand": "^5.0.12"
BIN
View File
Binary file not shown.
+20 -15
View File
@@ -47,25 +47,30 @@ async function main() {
console.log('✓ JWT secret already exists, skipping.'); console.log('✓ JWT secret already exists, skipping.');
} }
// Seed default pricing items (idempotent — only if table is empty) // Pricing items upserted on every deploy so the code is the source of truth.
const pricingCount = await prisma.pricingItem.count(); // Prices in AED. USD → AED at 3.6725 (CBUAE peg).
if (pricingCount === 0) { // Basic = Unitree G1 retail ($16k) + $5k markup = $21k ≈ 77,125 AED.
// EDU = $40k flat ≈ 146,900 AED.
// All persona attire = 5,000 AED each.
// Note: any prior admin-UI edits to these item prices/labels will be reset on each deploy.
const defaultItems = [ const defaultItems = [
{ id: 'base', label: 'G1 Robot Base', price: 250000, sortOrder: 0 }, { id: 'base', label: 'G1 Robot Basic', price: 77125, sortOrder: 0 },
{ id: 'emarati-kandura', label: 'Emarati Kandura', price: 15000, sortOrder: 1 }, { id: 'edu', label: 'G1 Robot EDU', price: 146900, sortOrder: 1 },
{ id: 'industrial-vest', label: 'Industrial Vest', price: 8500, sortOrder: 2 }, { id: 'emarati-kandura', label: 'Emarati Kandura', price: 5000, sortOrder: 2 },
{ id: 'business-suit', label: 'Business Suit', price: 12000, sortOrder: 3 }, { id: 'industrial-vest', label: 'Industrial Vest', price: 5000, sortOrder: 3 },
{ id: 'custom-color', label: 'Custom Color', price: 3500, sortOrder: 4 }, { id: 'business-suit', label: 'Business Suit', price: 5000, sortOrder: 4 },
{ id: 'robot-doctor', label: 'Robot Doctor', price: 5000, sortOrder: 5 }, { id: 'custom-color', label: 'Custom Color', price: 3500, sortOrder: 5 },
{ id: 'security-guard', label: 'Security Guard', price: 5000, sortOrder: 6 }, { id: 'robot-doctor', label: 'Robot Doctor', price: 5000, sortOrder: 6 },
{ id: 'security-guard', label: 'Security Guard', price: 5000, sortOrder: 7 },
]; ];
for (const item of defaultItems) { for (const item of defaultItems) {
await prisma.pricingItem.create({ data: item }); await prisma.pricingItem.upsert({
} where: { id: item.id },
console.log(`✓ Seeded ${defaultItems.length} default pricing items.`); create: item,
} else { update: { label: item.label, price: item.price, sortOrder: item.sortOrder },
console.log(`✓ ${pricingCount} pricing items already exist, skipping.`); });
} }
console.log(`✓ Upserted ${defaultItems.length} pricing items from code defaults.`);
console.log('Seeding complete!'); console.log('Seeding complete!');
} }
+40
View File
@@ -0,0 +1,40 @@
// One-off price refresh. Run with: npx tsx prisma/update-prices.ts
// Idempotent: upserts every row, safe to re-run.
import { PrismaClient } from '../src/generated/prisma/client.js';
import { PrismaLibSql } from '@prisma/adapter-libsql';
import path from 'path';
const dbPath = path.resolve(process.cwd(), 'prisma', 'lootah.db');
const adapter = new PrismaLibSql({ url: `file:${dbPath}` });
const prisma = new PrismaClient({ adapter } as ConstructorParameters<typeof PrismaClient>[0]);
const items = [
{ id: 'base', label: 'G1 Robot Basic', price: 77125, sortOrder: 0 },
{ id: 'edu', label: 'G1 Robot EDU', price: 146900, sortOrder: 1 },
{ id: 'emarati-kandura', label: 'Emarati Kandura', price: 5000, sortOrder: 2 },
{ id: 'industrial-vest', label: 'Industrial Vest', price: 5000, sortOrder: 3 },
{ id: 'business-suit', label: 'Business Suit', price: 5000, sortOrder: 4 },
{ id: 'custom-color', label: 'Custom Color', price: 3500, sortOrder: 5 },
{ id: 'robot-doctor', label: 'Robot Doctor', price: 5000, sortOrder: 6 },
{ id: 'security-guard', label: 'Security Guard', price: 5000, sortOrder: 7 },
];
async function main() {
for (const item of items) {
await prisma.pricingItem.upsert({
where: { id: item.id },
create: item,
update: { label: item.label, price: item.price, sortOrder: item.sortOrder },
});
console.log(`✓ ${item.id} → ${item.label} @ ${item.price} AED`);
}
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg width="98" height="26" viewBox="0 0 98 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_7615_2164)">
<path d="M90.9858 21.7754C93.4836 21.7754 98.0003 20.6516 98.0003 14.5274V4.22375H94.6635V14.3776C94.6635 17.7951 92.1419 18.4216 90.7494 18.4216H90.1531C88.7591 18.4216 86.2391 17.7951 86.2391 14.3776V4.22375H82.9023V14.5274C82.9023 20.6532 87.419 21.7754 89.9168 21.7754H90.9858Z" fill="#FFFFFF"/>
<path d="M43.2211 10.9441C43.2211 11.985 42.758 13.8659 40.2317 13.8659H34.7951V7.57755H40.2317C42.758 7.57755 43.2211 9.46007 43.2211 10.4994V10.9441ZM46.5579 10.2619C46.5579 8.11154 45.6 4.22375 40.3823 4.22375H31.46V21.6367H34.7967V17.2197H40.3823C45.6016 17.2197 46.5579 13.3319 46.5579 11.1816V10.2619Z" fill="#FFFFFF"/>
<path d="M80.9788 13.5455C80.9788 16.4259 79.6958 21.6351 72.705 21.6351H72.3181H72.7463H65.8809V4.22375H72.7463H72.3181H72.705C79.6958 4.22375 80.9788 9.43298 80.9788 12.3133V13.5455ZM77.642 12.5509C77.642 10.7799 76.8538 7.57755 72.556 7.57755H69.2176V18.2813H72.556C76.8538 18.2813 77.642 15.079 77.642 13.308V12.5509Z" fill="#FFFFFF"/>
<path d="M56.4887 21.7754C58.9865 21.7754 63.5032 20.6516 63.5032 14.5274V4.22375H60.1664V14.3776C60.1664 17.7951 57.6448 18.4216 56.2524 18.4216H55.6561C54.2621 18.4216 51.742 17.7951 51.742 14.3776V4.22375H48.4053V14.5274C48.4053 20.6532 52.922 21.7754 55.4198 21.7754H56.4887Z" fill="#FFFFFF"/>
<path d="M0.114186 25.9884L6.14225 22.4912C6.18982 22.4641 6.21837 22.4131 6.21837 22.3589V0.0762157C6.21837 0.0172373 6.15493 -0.0194248 6.10418 0.0108613L0.0761239 3.50812C0.0285464 3.53521 0 3.58622 0 3.64042V25.9231C0 25.982 0.0634366 26.0187 0.114186 25.9884Z" fill="#FFFFFF"/>
<path d="M16.5444 25.9884L22.5724 22.4912C22.62 22.4641 22.6485 22.4131 22.6485 22.3589V0.0762157C22.6485 0.0172373 22.5851 -0.0194248 22.5344 0.0108613L16.5063 3.50812C16.4587 3.53521 16.4302 3.58622 16.4302 3.64042V25.9231C16.4302 25.982 16.4936 26.0187 16.5444 25.9884Z" fill="#FFFFFF"/>
<path d="M14.4332 3.64042V9.94791C14.4332 10.0021 14.4047 10.0531 14.3571 10.0802L8.32903 13.5775C8.27828 13.6062 8.21484 13.5695 8.21484 13.5121V0.0762157C8.21484 0.0172373 8.27828 -0.0194248 8.32903 0.0108613L14.3571 3.50812C14.4047 3.53521 14.4332 3.58622 14.4332 3.64042Z" fill="#FFFFFF"/>
<path d="M8.21484 16.0514V22.3605C8.21484 22.4147 8.24339 22.4657 8.29097 22.4928L14.319 25.9901C14.3698 26.0188 14.4332 25.9821 14.4332 25.9247V12.4872C14.4332 12.4282 14.3698 12.3916 14.319 12.4219L8.29097 15.9191C8.24339 15.9462 8.21484 15.9972 8.21484 16.0514Z" fill="#FFFFFF"/>
</g>
<defs>
<clipPath id="clip0_7615_2164">
<rect width="98" height="26" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 870.93 196.18" style="enable-background:new 0 0 870.93 196.18;" xml:space="preserve">
<style type="text/css">
.st0{fill:#040000;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#040000;}
</style>
<g>
<path class="st0" d="M336.28,15.85l-28.22,113.64c-1.5,6.05-2.68,8.25-4.43,10.48c-1.72,2.2-3.89,3.93-6.4,5.13
c-2.51,1.18-4.86,1.81-10.92,1.81h-19.2l32.55-131.06C299.66,15.85,336.28,15.85,336.28,15.85z"/>
<path class="st0" d="M144.8,50.55l-18.94,76.29c-1.73,6.98-3.07,9.5-5.1,12.07c-2.01,2.56-4.48,4.55-7.39,5.94
c-2.9,1.35-5.61,2.08-12.58,2.08H29.16c-6.98,0-9.32-0.73-11.55-2.08c-2.22-1.39-3.71-3.37-4.44-5.94
c-0.75-2.56-0.84-5.09,0.9-12.07l18.94-76.29h36.62l-18.11,72.91c-0.69,2.79-0.65,3.8-0.35,4.84c0.29,1.02,0.88,1.81,1.78,2.37
c0.87,0.54,1.82,0.83,4.61,0.83h22.47c2.79,0,3.89-0.29,5.02-0.83c1.18-0.56,2.17-1.35,2.96-2.37c0.82-1.04,1.36-2.04,2.05-4.84
l18.11-72.91C108.17,50.55,144.8,50.55,144.8,50.55z"/>
<path class="st0" d="M587.04,147.17h-40.19l-29.7-57.44h30.11c2.79,0,3.87-0.29,5.02-0.83c0.48-0.23,0.93-0.5,1.34-0.81
c0.61-0.44,1.15-0.96,1.62-1.56c0.79-1.02,1.34-2.04,2.03-4.84l1.79-7.21c0.69-2.79,0.65-3.8,0.37-4.82
c-0.18-0.6-0.45-1.14-0.85-1.56c-0.25-0.33-0.56-0.6-0.94-0.81c-0.89-0.54-1.82-0.83-4.61-0.83h-30.11l-19.98,80.45h-36.62
l23.93-96.37h94.23c6.07,0,8.1,0.62,10.02,1.79c1.92,1.19,3.22,2.93,3.85,5.15c0.11,0.35,0.19,0.71,0.25,1.08
c0.39,1.93,0.24,4.3-1.02,9.39l-4.99,20.08c-1.26,5.09-2.3,7.46-3.64,9.39c-0.25,0.37-0.51,0.73-0.79,1.08
c-1.73,2.22-3.89,3.95-6.4,5.15c-2.51,1.18-4.84,1.79-10.91,1.79h-7.79l0,0L587.04,147.17z"/>
<path class="st0" d="M647.58,87.17h75.17l-4.31,17.35h-67.13c-2.79,0-3.87,0.29-5.02,0.83c-1.16,0.56-2.17,1.35-2.96,2.37
c-0.81,1.02-1.36,2.04-2.05,4.84l-4.23,17.02h75.17l-4.31,17.35h-91.69c-7,0-9.34-0.73-11.55-2.08c-2.22-1.39-3.73-3.37-4.46-5.94
c-0.73-2.56-0.84-5.09,0.9-12.07l14.62-58.88c1.5-6.05,2.68-8.25,4.43-10.47c1.73-2.22,3.89-3.95,6.4-5.15
c2.51-1.18,4.86-1.79,10.91-1.79h94.36l-4.31,17.35H660.4c-2.79,0-3.87,0.29-5.02,0.83c-1.16,0.56-2.17,1.35-2.96,2.37
c-0.81,1.02-1.36,2.04-2.05,4.84L647.58,87.17z"/>
<path class="st0" d="M777.75,87.17h75.17l-4.31,17.35h-67.13c-2.79,0-3.89,0.29-5.04,0.83c-1.16,0.56-2.15,1.35-2.96,2.37
c-0.79,1.02-1.34,2.04-2.03,4.84l-4.23,17.02h75.17l-4.31,17.35h-91.71c-7,0-9.34-0.73-11.55-2.08c-2.22-1.39-3.73-3.37-4.44-5.94
c-0.75-2.56-0.84-5.09,0.9-12.07l14.62-58.88c1.5-6.05,2.67-8.25,4.41-10.47c1.73-2.22,3.89-3.95,6.42-5.15
c2.49-1.18,4.84-1.79,10.89-1.79H862l-4.31,17.35h-67.13c-2.79,0-3.89,0.29-5.04,0.83c-1.16,0.56-2.15,1.35-2.96,2.37
c-0.79,1.02-1.34,2.04-2.03,4.84L777.75,87.17z"/>
<g>
<path class="st1" d="M274.41,50.55l-23.93,96.37h-25.33c-6.07,0-8.09-0.64-10.02-1.81c-0.25-0.15-0.48-0.31-0.7-0.5
c-1.3-0.96-2.27-2.25-2.88-3.78c0.01-0.04,0-0.06-0.02-0.08l-20.92-55.47l-15.31,61.64h-36.62l23.93-96.37h25.33
c6.05,0,8.1,0.62,10.02,1.79c0.25,0.15,0.48,0.31,0.7,0.5c1.32,0.98,2.27,2.25,2.87,3.82c0.01,0.02,0.01,0.02,0.03,0.04
l20.92,55.47l15.3-61.62H274.41z"/>
</g>
<g>
<polygon class="st0" points="473.07,50.55 468.52,68.86 422.27,68.86 394.27,181.61 357.65,181.61 385.64,68.86 339.39,68.86
343.93,50.55 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

+44
View File
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 870.93 196.18" style="enable-background:new 0 0 870.93 196.18;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M337,15.2l-27.95,112.55c-1.49,5.99-2.66,8.17-4.39,10.38c-1.7,2.18-3.85,3.89-6.34,5.08
c-2.48,1.16-4.82,1.79-10.81,1.79h-19.01L300.73,15.2H337z"/>
<path class="st0" d="M147.34,49.56l-18.76,75.56c-1.72,6.91-3.04,9.41-5.05,11.95c-1.99,2.54-4.44,4.51-7.32,5.88
c-2.87,1.34-5.55,2.06-12.46,2.06H32.81c-6.91,0-9.23-0.73-11.44-2.06c-2.2-1.37-3.68-3.34-4.4-5.88
c-0.74-2.54-0.83-5.04,0.89-11.95l18.76-75.56h36.27l-17.93,72.22c-0.69,2.77-0.65,3.76-0.35,4.79c0.28,1.01,0.87,1.79,1.77,2.35
c0.86,0.53,1.8,0.82,4.57,0.82H83.2c2.77,0,3.85-0.29,4.98-0.82c1.17-0.55,2.15-1.34,2.93-2.35c0.81-1.03,1.34-2.02,2.03-4.79
l17.93-72.22H147.34z"/>
<path class="st0" d="M585.36,145.26h-39.8l-29.42-56.89h29.82c2.77,0,3.83-0.29,4.98-0.82c0.48-0.23,0.93-0.5,1.33-0.8
c0.61-0.44,1.13-0.95,1.61-1.55c0.79-1.01,1.32-2.02,2.01-4.79l1.77-7.14c0.69-2.77,0.65-3.76,0.36-4.77
c-0.18-0.59-0.45-1.13-0.84-1.55c-0.24-0.32-0.56-0.59-0.93-0.8c-0.88-0.53-1.8-0.82-4.57-0.82h-29.82l-19.79,79.68h-36.27
l23.7-95.45h93.33c6.01,0,8.02,0.61,9.92,1.78c1.9,1.18,3.19,2.9,3.81,5.1c0.11,0.34,0.19,0.71,0.25,1.07
c0.39,1.91,0.24,4.26-1.01,9.3l-4.94,19.89c-1.25,5.04-2.27,7.39-3.61,9.3c-0.24,0.36-0.5,0.73-0.78,1.07
c-1.71,2.2-3.85,3.91-6.34,5.1c-2.48,1.16-4.79,1.78-10.81,1.78h-7.71l0,0L585.36,145.26z"/>
<path class="st0" d="M645.32,85.83h74.45l-4.27,17.18h-66.49c-2.77,0-3.83,0.29-4.98,0.82c-1.15,0.55-2.15,1.34-2.93,2.35
c-0.8,1.01-1.34,2.02-2.03,4.79l-4.19,16.86h74.45l-4.27,17.18h-90.81c-6.93,0-9.25-0.73-11.44-2.06c-2.2-1.37-3.69-3.34-4.42-5.88
c-0.72-2.54-0.83-5.04,0.89-11.95l14.48-58.32c1.49-5.99,2.66-8.17,4.39-10.37c1.71-2.2,3.85-3.91,6.34-5.1
c2.48-1.16,4.81-1.78,10.81-1.78h93.46l-4.27,17.18h-66.49c-2.77,0-3.83,0.29-4.98,0.82c-1.15,0.55-2.15,1.34-2.93,2.35
c-0.8,1.01-1.34,2.02-2.03,4.79L645.32,85.83z"/>
<path class="st0" d="M774.26,85.83h74.45l-4.27,17.18h-66.49c-2.77,0-3.85,0.29-5,0.82c-1.15,0.55-2.13,1.34-2.93,2.35
c-0.79,1.01-1.32,2.02-2.01,4.79l-4.19,16.86h74.45l-4.27,17.18h-90.83c-6.93,0-9.25-0.73-11.44-2.06c-2.2-1.37-3.69-3.34-4.4-5.88
c-0.74-2.54-0.83-5.04,0.89-11.95l14.48-58.32c1.49-5.99,2.64-8.17,4.37-10.37c1.71-2.2,3.85-3.91,6.36-5.1
c2.47-1.16,4.79-1.78,10.79-1.78h93.48l-4.27,17.18h-66.49c-2.77,0-3.85,0.29-5,0.82c-1.15,0.55-2.13,1.34-2.93,2.35
c-0.79,1.01-1.32,2.02-2.01,4.79L774.26,85.83z"/>
<g>
<path class="st1" d="M275.72,49.56l-23.7,95.45h-25.08c-6.01,0-8.01-0.63-9.92-1.79c-0.25-0.15-0.48-0.31-0.7-0.5
c-1.29-0.95-2.25-2.23-2.85-3.74c0.01-0.04,0-0.06-0.02-0.08l-20.72-54.94l-15.16,61.05H141.3L165,49.56h25.08
c5.99,0,8.02,0.61,9.92,1.78c0.25,0.15,0.48,0.31,0.7,0.5c1.3,0.97,2.25,2.23,2.84,3.78c0.01,0.02,0.01,0.02,0.03,0.04
l20.72,54.94l15.16-61.03H275.72z"/>
</g>
<g>
<polygon class="st0" points="472.48,49.56 467.98,67.7 422.16,67.7 394.43,179.37 358.16,179.37 385.89,67.7 340.08,67.7
344.58,49.56 "/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

+1
View File
@@ -0,0 +1 @@
Drop the approved Bu Sunaidah photo here as portrait.jpg (or .webp).
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Some files were not shown because too many files have changed in this diff Show More