1397 lines
37 KiB
Markdown
1397 lines
37 KiB
Markdown
|
|
# HarborSmith Landing Page Conversion Plan
|
||
|
|
## From Static HTML to Nuxt 3 + Tailwind CSS v4
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Table of Contents
|
||
|
|
1. [Executive Summary](#executive-summary)
|
||
|
|
2. [Current State Analysis](#current-state-analysis)
|
||
|
|
3. [Target Architecture](#target-architecture)
|
||
|
|
4. [Implementation Strategy](#implementation-strategy)
|
||
|
|
5. [Technical Implementation](#technical-implementation)
|
||
|
|
6. [Component Architecture](#component-architecture)
|
||
|
|
7. [Theme System](#theme-system)
|
||
|
|
8. [Performance Optimizations](#performance-optimizations)
|
||
|
|
9. [Deployment Strategy](#deployment-strategy)
|
||
|
|
10. [Migration Checklist](#migration-checklist)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Executive Summary
|
||
|
|
|
||
|
|
This document outlines the comprehensive plan for converting the HarborSmith landing page from static HTML/CSS/JS to a production-ready Nuxt 3 application with Tailwind CSS v4, while preserving the sophisticated design system with 4 theme variants, video hero, and premium animations.
|
||
|
|
|
||
|
|
### Key Objectives
|
||
|
|
- Preserve 100% visual parity with existing mockups
|
||
|
|
- Implement modern development practices with TypeScript
|
||
|
|
- Achieve < 1 second page load time
|
||
|
|
- Maintain SEO optimization with SSG
|
||
|
|
- Deploy as standalone Docker container
|
||
|
|
|
||
|
|
### Approach
|
||
|
|
Hybrid system using Tailwind for layout/utilities, CSS custom properties for dynamic theming, and Vue composables for interactivity.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Current State Analysis
|
||
|
|
|
||
|
|
### Existing Mockup Structure
|
||
|
|
```
|
||
|
|
website-mockups/
|
||
|
|
├── HTML Files (18 total)
|
||
|
|
│ ├── index.html # Main landing page
|
||
|
|
│ ├── charter-booking-*.html # 4-step booking flow
|
||
|
|
│ ├── maintenance/*.html # Portal pages
|
||
|
|
│ └── portal-login.html # Authentication
|
||
|
|
├── CSS Files
|
||
|
|
│ ├── voyage-layout.css # Main layout styles
|
||
|
|
│ ├── themes.css # Theme variables
|
||
|
|
│ ├── animations.css # Keyframe animations
|
||
|
|
│ └── booking.css # Booking-specific styles
|
||
|
|
└── JavaScript
|
||
|
|
├── main.js # Core functionality
|
||
|
|
├── animations.js # Scroll effects
|
||
|
|
└── booking.js # Booking logic
|
||
|
|
```
|
||
|
|
|
||
|
|
### Current Technologies
|
||
|
|
- **Styling**: Plain CSS with CSS variables
|
||
|
|
- **Themes**: 4 variants (Nautical, Coastal Dawn, Deep Sea, Monaco)
|
||
|
|
- **Icons**: Lucide via CDN
|
||
|
|
- **Fonts**: Inter + Playfair Display (Google Fonts)
|
||
|
|
- **Video**: Hero background with fallback
|
||
|
|
- **Animations**: CSS keyframes + JavaScript scroll effects
|
||
|
|
|
||
|
|
### Design System Highlights
|
||
|
|
- Warm gradients and ocean-inspired colors
|
||
|
|
- Premium yacht aesthetic
|
||
|
|
- Smooth transitions and animations
|
||
|
|
- Mobile-responsive design
|
||
|
|
- Accessibility considerations (WCAG AA)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Target Architecture
|
||
|
|
|
||
|
|
### Technology Stack
|
||
|
|
```
|
||
|
|
Frontend Framework: Nuxt 3 (v3.10+)
|
||
|
|
CSS Framework: Tailwind CSS v4 (beta)
|
||
|
|
UI Components: Nuxt UI v3
|
||
|
|
Animation: @vueuse/motion
|
||
|
|
State Management: Pinia
|
||
|
|
Icons: lucide-vue-next
|
||
|
|
Type Safety: TypeScript
|
||
|
|
Image Optimization: Nuxt Image
|
||
|
|
Deployment: Docker + nginx
|
||
|
|
```
|
||
|
|
|
||
|
|
### Project Structure
|
||
|
|
```
|
||
|
|
harborsmith-web/
|
||
|
|
├── app.vue # Root component with theme provider
|
||
|
|
├── nuxt.config.ts # Nuxt configuration
|
||
|
|
├── tailwind.config.ts # Tailwind with custom properties
|
||
|
|
├── package.json # Dependencies
|
||
|
|
├── Dockerfile # Production build
|
||
|
|
├── nginx.conf # Production server config
|
||
|
|
├── components/
|
||
|
|
│ ├── layout/
|
||
|
|
│ │ ├── HarborNav.vue # Navigation with theme switcher
|
||
|
|
│ │ └── HarborFooter.vue # Footer component
|
||
|
|
│ ├── sections/
|
||
|
|
│ │ ├── HeroVideo.vue # Video hero section
|
||
|
|
│ │ ├── TrustBadges.vue # Social proof
|
||
|
|
│ │ ├── FleetGrid.vue # Vessel showcase
|
||
|
|
│ │ ├── Services.vue # Services overview
|
||
|
|
│ │ └── Testimonials.vue # Customer reviews
|
||
|
|
│ └── ui/
|
||
|
|
│ ├── ThemedButton.vue # Gradient CTAs
|
||
|
|
│ ├── VesselCard.vue # Fleet display cards
|
||
|
|
│ └── ThemeSwitcher.vue # Theme selector dropdown
|
||
|
|
├── composables/
|
||
|
|
│ ├── useTheme.ts # Theme switching logic
|
||
|
|
│ ├── useScrollEffects.ts # Scroll animations
|
||
|
|
│ └── useVideoLoader.ts # Video optimization
|
||
|
|
├── stores/
|
||
|
|
│ └── theme.ts # Pinia theme store
|
||
|
|
├── assets/
|
||
|
|
│ └── css/
|
||
|
|
│ ├── base.css # Global styles
|
||
|
|
│ ├── themes.css # CSS custom properties
|
||
|
|
│ └── animations.css # Keyframe definitions
|
||
|
|
└── public/
|
||
|
|
├── videos/ # Hero videos
|
||
|
|
├── images/ # Static images
|
||
|
|
└── fonts/ # Self-hosted fonts (optional)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Strategy
|
||
|
|
|
||
|
|
### Phase 1: Foundation Setup (Day 1)
|
||
|
|
1. Initialize Nuxt 3 project
|
||
|
|
2. Configure Tailwind CSS v4
|
||
|
|
3. Set up TypeScript
|
||
|
|
4. Install core dependencies
|
||
|
|
5. Create base project structure
|
||
|
|
|
||
|
|
### Phase 2: Theme System (Day 2)
|
||
|
|
1. Port CSS variables to Tailwind config
|
||
|
|
2. Implement theme switcher with Pinia
|
||
|
|
3. Create theme persistence logic
|
||
|
|
4. Test all 4 theme variants
|
||
|
|
|
||
|
|
### Phase 3: Component Development (Days 3-4)
|
||
|
|
1. Convert navigation component
|
||
|
|
2. Build video hero section
|
||
|
|
3. Create reusable UI components
|
||
|
|
4. Implement fleet showcase grid
|
||
|
|
5. Add testimonial carousel
|
||
|
|
|
||
|
|
### Phase 4: Animations & Polish (Day 5)
|
||
|
|
1. Implement scroll animations
|
||
|
|
2. Add hover effects
|
||
|
|
3. Optimize transitions
|
||
|
|
4. Fine-tune responsive design
|
||
|
|
|
||
|
|
### Phase 5: Optimization & Deployment (Day 6)
|
||
|
|
1. Image optimization
|
||
|
|
2. Performance testing
|
||
|
|
3. Docker configuration
|
||
|
|
4. Production deployment
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Technical Implementation
|
||
|
|
|
||
|
|
### 1. Project Initialization
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create Nuxt 3 application
|
||
|
|
npx nuxi@latest init harborsmith-web --template minimal
|
||
|
|
cd harborsmith-web
|
||
|
|
|
||
|
|
# Install core dependencies
|
||
|
|
npm install -D @nuxtjs/tailwindcss@next @nuxt/ui@next
|
||
|
|
npm install lucide-vue-next @vueuse/motion @vueuse/nuxt
|
||
|
|
npm install pinia @pinia/nuxt
|
||
|
|
npm install @nuxt/image
|
||
|
|
|
||
|
|
# Development dependencies
|
||
|
|
npm install -D @types/node typescript sass
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Nuxt Configuration
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// nuxt.config.ts
|
||
|
|
export default defineNuxtConfig({
|
||
|
|
devtools: { enabled: true },
|
||
|
|
|
||
|
|
modules: [
|
||
|
|
'@nuxt/ui',
|
||
|
|
'@nuxtjs/tailwindcss',
|
||
|
|
'@vueuse/nuxt',
|
||
|
|
'@pinia/nuxt',
|
||
|
|
'@nuxt/image',
|
||
|
|
],
|
||
|
|
|
||
|
|
css: [
|
||
|
|
'~/assets/css/base.css',
|
||
|
|
'~/assets/css/themes.css',
|
||
|
|
'~/assets/css/animations.css',
|
||
|
|
],
|
||
|
|
|
||
|
|
typescript: {
|
||
|
|
strict: true,
|
||
|
|
typeCheck: true,
|
||
|
|
},
|
||
|
|
|
||
|
|
nitro: {
|
||
|
|
preset: 'static',
|
||
|
|
prerender: {
|
||
|
|
routes: ['/'],
|
||
|
|
crawlLinks: true,
|
||
|
|
},
|
||
|
|
compressPublicAssets: true,
|
||
|
|
},
|
||
|
|
|
||
|
|
image: {
|
||
|
|
quality: 80,
|
||
|
|
format: ['webp', 'jpg'],
|
||
|
|
screens: {
|
||
|
|
xs: 320,
|
||
|
|
sm: 640,
|
||
|
|
md: 768,
|
||
|
|
lg: 1024,
|
||
|
|
xl: 1280,
|
||
|
|
xxl: 1536,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
|
||
|
|
app: {
|
||
|
|
head: {
|
||
|
|
charset: 'utf-8',
|
||
|
|
viewport: 'width=device-width, initial-scale=1',
|
||
|
|
title: 'HarborSmith - Premium Yacht Charters SF Bay',
|
||
|
|
meta: [
|
||
|
|
{ name: 'description', content: 'Experience luxury yacht charters in the San Francisco Bay. Premium vessels, professional maintenance, unforgettable adventures.' },
|
||
|
|
{ property: 'og:title', content: 'HarborSmith - Your Adventure Awaits' },
|
||
|
|
{ property: 'og:image', content: '/og-image.jpg' },
|
||
|
|
],
|
||
|
|
link: [
|
||
|
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
|
||
|
|
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||
|
|
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: '' },
|
||
|
|
{
|
||
|
|
rel: 'stylesheet',
|
||
|
|
href: 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;700;900&display=swap'
|
||
|
|
},
|
||
|
|
],
|
||
|
|
script: [
|
||
|
|
// Prevent theme flash
|
||
|
|
{
|
||
|
|
innerHTML: `
|
||
|
|
(function() {
|
||
|
|
const theme = localStorage.getItem('harborsmith-theme') || 'nautical';
|
||
|
|
document.documentElement.className = 'theme-' + theme;
|
||
|
|
})();
|
||
|
|
`,
|
||
|
|
type: 'text/javascript',
|
||
|
|
}
|
||
|
|
],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
|
||
|
|
experimental: {
|
||
|
|
payloadExtraction: false,
|
||
|
|
renderJsonPayloads: false,
|
||
|
|
viewTransition: true,
|
||
|
|
},
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Tailwind Configuration
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// tailwind.config.ts
|
||
|
|
import type { Config } from 'tailwindcss'
|
||
|
|
|
||
|
|
export default <Config>{
|
||
|
|
content: [],
|
||
|
|
theme: {
|
||
|
|
extend: {
|
||
|
|
colors: {
|
||
|
|
// Dynamic theme colors using CSS variables
|
||
|
|
'primary': 'rgb(var(--primary-blue) / <alpha-value>)',
|
||
|
|
'accent': 'rgb(var(--warm-orange) / <alpha-value>)',
|
||
|
|
'accent-hover': 'rgb(var(--warm-amber) / <alpha-value>)',
|
||
|
|
'accent-light': 'rgb(var(--warm-yellow) / <alpha-value>)',
|
||
|
|
'cream': 'rgb(var(--soft-cream) / <alpha-value>)',
|
||
|
|
'text-primary': 'rgb(var(--text-dark) / <alpha-value>)',
|
||
|
|
'text-secondary': 'rgb(var(--text-light) / <alpha-value>)',
|
||
|
|
},
|
||
|
|
backgroundImage: {
|
||
|
|
'gradient-warm': 'var(--gradient-warm)',
|
||
|
|
'gradient-sunset': 'var(--gradient-sunset)',
|
||
|
|
'gradient-ocean': 'var(--gradient-ocean)',
|
||
|
|
},
|
||
|
|
fontFamily: {
|
||
|
|
'sans': ['Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
|
||
|
|
'display': ['Playfair Display', 'serif'],
|
||
|
|
},
|
||
|
|
spacing: {
|
||
|
|
'18': '4.5rem',
|
||
|
|
'88': '22rem',
|
||
|
|
'120': '30rem',
|
||
|
|
},
|
||
|
|
animation: {
|
||
|
|
'fade-up': 'fadeInUp 0.8s ease forwards',
|
||
|
|
'fade-up-delay': 'fadeInUp 0.8s ease 0.2s forwards',
|
||
|
|
'fade-up-delay-2': 'fadeInUp 0.8s ease 0.4s forwards',
|
||
|
|
'float': 'float 20s ease-in-out infinite',
|
||
|
|
'pulse-slow': 'pulse 3s ease-in-out infinite',
|
||
|
|
},
|
||
|
|
transitionTimingFunction: {
|
||
|
|
'bounce-in': 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
plugins: [],
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Component Architecture
|
||
|
|
|
||
|
|
### Video Hero Component
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<!-- components/sections/HeroVideo.vue -->
|
||
|
|
<template>
|
||
|
|
<section class="hero-voyage relative h-screen overflow-hidden">
|
||
|
|
<!-- Video Background -->
|
||
|
|
<div class="absolute inset-0 z-0">
|
||
|
|
<!-- Video Element -->
|
||
|
|
<ClientOnly>
|
||
|
|
<video
|
||
|
|
v-if="!videoError && videoLoaded"
|
||
|
|
ref="videoRef"
|
||
|
|
:key="videoUrl"
|
||
|
|
autoplay
|
||
|
|
loop
|
||
|
|
muted
|
||
|
|
playsinline
|
||
|
|
@loadeddata="onVideoLoaded"
|
||
|
|
@error="onVideoError"
|
||
|
|
class="w-full h-full object-cover scale-110"
|
||
|
|
>
|
||
|
|
<source :src="videoUrl" type="video/mp4">
|
||
|
|
</video>
|
||
|
|
</ClientOnly>
|
||
|
|
|
||
|
|
<!-- Fallback/Poster Image -->
|
||
|
|
<NuxtImg
|
||
|
|
:src="posterImage"
|
||
|
|
alt="San Francisco Bay"
|
||
|
|
class="absolute inset-0 w-full h-full object-cover"
|
||
|
|
:class="{ 'opacity-0': videoLoaded && !videoError }"
|
||
|
|
loading="eager"
|
||
|
|
quality="90"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<!-- Gradient Overlays -->
|
||
|
|
<div class="absolute inset-0 bg-gradient-warm opacity-40 mix-blend-multiply" />
|
||
|
|
<div class="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-black/50" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Hero Content -->
|
||
|
|
<div class="relative z-10 h-full flex items-center justify-center px-4">
|
||
|
|
<div class="text-center text-white max-w-4xl mx-auto">
|
||
|
|
<!-- Trust Badge -->
|
||
|
|
<div class="inline-flex items-center gap-2 bg-white/10 backdrop-blur-sm rounded-full px-4 py-2 mb-6">
|
||
|
|
<div class="flex">
|
||
|
|
<Icon v-for="i in 5" :key="i" name="lucide:star" class="w-4 h-4 text-yellow-400 fill-current" />
|
||
|
|
</div>
|
||
|
|
<span class="text-sm">500+ Premium Charters</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Headlines -->
|
||
|
|
<h1 class="font-display text-5xl md:text-6xl lg:text-7xl font-bold mb-6 leading-tight">
|
||
|
|
Your Adventure Awaits
|
||
|
|
</h1>
|
||
|
|
<p class="text-xl md:text-2xl mb-8 opacity-95 font-light">
|
||
|
|
Premium Yacht Charters in the San Francisco Bay
|
||
|
|
</p>
|
||
|
|
|
||
|
|
<!-- CTA Buttons -->
|
||
|
|
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||
|
|
<ThemedButton size="lg" variant="primary" @click="scrollToFleet">
|
||
|
|
Explore Our Fleet
|
||
|
|
<Icon name="lucide:arrow-right" class="ml-2" />
|
||
|
|
</ThemedButton>
|
||
|
|
<ThemedButton size="lg" variant="outline" @click="navigateToBooking">
|
||
|
|
View Availability
|
||
|
|
</ThemedButton>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Scroll Indicator -->
|
||
|
|
<div class="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
|
||
|
|
<button @click="scrollToNext" class="text-white/70 hover:text-white transition-colors">
|
||
|
|
<Icon name="lucide:chevron-down" class="w-8 h-8" />
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</section>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
const videoRef = ref<HTMLVideoElement>()
|
||
|
|
const videoLoaded = ref(false)
|
||
|
|
const videoError = ref(false)
|
||
|
|
|
||
|
|
// Video configuration
|
||
|
|
const videoUrl = 'https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4'
|
||
|
|
const posterImage = '/images/hero-poster.jpg'
|
||
|
|
|
||
|
|
// Video handlers
|
||
|
|
const onVideoLoaded = () => {
|
||
|
|
videoLoaded.value = true
|
||
|
|
}
|
||
|
|
|
||
|
|
const onVideoError = () => {
|
||
|
|
videoError.value = true
|
||
|
|
console.warn('Video failed to load, using fallback image')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Navigation
|
||
|
|
const scrollToFleet = () => {
|
||
|
|
document.getElementById('fleet')?.scrollIntoView({
|
||
|
|
behavior: 'smooth',
|
||
|
|
block: 'start'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const scrollToNext = () => {
|
||
|
|
window.scrollBy({
|
||
|
|
top: window.innerHeight,
|
||
|
|
behavior: 'smooth'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const navigateToBooking = () => {
|
||
|
|
navigateTo('/booking')
|
||
|
|
}
|
||
|
|
|
||
|
|
// Lazy load video after initial render
|
||
|
|
onMounted(() => {
|
||
|
|
// Only load video on desktop or good connection
|
||
|
|
if (navigator.connection?.effectiveType === '4g' || !navigator.connection) {
|
||
|
|
videoRef.value?.load()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
```
|
||
|
|
|
||
|
|
### Navigation Component
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<!-- components/layout/HarborNav.vue -->
|
||
|
|
<template>
|
||
|
|
<nav
|
||
|
|
:class="[
|
||
|
|
'fixed top-0 w-full z-50 transition-all duration-500',
|
||
|
|
{
|
||
|
|
'bg-white/95 backdrop-blur-md shadow-lg': scrolled,
|
||
|
|
'bg-gradient-to-b from-black/50 to-transparent': !scrolled
|
||
|
|
}
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
<div class="container mx-auto px-4">
|
||
|
|
<div class="flex items-center justify-between h-20">
|
||
|
|
<!-- Logo -->
|
||
|
|
<NuxtLink to="/" class="flex items-center gap-3 group">
|
||
|
|
<img
|
||
|
|
src="/logo.jpg"
|
||
|
|
alt="HarborSmith"
|
||
|
|
class="h-12 w-12 rounded-lg shadow-md group-hover:shadow-xl transition-shadow"
|
||
|
|
>
|
||
|
|
<span
|
||
|
|
class="font-display text-2xl font-bold transition-colors"
|
||
|
|
:class="scrolled ? 'text-primary' : 'text-white'"
|
||
|
|
>
|
||
|
|
HarborSmith
|
||
|
|
</span>
|
||
|
|
</NuxtLink>
|
||
|
|
|
||
|
|
<!-- Desktop Navigation -->
|
||
|
|
<div class="hidden md:flex items-center gap-8">
|
||
|
|
<!-- Nav Links -->
|
||
|
|
<NuxtLink
|
||
|
|
v-for="link in navLinks"
|
||
|
|
:key="link.href"
|
||
|
|
:to="link.href"
|
||
|
|
:class="[
|
||
|
|
'font-medium transition-colors relative',
|
||
|
|
scrolled ? 'text-gray-700 hover:text-accent' : 'text-white hover:text-accent-light',
|
||
|
|
'after:absolute after:bottom-0 after:left-0 after:w-0 after:h-0.5',
|
||
|
|
'after:bg-accent after:transition-all hover:after:w-full'
|
||
|
|
]"
|
||
|
|
>
|
||
|
|
{{ link.label }}
|
||
|
|
</NuxtLink>
|
||
|
|
|
||
|
|
<!-- Theme Switcher -->
|
||
|
|
<ThemeSwitcher :scrolled="scrolled" />
|
||
|
|
|
||
|
|
<!-- CTA Button -->
|
||
|
|
<ThemedButton variant="primary" size="sm">
|
||
|
|
Book Charter
|
||
|
|
<Icon name="lucide:calendar" class="ml-2 w-4 h-4" />
|
||
|
|
</ThemedButton>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Mobile Menu Button -->
|
||
|
|
<button
|
||
|
|
@click="mobileMenuOpen = !mobileMenuOpen"
|
||
|
|
class="md:hidden text-current"
|
||
|
|
>
|
||
|
|
<Icon
|
||
|
|
:name="mobileMenuOpen ? 'lucide:x' : 'lucide:menu'"
|
||
|
|
class="w-6 h-6"
|
||
|
|
/>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Mobile Menu -->
|
||
|
|
<Transition name="slide-down">
|
||
|
|
<div
|
||
|
|
v-if="mobileMenuOpen"
|
||
|
|
class="md:hidden bg-white/95 backdrop-blur-md border-t"
|
||
|
|
>
|
||
|
|
<div class="container mx-auto px-4 py-4 space-y-2">
|
||
|
|
<NuxtLink
|
||
|
|
v-for="link in navLinks"
|
||
|
|
:key="link.href"
|
||
|
|
:to="link.href"
|
||
|
|
@click="mobileMenuOpen = false"
|
||
|
|
class="block px-4 py-2 text-gray-700 hover:text-accent hover:bg-gray-50 rounded-lg transition-colors"
|
||
|
|
>
|
||
|
|
{{ link.label }}
|
||
|
|
</NuxtLink>
|
||
|
|
<div class="pt-2 border-t">
|
||
|
|
<ThemedButton variant="primary" size="sm" class="w-full">
|
||
|
|
Book Charter
|
||
|
|
</ThemedButton>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Transition>
|
||
|
|
</nav>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
const scrolled = ref(false)
|
||
|
|
const mobileMenuOpen = ref(false)
|
||
|
|
|
||
|
|
const navLinks = [
|
||
|
|
{ label: 'Fleet', href: '#fleet' },
|
||
|
|
{ label: 'Services', href: '#services' },
|
||
|
|
{ label: 'Experiences', href: '#experiences' },
|
||
|
|
{ label: 'About', href: '/about' },
|
||
|
|
{ label: 'Contact', href: '/contact' },
|
||
|
|
]
|
||
|
|
|
||
|
|
// Handle scroll behavior
|
||
|
|
onMounted(() => {
|
||
|
|
const handleScroll = () => {
|
||
|
|
scrolled.value = window.scrollY > 50
|
||
|
|
}
|
||
|
|
|
||
|
|
window.addEventListener('scroll', handleScroll, { passive: true })
|
||
|
|
|
||
|
|
onUnmounted(() => {
|
||
|
|
window.removeEventListener('scroll', handleScroll)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped>
|
||
|
|
.slide-down-enter-active,
|
||
|
|
.slide-down-leave-active {
|
||
|
|
transition: all 0.3s ease;
|
||
|
|
}
|
||
|
|
|
||
|
|
.slide-down-enter-from,
|
||
|
|
.slide-down-leave-to {
|
||
|
|
transform: translateY(-100%);
|
||
|
|
opacity: 0;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Theme System
|
||
|
|
|
||
|
|
### CSS Custom Properties
|
||
|
|
|
||
|
|
```css
|
||
|
|
/* assets/css/themes.css */
|
||
|
|
:root {
|
||
|
|
/* Default Theme: Nautical */
|
||
|
|
--primary-blue: 0 31 63; /* #001f3f */
|
||
|
|
--warm-orange: 220 20 60; /* #dc143c */
|
||
|
|
--warm-amber: 185 28 60; /* #b91c3c */
|
||
|
|
--warm-yellow: 239 68 68; /* #ef4444 */
|
||
|
|
--soft-cream: 240 244 248; /* #f0f4f8 */
|
||
|
|
--text-dark: 10 22 40; /* #0a1628 */
|
||
|
|
--text-light: 74 85 104; /* #4a5568 */
|
||
|
|
|
||
|
|
/* Gradients */
|
||
|
|
--gradient-warm: linear-gradient(135deg, #dc143c 0%, #ef4444 100%);
|
||
|
|
--gradient-sunset: linear-gradient(135deg, #b91c3c 0%, #dc143c 50%, #ef4444 100%);
|
||
|
|
--gradient-ocean: linear-gradient(135deg, #001f3f 0%, #003366 100%);
|
||
|
|
|
||
|
|
/* Shadows */
|
||
|
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||
|
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||
|
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||
|
|
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Coastal Dawn Theme */
|
||
|
|
.theme-coastal-dawn {
|
||
|
|
--primary-blue: 169 180 194; /* #A9B4C2 */
|
||
|
|
--warm-orange: 212 175 55; /* #D4AF37 */
|
||
|
|
--warm-amber: 201 169 97; /* #C9A961 */
|
||
|
|
--warm-yellow: 230 208 136; /* #E6D088 */
|
||
|
|
--soft-cream: 248 247 244; /* #F8F7F4 */
|
||
|
|
--text-dark: 51 55 69; /* #333745 */
|
||
|
|
--text-light: 107 114 128; /* #6B7280 */
|
||
|
|
|
||
|
|
--gradient-warm: linear-gradient(135deg, #D4AF37 0%, #E6D088 100%);
|
||
|
|
--gradient-sunset: linear-gradient(135deg, #C9A961 0%, #D4AF37 50%, #E6D088 100%);
|
||
|
|
--gradient-ocean: linear-gradient(135deg, #A9B4C2 0%, #C5D3E0 100%);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Deep Sea Theme */
|
||
|
|
.theme-deep-sea {
|
||
|
|
--primary-blue: 30 32 34; /* #1E2022 */
|
||
|
|
--warm-orange: 0 191 255; /* #00BFFF */
|
||
|
|
--warm-amber: 30 144 255; /* #1E90FF */
|
||
|
|
--warm-yellow: 65 105 225; /* #4169E1 */
|
||
|
|
--soft-cream: 42 45 48; /* #2A2D30 */
|
||
|
|
--text-dark: 229 228 226; /* #E5E4E2 */
|
||
|
|
--text-light: 192 192 192; /* #C0C0C0 */
|
||
|
|
|
||
|
|
--gradient-warm: linear-gradient(135deg, #00BFFF 0%, #4169E1 100%);
|
||
|
|
--gradient-sunset: linear-gradient(135deg, #1E90FF 0%, #00BFFF 50%, #4169E1 100%);
|
||
|
|
--gradient-ocean: linear-gradient(135deg, #1E2022 0%, #2A2D30 100%);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Monaco White Theme */
|
||
|
|
.theme-monaco {
|
||
|
|
--primary-blue: 44 62 80; /* #2C3E50 */
|
||
|
|
--warm-orange: 231 76 60; /* #E74C3C */
|
||
|
|
--warm-amber: 230 126 34; /* #E67E22 */
|
||
|
|
--warm-yellow: 243 156 18; /* #F39C12 */
|
||
|
|
--soft-cream: 248 249 250; /* #F8F9FA */
|
||
|
|
--text-dark: 44 62 80; /* #2C3E50 */
|
||
|
|
--text-light: 127 140 141; /* #7F8C8D */
|
||
|
|
|
||
|
|
--gradient-warm: linear-gradient(135deg, #E74C3C 0%, #F39C12 100%);
|
||
|
|
--gradient-sunset: linear-gradient(135deg, #E67E22 0%, #E74C3C 50%, #F39C12 100%);
|
||
|
|
--gradient-ocean: linear-gradient(135deg, #2C3E50 0%, #34495E 100%);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Theme Store (Pinia)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// stores/theme.ts
|
||
|
|
import { defineStore } from 'pinia'
|
||
|
|
|
||
|
|
export type ThemeVariant = 'nautical' | 'coastal-dawn' | 'deep-sea' | 'monaco'
|
||
|
|
|
||
|
|
interface ThemeOption {
|
||
|
|
id: ThemeVariant
|
||
|
|
name: string
|
||
|
|
description: string
|
||
|
|
colors: {
|
||
|
|
primary: string
|
||
|
|
accent: string
|
||
|
|
background: string
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export const useThemeStore = defineStore('theme', {
|
||
|
|
state: () => ({
|
||
|
|
currentTheme: 'nautical' as ThemeVariant,
|
||
|
|
themes: [
|
||
|
|
{
|
||
|
|
id: 'nautical',
|
||
|
|
name: 'Classic Nautical',
|
||
|
|
description: 'Deep navy and crimson',
|
||
|
|
colors: { primary: '#001f3f', accent: '#dc143c', background: '#ffffff' }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'coastal-dawn',
|
||
|
|
name: 'Coastal Dawn',
|
||
|
|
description: 'Soft blue and gold',
|
||
|
|
colors: { primary: '#A9B4C2', accent: '#D4AF37', background: '#F8F7F4' }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'deep-sea',
|
||
|
|
name: 'Deep Sea',
|
||
|
|
description: 'Dark mode with electric blue',
|
||
|
|
colors: { primary: '#1E2022', accent: '#00BFFF', background: '#2A2D30' }
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'monaco',
|
||
|
|
name: 'Monaco White',
|
||
|
|
description: 'Clean and sophisticated',
|
||
|
|
colors: { primary: '#2C3E50', accent: '#E74C3C', background: '#F8F9FA' }
|
||
|
|
}
|
||
|
|
] as ThemeOption[]
|
||
|
|
}),
|
||
|
|
|
||
|
|
getters: {
|
||
|
|
activeTheme: (state) => state.themes.find(t => t.id === state.currentTheme),
|
||
|
|
},
|
||
|
|
|
||
|
|
actions: {
|
||
|
|
setTheme(theme: ThemeVariant) {
|
||
|
|
this.currentTheme = theme
|
||
|
|
|
||
|
|
// Apply theme class to HTML element
|
||
|
|
if (process.client) {
|
||
|
|
document.documentElement.className = `theme-${theme}`
|
||
|
|
|
||
|
|
// Persist to localStorage
|
||
|
|
localStorage.setItem('harborsmith-theme', theme)
|
||
|
|
|
||
|
|
// Also set cookie for SSR
|
||
|
|
const cookie = useCookie('harborsmith-theme', {
|
||
|
|
httpOnly: false,
|
||
|
|
sameSite: 'strict',
|
||
|
|
maxAge: 60 * 60 * 24 * 365 // 1 year
|
||
|
|
})
|
||
|
|
cookie.value = theme
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
loadTheme() {
|
||
|
|
if (process.client) {
|
||
|
|
const saved = localStorage.getItem('harborsmith-theme') ||
|
||
|
|
useCookie('harborsmith-theme').value
|
||
|
|
|
||
|
|
if (saved && this.themes.find(t => t.id === saved)) {
|
||
|
|
this.setTheme(saved as ThemeVariant)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### Theme Switcher Component
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<!-- components/ui/ThemeSwitcher.vue -->
|
||
|
|
<template>
|
||
|
|
<UDropdown
|
||
|
|
:items="themeMenuItems"
|
||
|
|
:popper="{ placement: 'bottom-end' }"
|
||
|
|
:ui="{ item: { disabled: 'cursor-default opacity-60' } }"
|
||
|
|
>
|
||
|
|
<UButton
|
||
|
|
:color="scrolled ? 'gray' : 'white'"
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
:icon="currentThemeIcon"
|
||
|
|
:class="!scrolled && 'hover:bg-white/10'"
|
||
|
|
>
|
||
|
|
<template #trailing>
|
||
|
|
<Icon name="lucide:chevron-down" class="w-4 h-4 ml-1" />
|
||
|
|
</template>
|
||
|
|
</UButton>
|
||
|
|
|
||
|
|
<template #item="{ item }">
|
||
|
|
<div class="flex items-center justify-between w-full">
|
||
|
|
<div class="flex items-center gap-2">
|
||
|
|
<div class="flex gap-1">
|
||
|
|
<span
|
||
|
|
v-for="color in item.colors"
|
||
|
|
:key="color"
|
||
|
|
:style="{ backgroundColor: color }"
|
||
|
|
class="w-3 h-3 rounded-full border border-gray-200"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
<span>{{ item.label }}</span>
|
||
|
|
</div>
|
||
|
|
<Icon
|
||
|
|
v-if="item.active"
|
||
|
|
name="lucide:check"
|
||
|
|
class="w-4 h-4 text-accent"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
</UDropdown>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
import type { DropdownItem } from '#ui/types'
|
||
|
|
|
||
|
|
const props = defineProps<{
|
||
|
|
scrolled?: boolean
|
||
|
|
}>()
|
||
|
|
|
||
|
|
const themeStore = useThemeStore()
|
||
|
|
|
||
|
|
const currentThemeIcon = computed(() => {
|
||
|
|
const icons = {
|
||
|
|
'nautical': 'lucide:anchor',
|
||
|
|
'coastal-dawn': 'lucide:sunrise',
|
||
|
|
'deep-sea': 'lucide:waves',
|
||
|
|
'monaco': 'lucide:sun'
|
||
|
|
}
|
||
|
|
return icons[themeStore.currentTheme]
|
||
|
|
})
|
||
|
|
|
||
|
|
const themeMenuItems = computed<DropdownItem[][]>(() => [[
|
||
|
|
{
|
||
|
|
label: 'Classic Nautical',
|
||
|
|
colors: ['#001f3f', '#dc143c', '#ffffff'],
|
||
|
|
active: themeStore.currentTheme === 'nautical',
|
||
|
|
click: () => themeStore.setTheme('nautical')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: 'Coastal Dawn',
|
||
|
|
colors: ['#A9B4C2', '#D4AF37', '#F8F7F4'],
|
||
|
|
active: themeStore.currentTheme === 'coastal-dawn',
|
||
|
|
click: () => themeStore.setTheme('coastal-dawn')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: 'Deep Sea',
|
||
|
|
colors: ['#1E2022', '#00BFFF', '#2A2D30'],
|
||
|
|
active: themeStore.currentTheme === 'deep-sea',
|
||
|
|
click: () => themeStore.setTheme('deep-sea')
|
||
|
|
},
|
||
|
|
{
|
||
|
|
label: 'Monaco White',
|
||
|
|
colors: ['#2C3E50', '#E74C3C', '#F8F9FA'],
|
||
|
|
active: themeStore.currentTheme === 'monaco',
|
||
|
|
click: () => themeStore.setTheme('monaco')
|
||
|
|
}
|
||
|
|
]])
|
||
|
|
</script>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Performance Optimizations
|
||
|
|
|
||
|
|
### 1. Image Optimization Strategy
|
||
|
|
|
||
|
|
```vue
|
||
|
|
<!-- components/ui/OptimizedImage.vue -->
|
||
|
|
<template>
|
||
|
|
<div class="relative overflow-hidden" :class="containerClass">
|
||
|
|
<!-- Blur placeholder -->
|
||
|
|
<div
|
||
|
|
v-if="!loaded"
|
||
|
|
class="absolute inset-0 bg-gradient-to-br from-gray-200 to-gray-300 animate-pulse"
|
||
|
|
/>
|
||
|
|
|
||
|
|
<!-- Main image -->
|
||
|
|
<NuxtImg
|
||
|
|
:src="src"
|
||
|
|
:alt="alt"
|
||
|
|
:loading="loading"
|
||
|
|
:quality="quality"
|
||
|
|
:format="format"
|
||
|
|
:sizes="sizes"
|
||
|
|
@load="onLoad"
|
||
|
|
class="w-full h-full object-cover transition-opacity duration-500"
|
||
|
|
:class="{ 'opacity-0': !loaded }"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup lang="ts">
|
||
|
|
interface Props {
|
||
|
|
src: string
|
||
|
|
alt: string
|
||
|
|
loading?: 'lazy' | 'eager'
|
||
|
|
quality?: number
|
||
|
|
format?: string
|
||
|
|
sizes?: string
|
||
|
|
containerClass?: string
|
||
|
|
}
|
||
|
|
|
||
|
|
const props = withDefaults(defineProps<Props>(), {
|
||
|
|
loading: 'lazy',
|
||
|
|
quality: 80,
|
||
|
|
format: 'webp',
|
||
|
|
sizes: '100vw'
|
||
|
|
})
|
||
|
|
|
||
|
|
const loaded = ref(false)
|
||
|
|
const onLoad = () => loaded.value = true
|
||
|
|
</script>
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Scroll Animation Composable
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// composables/useScrollEffects.ts
|
||
|
|
import { useMotion } from '@vueuse/motion'
|
||
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||
|
|
|
||
|
|
export const useScrollEffects = () => {
|
||
|
|
const elements = ref<HTMLElement[]>([])
|
||
|
|
const observer = ref<IntersectionObserver>()
|
||
|
|
|
||
|
|
const fadeInUp = {
|
||
|
|
initial: {
|
||
|
|
opacity: 0,
|
||
|
|
y: 50
|
||
|
|
},
|
||
|
|
enter: {
|
||
|
|
opacity: 1,
|
||
|
|
y: 0,
|
||
|
|
transition: {
|
||
|
|
duration: 800,
|
||
|
|
ease: [0.4, 0, 0.2, 1]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const fadeInScale = {
|
||
|
|
initial: {
|
||
|
|
opacity: 0,
|
||
|
|
scale: 0.95
|
||
|
|
},
|
||
|
|
enter: {
|
||
|
|
opacity: 1,
|
||
|
|
scale: 1,
|
||
|
|
transition: {
|
||
|
|
duration: 600,
|
||
|
|
ease: [0.4, 0, 0.2, 1]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const initScrollAnimations = () => {
|
||
|
|
observer.value = new IntersectionObserver(
|
||
|
|
(entries) => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting) {
|
||
|
|
entry.target.classList.add('animate-visible')
|
||
|
|
observer.value?.unobserve(entry.target)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
},
|
||
|
|
{
|
||
|
|
threshold: 0.1,
|
||
|
|
rootMargin: '50px'
|
||
|
|
}
|
||
|
|
)
|
||
|
|
|
||
|
|
// Find all elements with data-animate attribute
|
||
|
|
elements.value = Array.from(
|
||
|
|
document.querySelectorAll('[data-animate]')
|
||
|
|
) as HTMLElement[]
|
||
|
|
|
||
|
|
elements.value.forEach(el => observer.value?.observe(el))
|
||
|
|
}
|
||
|
|
|
||
|
|
onMounted(() => initScrollAnimations())
|
||
|
|
onUnmounted(() => observer.value?.disconnect())
|
||
|
|
|
||
|
|
return {
|
||
|
|
fadeInUp,
|
||
|
|
fadeInScale
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Performance Monitoring
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// plugins/performance.client.ts
|
||
|
|
export default defineNuxtPlugin(() => {
|
||
|
|
if (process.dev) return
|
||
|
|
|
||
|
|
// Web Vitals monitoring
|
||
|
|
onNuxtReady(() => {
|
||
|
|
// First Contentful Paint
|
||
|
|
const paintObserver = new PerformanceObserver((list) => {
|
||
|
|
for (const entry of list.getEntries()) {
|
||
|
|
if (entry.name === 'first-contentful-paint') {
|
||
|
|
console.log('FCP:', entry.startTime)
|
||
|
|
// Send to analytics
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
paintObserver.observe({ entryTypes: ['paint'] })
|
||
|
|
|
||
|
|
// Largest Contentful Paint
|
||
|
|
const lcpObserver = new PerformanceObserver((list) => {
|
||
|
|
const entries = list.getEntries()
|
||
|
|
const lastEntry = entries[entries.length - 1]
|
||
|
|
console.log('LCP:', lastEntry.startTime)
|
||
|
|
// Send to analytics
|
||
|
|
})
|
||
|
|
lcpObserver.observe({ entryTypes: ['largest-contentful-paint'] })
|
||
|
|
|
||
|
|
// Cumulative Layout Shift
|
||
|
|
let clsValue = 0
|
||
|
|
const clsObserver = new PerformanceObserver((list) => {
|
||
|
|
for (const entry of list.getEntries()) {
|
||
|
|
if (!entry.hadRecentInput) {
|
||
|
|
clsValue += entry.value
|
||
|
|
console.log('CLS:', clsValue)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
clsObserver.observe({ entryTypes: ['layout-shift'] })
|
||
|
|
})
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Deployment Strategy
|
||
|
|
|
||
|
|
### Docker Configuration
|
||
|
|
|
||
|
|
```dockerfile
|
||
|
|
# Dockerfile
|
||
|
|
# Build stage
|
||
|
|
FROM node:20-alpine AS builder
|
||
|
|
|
||
|
|
# Set working directory
|
||
|
|
WORKDIR /app
|
||
|
|
|
||
|
|
# Copy package files
|
||
|
|
COPY package*.json ./
|
||
|
|
|
||
|
|
# Install dependencies
|
||
|
|
RUN npm ci --only=production
|
||
|
|
|
||
|
|
# Copy source code
|
||
|
|
COPY . .
|
||
|
|
|
||
|
|
# Build application
|
||
|
|
RUN npm run generate
|
||
|
|
|
||
|
|
# Production stage
|
||
|
|
FROM nginx:alpine
|
||
|
|
|
||
|
|
# Install nginx-module-brotli for better compression
|
||
|
|
RUN apk add --no-cache nginx-mod-http-brotli
|
||
|
|
|
||
|
|
# Copy built static files
|
||
|
|
COPY --from=builder /app/.output/public /usr/share/nginx/html
|
||
|
|
|
||
|
|
# Copy nginx configuration
|
||
|
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||
|
|
|
||
|
|
# Copy custom nginx site config
|
||
|
|
COPY default.conf /etc/nginx/conf.d/default.conf
|
||
|
|
|
||
|
|
# Expose port
|
||
|
|
EXPOSE 80
|
||
|
|
|
||
|
|
# Health check
|
||
|
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||
|
|
CMD wget --quiet --tries=1 --spider http://localhost/ || exit 1
|
||
|
|
|
||
|
|
# Start nginx
|
||
|
|
CMD ["nginx", "-g", "daemon off;"]
|
||
|
|
```
|
||
|
|
|
||
|
|
### nginx Configuration
|
||
|
|
|
||
|
|
```nginx
|
||
|
|
# nginx.conf
|
||
|
|
user nginx;
|
||
|
|
worker_processes auto;
|
||
|
|
error_log /var/log/nginx/error.log warn;
|
||
|
|
pid /var/run/nginx.pid;
|
||
|
|
|
||
|
|
events {
|
||
|
|
worker_connections 1024;
|
||
|
|
}
|
||
|
|
|
||
|
|
http {
|
||
|
|
include /etc/nginx/mime.types;
|
||
|
|
default_type application/octet-stream;
|
||
|
|
|
||
|
|
# Logging
|
||
|
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||
|
|
'$status $body_bytes_sent "$http_referer" '
|
||
|
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||
|
|
access_log /var/log/nginx/access.log main;
|
||
|
|
|
||
|
|
# Performance
|
||
|
|
sendfile on;
|
||
|
|
tcp_nopush on;
|
||
|
|
tcp_nodelay on;
|
||
|
|
keepalive_timeout 65;
|
||
|
|
types_hash_max_size 2048;
|
||
|
|
|
||
|
|
# Gzip compression
|
||
|
|
gzip on;
|
||
|
|
gzip_disable "msie6";
|
||
|
|
gzip_vary on;
|
||
|
|
gzip_proxied any;
|
||
|
|
gzip_comp_level 6;
|
||
|
|
gzip_types text/plain text/css text/xml text/javascript
|
||
|
|
application/json application/javascript application/xml+rss
|
||
|
|
application/rss+xml application/atom+xml image/svg+xml
|
||
|
|
text/x-js text/x-cross-domain-policy application/x-font-ttf
|
||
|
|
application/x-font-opentype application/vnd.ms-fontobject
|
||
|
|
image/x-icon;
|
||
|
|
|
||
|
|
# Brotli compression
|
||
|
|
brotli on;
|
||
|
|
brotli_comp_level 6;
|
||
|
|
brotli_types text/plain text/css text/xml text/javascript
|
||
|
|
application/json application/javascript application/xml+rss
|
||
|
|
application/rss+xml application/atom+xml image/svg+xml;
|
||
|
|
|
||
|
|
include /etc/nginx/conf.d/*.conf;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```nginx
|
||
|
|
# default.conf
|
||
|
|
server {
|
||
|
|
listen 80;
|
||
|
|
server_name localhost;
|
||
|
|
root /usr/share/nginx/html;
|
||
|
|
index index.html;
|
||
|
|
|
||
|
|
# Security headers
|
||
|
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||
|
|
add_header X-Content-Type-Options "nosniff" always;
|
||
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
||
|
|
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||
|
|
add_header Content-Security-Policy "default-src 'self' https:; script-src 'self' 'unsafe-inline' https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; media-src 'self' https://videos.pexels.com;" always;
|
||
|
|
|
||
|
|
# Cache static assets
|
||
|
|
location ~* \.(jpg|jpeg|png|gif|ico|css|js|woff|woff2|ttf|svg|webp)$ {
|
||
|
|
expires 1y;
|
||
|
|
add_header Cache-Control "public, immutable";
|
||
|
|
}
|
||
|
|
|
||
|
|
# Cache HTML (shorter duration)
|
||
|
|
location ~* \.(html)$ {
|
||
|
|
expires 1h;
|
||
|
|
add_header Cache-Control "public, must-revalidate";
|
||
|
|
}
|
||
|
|
|
||
|
|
# SPA fallback
|
||
|
|
location / {
|
||
|
|
try_files $uri $uri/ /index.html;
|
||
|
|
}
|
||
|
|
|
||
|
|
# Health check endpoint
|
||
|
|
location /health {
|
||
|
|
access_log off;
|
||
|
|
return 200 "OK";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Docker Compose (Development)
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
# docker-compose.yml
|
||
|
|
version: '3.8'
|
||
|
|
|
||
|
|
services:
|
||
|
|
web:
|
||
|
|
build:
|
||
|
|
context: .
|
||
|
|
dockerfile: Dockerfile
|
||
|
|
container_name: harborsmith-web
|
||
|
|
ports:
|
||
|
|
- "3001:80"
|
||
|
|
environment:
|
||
|
|
- NODE_ENV=production
|
||
|
|
restart: unless-stopped
|
||
|
|
healthcheck:
|
||
|
|
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
|
||
|
|
interval: 30s
|
||
|
|
timeout: 10s
|
||
|
|
retries: 3
|
||
|
|
networks:
|
||
|
|
- harborsmith
|
||
|
|
|
||
|
|
networks:
|
||
|
|
harborsmith:
|
||
|
|
driver: bridge
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Migration Checklist
|
||
|
|
|
||
|
|
### Pre-Migration
|
||
|
|
- [ ] Backup current mockups
|
||
|
|
- [ ] Document all custom styles
|
||
|
|
- [ ] List all JavaScript functionality
|
||
|
|
- [ ] Catalog all images and videos
|
||
|
|
- [ ] Note responsive breakpoints
|
||
|
|
|
||
|
|
### Phase 1: Setup
|
||
|
|
- [ ] Initialize Nuxt 3 project
|
||
|
|
- [ ] Configure TypeScript
|
||
|
|
- [ ] Install dependencies
|
||
|
|
- [ ] Setup Tailwind CSS v4
|
||
|
|
- [ ] Configure Nuxt modules
|
||
|
|
|
||
|
|
### Phase 2: Theme System
|
||
|
|
- [ ] Port CSS variables
|
||
|
|
- [ ] Create theme files
|
||
|
|
- [ ] Implement Pinia store
|
||
|
|
- [ ] Build theme switcher
|
||
|
|
- [ ] Test theme persistence
|
||
|
|
|
||
|
|
### Phase 3: Components
|
||
|
|
- [ ] Convert navigation
|
||
|
|
- [ ] Build video hero
|
||
|
|
- [ ] Create button components
|
||
|
|
- [ ] Port trust badges
|
||
|
|
- [ ] Convert fleet grid
|
||
|
|
- [ ] Build testimonials
|
||
|
|
- [ ] Add footer
|
||
|
|
|
||
|
|
### Phase 4: Content
|
||
|
|
- [ ] Migrate images
|
||
|
|
- [ ] Optimize videos
|
||
|
|
- [ ] Add meta tags
|
||
|
|
- [ ] Setup analytics
|
||
|
|
- [ ] Configure sitemap
|
||
|
|
|
||
|
|
### Phase 5: Optimization
|
||
|
|
- [ ] Run Lighthouse audit
|
||
|
|
- [ ] Optimize bundle size
|
||
|
|
- [ ] Test all viewports
|
||
|
|
- [ ] Check accessibility
|
||
|
|
- [ ] Validate SEO
|
||
|
|
|
||
|
|
### Phase 6: Deployment
|
||
|
|
- [ ] Build Docker image
|
||
|
|
- [ ] Configure nginx
|
||
|
|
- [ ] Setup CI/CD
|
||
|
|
- [ ] Deploy to staging
|
||
|
|
- [ ] Production deployment
|
||
|
|
|
||
|
|
### Post-Migration
|
||
|
|
- [ ] Performance monitoring
|
||
|
|
- [ ] Error tracking
|
||
|
|
- [ ] User testing
|
||
|
|
- [ ] Documentation
|
||
|
|
- [ ] Team training
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Testing Strategy
|
||
|
|
|
||
|
|
### Unit Tests (Vitest)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// components/ui/ThemedButton.test.ts
|
||
|
|
import { describe, it, expect } from 'vitest'
|
||
|
|
import { mount } from '@vue/test-utils'
|
||
|
|
import ThemedButton from './ThemedButton.vue'
|
||
|
|
|
||
|
|
describe('ThemedButton', () => {
|
||
|
|
it('renders with primary variant', () => {
|
||
|
|
const wrapper = mount(ThemedButton, {
|
||
|
|
props: {
|
||
|
|
variant: 'primary'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
expect(wrapper.classes()).toContain('bg-gradient-warm')
|
||
|
|
})
|
||
|
|
|
||
|
|
it('emits click event', async () => {
|
||
|
|
const wrapper = mount(ThemedButton)
|
||
|
|
await wrapper.trigger('click')
|
||
|
|
|
||
|
|
expect(wrapper.emitted('click')).toHaveLength(1)
|
||
|
|
})
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
### E2E Tests (Playwright)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// tests/e2e/landing.spec.ts
|
||
|
|
import { test, expect } from '@playwright/test'
|
||
|
|
|
||
|
|
test.describe('Landing Page', () => {
|
||
|
|
test('loads with video hero', async ({ page }) => {
|
||
|
|
await page.goto('/')
|
||
|
|
|
||
|
|
// Check hero section
|
||
|
|
const hero = await page.locator('.hero-voyage')
|
||
|
|
await expect(hero).toBeVisible()
|
||
|
|
|
||
|
|
// Check video or fallback image
|
||
|
|
const video = page.locator('video')
|
||
|
|
const hasVideo = await video.count() > 0
|
||
|
|
|
||
|
|
if (hasVideo) {
|
||
|
|
await expect(video).toHaveAttribute('autoplay')
|
||
|
|
} else {
|
||
|
|
const fallbackImage = page.locator('.hero-voyage img')
|
||
|
|
await expect(fallbackImage).toBeVisible()
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
test('theme switcher works', async ({ page }) => {
|
||
|
|
await page.goto('/')
|
||
|
|
|
||
|
|
// Open theme menu
|
||
|
|
await page.click('[data-testid="theme-switcher"]')
|
||
|
|
|
||
|
|
// Select coastal dawn theme
|
||
|
|
await page.click('text=Coastal Dawn')
|
||
|
|
|
||
|
|
// Check theme applied
|
||
|
|
const html = page.locator('html')
|
||
|
|
await expect(html).toHaveClass('theme-coastal-dawn')
|
||
|
|
|
||
|
|
// Check persistence
|
||
|
|
await page.reload()
|
||
|
|
await expect(html).toHaveClass('theme-coastal-dawn')
|
||
|
|
})
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Success Metrics
|
||
|
|
|
||
|
|
### Performance Targets
|
||
|
|
- **Lighthouse Score**: > 95 (all categories)
|
||
|
|
- **First Contentful Paint**: < 1.2s
|
||
|
|
- **Largest Contentful Paint**: < 2.5s
|
||
|
|
- **Time to Interactive**: < 3.5s
|
||
|
|
- **Cumulative Layout Shift**: < 0.1
|
||
|
|
- **JavaScript Bundle**: < 200KB (initial)
|
||
|
|
- **CSS Bundle**: < 50KB
|
||
|
|
|
||
|
|
### Quality Metrics
|
||
|
|
- **TypeScript Coverage**: 100%
|
||
|
|
- **Test Coverage**: > 80%
|
||
|
|
- **Accessibility**: WCAG AA compliant
|
||
|
|
- **SEO Score**: 100
|
||
|
|
- **Mobile Score**: 100
|
||
|
|
|
||
|
|
### Business Metrics
|
||
|
|
- **Conversion Rate**: Track booking CTAs
|
||
|
|
- **Engagement**: Time on site, scroll depth
|
||
|
|
- **Theme Usage**: Analytics on theme preferences
|
||
|
|
- **Performance**: Real user monitoring
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Maintenance & Updates
|
||
|
|
|
||
|
|
### Regular Tasks
|
||
|
|
- Update dependencies monthly
|
||
|
|
- Review performance metrics weekly
|
||
|
|
- Monitor error logs daily
|
||
|
|
- Backup before major changes
|
||
|
|
- Document all customizations
|
||
|
|
|
||
|
|
### Optimization Opportunities
|
||
|
|
- Implement service worker for offline
|
||
|
|
- Add PWA capabilities
|
||
|
|
- Integrate with CDN
|
||
|
|
- Implement A/B testing
|
||
|
|
- Add personalization features
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Conclusion
|
||
|
|
|
||
|
|
This comprehensive plan provides a clear roadmap for converting the HarborSmith landing page from static HTML to a modern Nuxt 3 application. The approach preserves the sophisticated design while implementing best practices for performance, maintainability, and developer experience.
|
||
|
|
|
||
|
|
The hybrid strategy of using Tailwind for utilities with CSS custom properties for theming ensures flexibility while maintaining the exact visual design. The component-based architecture with TypeScript provides type safety and reusability for future development.
|
||
|
|
|
||
|
|
Following this plan will result in a production-ready landing page that loads in under 1 second, maintains perfect visual parity with the mockups, and provides a solid foundation for the entire HarborSmith web platform.
|