fix: resolve Next.js build failure by removing Vue archive
All checks were successful
build-website / build (push) Successful in 2m37s
- Removed vue-archive directory containing old Nuxt code - Removed harborsmith-nextjs temporary directory - Updated tsconfig.json to exclude archived directories - Updated .dockerignore to exclude archived code - Updated .gitignore with proper Next.js patterns - Updated README.md to reflect current project structure Build now completes successfully without TypeScript errors. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
@@ -47,3 +47,7 @@ docker-compose.test.yml
|
||||
tmp
|
||||
temp
|
||||
*.tmp
|
||||
|
||||
# Archived code
|
||||
vue-archive
|
||||
harborsmith-nextjs
|
||||
|
||||
7
.gitignore
vendored
@@ -32,3 +32,10 @@ Website-PDF-Mockups/
|
||||
|
||||
apps/website/nul
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
|
||||
# Archived/temp directories
|
||||
harborsmith-nextjs
|
||||
vue-archive
|
||||
|
||||
|
||||
@@ -54,8 +54,7 @@ Open [http://localhost:3000](http://localhost:3000) with your browser.
|
||||
├── app/ # Next.js app directory
|
||||
├── components/ # React components
|
||||
├── public/ # Static assets (images, videos)
|
||||
├── styles/ # Global styles
|
||||
└── vue-archive/ # Archived Vue/Nuxt version
|
||||
└── styles/ # Global styles
|
||||
```
|
||||
|
||||
## Deployment
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "vue-archive", "harborsmith-nextjs"]
|
||||
}
|
||||
24
vue-archive/apps/website/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -1,38 +0,0 @@
|
||||
# Multi-stage build for Harbor Smith website
|
||||
|
||||
# Stage 1: Build
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production && \
|
||||
npm cache clean --force
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Build the Nuxt application for static generation
|
||||
RUN npm run generate
|
||||
|
||||
# Stage 2: Production
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy custom nginx config
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
# Copy built static files from builder stage
|
||||
COPY --from=builder /app/.output/public /usr/share/nginx/html
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost || exit 1
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
@@ -1,2 +0,0 @@
|
||||
FROM nginx:1.27-alpine
|
||||
COPY .output/public /usr/share/nginx/html
|
||||
@@ -1,75 +0,0 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
@@ -1,608 +0,0 @@
|
||||
/* Harbor Smith Main Styles */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Reset styles for iOS notch video extension */
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
/* Force transparent background to allow video in safe area */
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Ensure all wrapper elements are also transparent */
|
||||
#__nuxt, .app-layout, main {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Smooth Scroll Behavior and iOS Overscroll Prevention */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
overscroll-behavior-y: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
/* Proper iOS viewport handling */
|
||||
height: 100%;
|
||||
height: -webkit-fill-available;
|
||||
/* Allow video to extend horizontally */
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
body {
|
||||
overscroll-behavior-y: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
/* Prevent double-tap zoom on mobile */
|
||||
touch-action: manipulation;
|
||||
/* Allow video to extend horizontally */
|
||||
max-width: 100vw !important;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* iOS-specific viewport handling */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
/* iOS Safari */
|
||||
.hero-voyage {
|
||||
height: 100vh;
|
||||
height: -webkit-fill-available;
|
||||
height: 100dvh;
|
||||
}
|
||||
|
||||
/* Removed conflicting height calculation for hero-video-container
|
||||
The video container should fill the viewport, not exceed it */
|
||||
}
|
||||
|
||||
/* Handle dynamic viewport on mobile browsers */
|
||||
@media screen and (max-width: 768px) {
|
||||
@supports (height: 100dvh) {
|
||||
.hero-voyage {
|
||||
height: 100dvh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
min-height: 100dvh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Hardware Acceleration for Smooth Animations */
|
||||
.hero-video-container,
|
||||
.parallax,
|
||||
.animate-fade-in,
|
||||
.animate-fade-up-delay,
|
||||
.animate-fade-up-delay-2,
|
||||
.animate-slide-up,
|
||||
.animate-scale-in,
|
||||
.animate-wave {
|
||||
will-change: transform, opacity;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
-webkit-perspective: 1000px;
|
||||
perspective: 1000px;
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
/* Optimize scroll performance */
|
||||
/* Removed contain property to allow video to extend into safe area */
|
||||
|
||||
/* Accessibility: Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
will-change: auto !important;
|
||||
}
|
||||
|
||||
.hero-video-container,
|
||||
.parallax {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom Properties */
|
||||
@layer base {
|
||||
:root {
|
||||
/* Primary Colors */
|
||||
--primary-blue: #001f3f;
|
||||
--harbor-navy: #1e3a5f;
|
||||
--harbor-gold: #b48b4e;
|
||||
--harbor-amber: #9d7943;
|
||||
--harbor-yellow: #c9a56f;
|
||||
--harbor-light: #f0f0f0;
|
||||
|
||||
/* Gradients */
|
||||
--gradient-warm: linear-gradient(135deg, #b48b4e 0%, #c9a56f 100%);
|
||||
--gradient-blue: linear-gradient(135deg, #001f3f 0%, #1e3a5f 100%);
|
||||
|
||||
/* Responsive Spacing Scale */
|
||||
--space-xs: clamp(0.5rem, 2vw, 0.75rem);
|
||||
--space-sm: clamp(1rem, 3vw, 1.5rem);
|
||||
--space-md: clamp(1.5rem, 4vw, 2rem);
|
||||
--space-lg: clamp(2rem, 5vw, 3rem);
|
||||
--space-xl: clamp(3rem, 6vw, 4rem);
|
||||
--space-2xl: clamp(4rem, 8vw, 6rem);
|
||||
|
||||
/* Fluid Typography */
|
||||
--text-xs: clamp(0.75rem, 2vw, 0.875rem);
|
||||
--text-sm: clamp(0.875rem, 2.5vw, 1rem);
|
||||
--text-base: clamp(1rem, 2.5vw, 1.125rem);
|
||||
--text-lg: clamp(1.125rem, 3vw, 1.25rem);
|
||||
--text-xl: clamp(1.25rem, 3.5vw, 1.5rem);
|
||||
--text-2xl: clamp(1.5rem, 4vw, 1.875rem);
|
||||
--text-3xl: clamp(1.875rem, 5vw, 2.25rem);
|
||||
--text-4xl: clamp(2.25rem, 6vw, 3rem);
|
||||
--text-5xl: clamp(3rem, 7vw, 3.75rem);
|
||||
}
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
@layer base {
|
||||
body {
|
||||
/* Removed @apply to prevent Tailwind from adding default backgrounds */
|
||||
font-family: Inter, sans-serif;
|
||||
color: #1f2937;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
@apply font-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-5xl md:text-6xl lg:text-7xl font-bold;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-4xl md:text-5xl font-bold;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-3xl md:text-4xl font-bold;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-2xl md:text-3xl font-semibold;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-xl md:text-2xl font-semibold;
|
||||
}
|
||||
|
||||
h6 {
|
||||
@apply text-lg md:text-xl font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
/* Page Transitions */
|
||||
.page-enter-active,
|
||||
.page-leave-active {
|
||||
transition: all 0.4s;
|
||||
}
|
||||
.page-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
.page-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
|
||||
.layout-enter-active,
|
||||
.layout-leave-active {
|
||||
transition: all 0.4s;
|
||||
}
|
||||
.layout-enter-from {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.layout-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* Harbor Smith Custom Components */
|
||||
@layer components {
|
||||
/* Navigation Styles handled via voyage-layout.css */
|
||||
/* Button Styles - Optimized for Mobile Touch Targets (min 48x48px) */
|
||||
.btn-primary-warm {
|
||||
@apply px-8 py-3 bg-gradient-to-r from-harbor-gold to-harbor-yellow text-white font-semibold rounded-full;
|
||||
@apply hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-300;
|
||||
@apply relative overflow-hidden;
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.btn-secondary-warm {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1.75rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #ffffff;
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
backdrop-filter: blur(12px);
|
||||
transition: all 0.3s ease;
|
||||
min-height: 48px;
|
||||
min-width: 48px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.btn-secondary-warm:hover {
|
||||
background: rgba(255, 255, 255, 0.24);
|
||||
border-color: rgba(255, 255, 255, 0.55);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 30px rgba(12, 35, 64, 0.25);
|
||||
}
|
||||
|
||||
.btn-booking {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #ffffff;
|
||||
border: 2px solid rgba(255, 255, 255, 0.35);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.btn-booking:hover {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
color: #10213c;
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.btn-booking.primary {
|
||||
background: #ffffff;
|
||||
color: #b48b4e;
|
||||
border-color: #ffffff;
|
||||
box-shadow: 0 10px 25px rgba(180, 139, 78, 0.25);
|
||||
}
|
||||
|
||||
.btn-booking.primary:hover {
|
||||
background: linear-gradient(135deg, #b48b4e 0%, #c9a56f 100%);
|
||||
color: #ffffff;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.btn-booking.secondary {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #ffffff;
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.btn-booking.secondary:hover {
|
||||
background: #ffffff;
|
||||
color: #10213c;
|
||||
border-color: #ffffff;
|
||||
}
|
||||
|
||||
/* Card Styles */
|
||||
.story-card {
|
||||
@apply bg-white rounded-xl overflow-hidden shadow-lg;
|
||||
@apply transform transition-all duration-500 hover:-translate-y-2 hover:shadow-2xl;
|
||||
}
|
||||
|
||||
.booking-card {
|
||||
@apply bg-white rounded-2xl p-8 shadow-xl;
|
||||
@apply transform transition-all duration-500;
|
||||
}
|
||||
|
||||
.booking-card.featured {
|
||||
@apply scale-105 border-4 border-harbor-gold;
|
||||
box-shadow: 0 20px 40px rgba(180, 139, 78, 0.2);
|
||||
}
|
||||
|
||||
.yacht-card {
|
||||
@apply bg-white rounded-2xl overflow-hidden shadow-xl;
|
||||
@apply opacity-0 scale-95 transition-all duration-700;
|
||||
}
|
||||
|
||||
.yacht-card.active {
|
||||
@apply opacity-100 scale-100;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
/* Note: hero-video-container positioning is handled in voyage-layout.css
|
||||
for proper mobile/desktop behavior and iOS notch support */
|
||||
.hero-video-container {
|
||||
/* @apply absolute inset-0 w-full h-full overflow-hidden; */
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
@apply absolute inset-0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Hero Logo Styles */
|
||||
.hero-logo img {
|
||||
height: 250px;
|
||||
margin: 40px 0;
|
||||
width: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-logo img {
|
||||
height: auto;
|
||||
width: clamp(260px, 80vw, 360px);
|
||||
max-height: clamp(160px, 45vw, 220px);
|
||||
margin: 0 auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* Proper hero sizing on mobile */
|
||||
.hero-content {
|
||||
padding: var(--space-sm);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Ensure touch targets are at least 48px for better mobile UX */
|
||||
button, .btn-primary-warm, .btn-secondary-warm {
|
||||
min-height: 50px;
|
||||
min-width: 50px;
|
||||
font-size: var(--text-base);
|
||||
padding: 14px clamp(20px, 5vw, 32px);
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-warm {
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(0, 31, 63, 0.3) 0%,
|
||||
rgba(0, 31, 63, 0.5) 50%,
|
||||
rgba(0, 31, 63, 0.7) 100%);
|
||||
}
|
||||
|
||||
.gradient-depth {
|
||||
background: linear-gradient(to right,
|
||||
rgba(220, 20, 60, 0.1) 0%,
|
||||
transparent 100%);
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
@apply relative z-10 text-white text-center;
|
||||
}
|
||||
|
||||
/* Fleet Carousel */
|
||||
.fleet-nav {
|
||||
@apply absolute top-1/2 -translate-y-1/2 z-10;
|
||||
@apply w-12 h-12 bg-white/90 rounded-full shadow-xl;
|
||||
@apply flex items-center justify-center cursor-pointer;
|
||||
@apply hover:bg-white hover:scale-110 transition-all duration-300;
|
||||
}
|
||||
|
||||
.fleet-prev {
|
||||
@apply fleet-nav left-4;
|
||||
}
|
||||
|
||||
.fleet-next {
|
||||
@apply fleet-nav right-4;
|
||||
}
|
||||
|
||||
.fleet-dots {
|
||||
@apply flex gap-3 justify-center mt-8;
|
||||
}
|
||||
|
||||
.dot {
|
||||
@apply w-3 h-3 rounded-full bg-gray-300 cursor-pointer;
|
||||
@apply transition-all duration-300 hover:bg-harbor-gold;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
@apply bg-harbor-gold w-8;
|
||||
}
|
||||
|
||||
/* Trust Badge */
|
||||
.trust-badge {
|
||||
@apply inline-flex items-center gap-2 px-4 py-2 bg-harbor-gold/20 backdrop-blur-sm rounded-full;
|
||||
}
|
||||
|
||||
/* Section Styles */
|
||||
.section-title {
|
||||
@apply text-3xl md:text-5xl lg:text-6xl font-serif font-bold text-harbor-navy;
|
||||
@apply mb-4;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
@apply text-lg md:text-2xl text-gray-600 mb-8 md:mb-12;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-title {
|
||||
font-size: 1.875rem;
|
||||
line-height: 1.2;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation Utilities */
|
||||
@layer utilities {
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-up-delay {
|
||||
animation: fadeUp 1s ease-out 0.3s both;
|
||||
}
|
||||
|
||||
.animate-fade-up-delay-2 {
|
||||
animation: fadeUp 1s ease-out 0.6s both;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-scale-in {
|
||||
animation: scaleIn 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-wave {
|
||||
animation: wave 10s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-20px); }
|
||||
}
|
||||
}
|
||||
|
||||
/* Scrollbar Styles */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--harbor-gold);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--harbor-amber);
|
||||
}
|
||||
|
||||
/* Ripple Effect for Buttons */
|
||||
@keyframes ripple {
|
||||
to {
|
||||
transform: scale(4);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
transform: scale(0);
|
||||
animation: ripple 0.6s ease-out;
|
||||
pointer-events: none;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
/* Gold Drop Shadows */
|
||||
.shadow-gold {
|
||||
box-shadow: 0 4px 15px rgba(180, 139, 78, 0.2);
|
||||
}
|
||||
|
||||
.shadow-gold-lg {
|
||||
box-shadow: 0 10px 25px rgba(180, 139, 78, 0.25);
|
||||
}
|
||||
|
||||
.shadow-gold-xl {
|
||||
box-shadow: 0 20px 40px rgba(180, 139, 78, 0.3);
|
||||
}
|
||||
|
||||
/* Apply gold shadows to key elements */
|
||||
.btn-primary-warm,
|
||||
.btn-secondary-warm {
|
||||
box-shadow: 0 4px 15px rgba(180, 139, 78, 0.2);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.btn-primary-warm:hover,
|
||||
.btn-secondary-warm:hover {
|
||||
box-shadow: 0 8px 25px rgba(180, 139, 78, 0.35);
|
||||
}
|
||||
|
||||
.story-card,
|
||||
.booking-card,
|
||||
.service-card {
|
||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.story-card:hover,
|
||||
.booking-card:hover,
|
||||
.service-card:hover {
|
||||
box-shadow: 0 20px 40px rgba(180, 139, 78, 0.25);
|
||||
}
|
||||
|
||||
/* Video Background Optimization */
|
||||
.hero-video {
|
||||
@apply absolute w-full h-full object-cover;
|
||||
/* Removed transform scale to prevent overflow */
|
||||
/* transform: scale(1.1); */
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.skeleton {
|
||||
@apply bg-gray-200 animate-pulse rounded;
|
||||
}
|
||||
|
||||
/* Responsive Typography Scale */
|
||||
@media (max-width: 768px) {
|
||||
html {
|
||||
font-size: 16px; /* Keep base font size for readability */
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
/* Ensure all containers use full width */
|
||||
.container,
|
||||
.content-wrapper,
|
||||
main {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
html {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
/* HarborSmith - Theme Styles */
|
||||
/* ========================= */
|
||||
|
||||
/* Classical Nautical Theme (Default) - Navy & Crimson */
|
||||
[data-theme="nautical"] {
|
||||
--primary: #001f3f; /* Classic navy blue */
|
||||
--accent: #dc143c; /* Nautical red/crimson */
|
||||
--background: #ffffff;
|
||||
--surface: #f0f4f8;
|
||||
--text: #0a1628;
|
||||
--text-secondary: #4a5568;
|
||||
--border: #cbd5e0;
|
||||
--overlay: rgba(0, 31, 63, 0.7);
|
||||
--gradient-start: #001f3f;
|
||||
--gradient-end: #003366;
|
||||
}
|
||||
|
||||
/* Gold Theme - Navy with Gold accents */
|
||||
[data-theme="gold"] {
|
||||
--primary: #001f3f; /* Classic navy blue */
|
||||
--accent: #bc970c; /* Gold */
|
||||
--background: #ffffff;
|
||||
--surface: #f0f4f8;
|
||||
--text: #0a1628;
|
||||
--text-secondary: #4a5568;
|
||||
--border: #cbd5e0;
|
||||
--overlay: rgba(0, 31, 63, 0.7);
|
||||
--gradient-start: #001f3f;
|
||||
--gradient-end: #003366;
|
||||
}
|
||||
|
||||
/* Coastal Dawn Theme - Soft & Serene Luxury */
|
||||
[data-theme="coastal-dawn"] {
|
||||
--primary: #A9B4C2; /* Cadet Blue */
|
||||
--accent: #D4AF37; /* Gilded Gold */
|
||||
--background: #F8F7F4; /* Alabaster White */
|
||||
--surface: #FFFFFF;
|
||||
--text: #333745; /* Charcoal Slate */
|
||||
--text-secondary: #6B7280;
|
||||
--border: #E5E7EB;
|
||||
--overlay: rgba(169, 180, 194, 0.4);
|
||||
--gradient-start: #A9B4C2;
|
||||
--gradient-end: #C5D3E0;
|
||||
}
|
||||
|
||||
/* Deep Sea Slate Theme - Modern & Technical */
|
||||
[data-theme="deep-sea"] {
|
||||
--primary: #1E2022; /* Gunmetal Grey */
|
||||
--accent: #00BFFF; /* Deep Sky Blue */
|
||||
--background: #1E2022; /* Dark background */
|
||||
--surface: #2A2D30;
|
||||
--text: #E5E4E2; /* Platinum text */
|
||||
--text-secondary: #C0C0C0; /* Silver */
|
||||
--border: #3A3D40;
|
||||
--overlay: rgba(30, 32, 34, 0.8);
|
||||
--gradient-start: #1E2022;
|
||||
--gradient-end: #2A2D30;
|
||||
}
|
||||
|
||||
/* Monaco White Theme - Pristine & Minimalist */
|
||||
[data-theme="monaco-white"] {
|
||||
--primary: #2C3E50; /* Midnight Blue */
|
||||
--accent: #E74C3C; /* Pomegranate Red */
|
||||
--background: #FFFFFF;
|
||||
--surface: #F8F9FA;
|
||||
--text: #2C3E50;
|
||||
--text-secondary: #7F8C8D;
|
||||
--border: #ECF0F1;
|
||||
--overlay: rgba(44, 62, 80, 0.05);
|
||||
--gradient-start: #FFFFFF;
|
||||
--gradient-end: #F8F9FA;
|
||||
}
|
||||
|
||||
/* Update hero gradient for all themes */
|
||||
[data-theme] .hero-background {
|
||||
background: linear-gradient(135deg, var(--gradient-start) 0%, var(--gradient-end) 100%);
|
||||
}
|
||||
|
||||
/* Ensure good contrast for nav items */
|
||||
[data-theme] .nav-link {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
[data-theme] .nav-link:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Theme switcher button styling */
|
||||
[data-theme] .theme-switcher {
|
||||
background: var(--surface);
|
||||
border: 2px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
[data-theme] .theme-switcher:hover {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Better button contrast */
|
||||
[data-theme] .btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
[data-theme] .btn-primary:hover {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Schedule Service button - make it filled */
|
||||
[data-theme] .btn-secondary {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: 2px solid var(--primary);
|
||||
}
|
||||
|
||||
[data-theme] .btn-secondary:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Card improvements */
|
||||
[data-theme] .service-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
[data-theme] .yacht-card {
|
||||
background: white;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Stats section contrast */
|
||||
[data-theme] .stats-section {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* Footer styling */
|
||||
[data-theme] footer {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
[data-theme] footer a {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
[data-theme] footer a:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
/* Special styling for Classical Nautical theme */
|
||||
[data-theme="nautical"] .btn-primary {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 4px 6px rgba(220, 20, 60, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="nautical"] .btn-secondary {
|
||||
background: var(--primary);
|
||||
box-shadow: 0 4px 6px rgba(0, 31, 63, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="nautical"] .service-card {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
[data-theme="nautical"] .yacht-card {
|
||||
border-top: 3px solid var(--accent);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<footer class="voyage-footer" id="contact">
|
||||
<div class="footer-container">
|
||||
<div class="footer-content">
|
||||
<div class="footer-brand">
|
||||
<img src="/HARBOR-SMITH-white.png" alt="Harbor Smith" class="footer-logo" width="50" height="50">
|
||||
<h3>HARBOR SMITH</h3>
|
||||
<p>Your trusted partner for professional boat maintenance in the San Francisco Bay Area</p>
|
||||
<div class="social-links">
|
||||
<a href="#" aria-label="Facebook"><LucideFacebook /></a>
|
||||
<a href="#" aria-label="Instagram"><LucideInstagram /></a>
|
||||
<a href="#" aria-label="Twitter"><LucideTwitter /></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-contact">
|
||||
<h4>Get in Touch</h4>
|
||||
<p><LucideMapPin class="footer-icon" /> San Francisco Bay Area</p>
|
||||
<p><LucidePhone class="footer-icon" /> <a href="tel:510-701-2535" style="color: inherit; text-decoration: none;">(510) 701-2535</a></p>
|
||||
<p><LucideMail class="footer-icon" /> <a href="mailto:hello@harborsmith.co" style="color: inherit; text-decoration: none;">hello@harborsmith.co</a></p>
|
||||
<p><LucideClock class="footer-icon" /> Mobile Service Available 7 Days</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom">
|
||||
<p>© 2025 Harbor Smith Boat Maintenance Services. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Footer uses globally registered Lucide icons
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Footer layout handled by voyage-layout.css */
|
||||
</style>
|
||||
@@ -1,89 +0,0 @@
|
||||
<template>
|
||||
<nav
|
||||
id="voyageNav"
|
||||
:class="['voyage-nav', { scrolled: isScrolled }]"
|
||||
>
|
||||
<div class="nav-container">
|
||||
<div class="nav-brand" @click="scrollToTop" style="cursor: pointer;">
|
||||
<img
|
||||
:src="navLogo"
|
||||
:alt="logoAlt"
|
||||
class="nav-logo"
|
||||
id="navLogo"
|
||||
width="150"
|
||||
height="50"
|
||||
>
|
||||
<span>HARBOR SMITH</span>
|
||||
</div>
|
||||
<div class="nav-links">
|
||||
<a
|
||||
v-for="link in navLinks"
|
||||
:key="link.href"
|
||||
:href="link.href"
|
||||
class="nav-link"
|
||||
@click="handleSmoothScroll($event, link.href)"
|
||||
>
|
||||
{{ link.label }}
|
||||
</a>
|
||||
<a
|
||||
href="tel:510-701-2535"
|
||||
class="nav-link nav-cta"
|
||||
>
|
||||
Call Now
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useSmoothScroll } from '~/composables/useSmoothScroll'
|
||||
|
||||
const { scrollToElement, scrollToTop: smoothScrollToTop } = useSmoothScroll()
|
||||
const isScrolled = ref(false)
|
||||
|
||||
const navLinks = [
|
||||
{ href: '#services', label: 'Services' },
|
||||
{ href: '#testimonials', label: 'Testimonials' },
|
||||
{ href: '#contact', label: 'Contact' }
|
||||
]
|
||||
|
||||
const navLogo = computed(() =>
|
||||
isScrolled.value ? '/HARBOR-SMITH_navy.png' : '/HARBOR-SMITH-white.png'
|
||||
)
|
||||
|
||||
const logoAlt = computed(() =>
|
||||
isScrolled.value ? 'Harbor Smith Navy Logo' : 'Harbor Smith White Logo'
|
||||
)
|
||||
|
||||
const handleScroll = () => {
|
||||
const currentOffset = window.scrollY || window.pageYOffset
|
||||
isScrolled.value = currentOffset > 50
|
||||
}
|
||||
|
||||
const handleSmoothScroll = (event: Event, href: string) => {
|
||||
if (!href.startsWith('#')) {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
scrollToElement(href, 800) // Constant-speed smooth scrolling
|
||||
}
|
||||
|
||||
const scrollToTop = () => {
|
||||
smoothScrollToTop(800) // Constant-speed smooth scrolling
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleScroll()
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Navigation styling handled in voyage-layout.css */
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<template>
|
||||
<section class="booking-cta" style="background: linear-gradient(135deg, rgba(30, 58, 95, 0.9), rgba(75, 124, 184, 0.85)), url('sf_bay_exposure.jpg') center/cover no-repeat; position: relative;">
|
||||
<div class="booking-container">
|
||||
<div class="booking-content">
|
||||
<h2 class="booking-title">Ready to Schedule Your Service?</h2>
|
||||
<p class="booking-subtitle">
|
||||
Join hundreds of boat owners who trust Harbor Smith for professional maintenance
|
||||
</p>
|
||||
|
||||
<div class="booking-options" style="justify-content: center; gap: 3rem;">
|
||||
<div class="booking-card featured">
|
||||
<span class="booking-icon">
|
||||
<LucidePhone />
|
||||
</span>
|
||||
<h3>Call Us Today</h3>
|
||||
<p>Get a personalized quote</p>
|
||||
<button class="btn-booking primary" @click="handleCall">Call (510) 701-2535</button>
|
||||
</div>
|
||||
|
||||
<div class="booking-card">
|
||||
<span class="booking-icon">
|
||||
<LucideMail />
|
||||
</span>
|
||||
<h3>Email Us</h3>
|
||||
<p>Send us your service request</p>
|
||||
<button class="btn-booking" @click="handleEmail">Contact Us</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const handleBookNow = () => {
|
||||
window.location.href = 'maintenance-booking.html'
|
||||
}
|
||||
|
||||
const handleCall = () => {
|
||||
window.location.href = 'tel:510-701-2535'
|
||||
}
|
||||
|
||||
const handleEmail = () => {
|
||||
window.location.href = 'mailto:hello@harborsmith.co'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Booking styles defined in voyage-layout.css */
|
||||
</style>
|
||||
@@ -1,56 +0,0 @@
|
||||
<template>
|
||||
<section class="gallery-section">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Our Work in Action</h2>
|
||||
<p class="section-subtitle">Professional maintenance services delivered with care</p>
|
||||
</div>
|
||||
<div class="image-gallery">
|
||||
<div class="gallery-item large">
|
||||
<img src="/diver_cleaning_2.jpg" alt="Professional hull cleaning" width="800" height="400" loading="lazy">
|
||||
<div class="gallery-overlay">
|
||||
<span class="gallery-caption">Expert Hull Cleaning</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-item">
|
||||
<img src="/ExtCleaning.jpg" alt="Exterior cleaning service" width="400" height="300" loading="lazy">
|
||||
<div class="gallery-overlay">
|
||||
<span class="gallery-caption">Detailed Cleaning</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-item">
|
||||
<img src="/Washdown2.jpg" alt="Professional washdown" width="400" height="300" loading="lazy">
|
||||
<div class="gallery-overlay">
|
||||
<span class="gallery-caption">Thorough Washdown</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-item">
|
||||
<img src="/Helm.jpg" alt="Interior maintenance" width="400" height="300" loading="lazy">
|
||||
<div class="gallery-overlay">
|
||||
<span class="gallery-caption">Interior Care</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-item">
|
||||
<img src="/Foredeck.jpg" alt="Deck maintenance" width="400" height="300" loading="lazy">
|
||||
<div class="gallery-overlay">
|
||||
<span class="gallery-caption">Deck Service</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-item">
|
||||
<img src="/Waxing.jpg" alt="Boat waxing service" width="400" height="300" loading="lazy">
|
||||
<div class="gallery-overlay">
|
||||
<span class="gallery-caption">Protective Waxing</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Static gallery content driven by voyage-layout styles
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Gallery styling provided by voyage-layout.css */
|
||||
</style>
|
||||
@@ -1,360 +0,0 @@
|
||||
<template>
|
||||
<section class="hero-voyage" id="heroSection">
|
||||
<!-- Video is now injected directly into DOM to bypass Vue/Nuxt issues -->
|
||||
|
||||
<div class="hero-content" :class="{ 'is-ready': videoLoaded }">
|
||||
<div class="hero-main">
|
||||
<div class="hero-logo animate-fade-in">
|
||||
<img src="/HARBOR-SMITH-white.png" alt="Harbor Smith" width="400" height="250">
|
||||
</div>
|
||||
|
||||
<p class="hero-subtext animate-fade-up-delay">
|
||||
<span style="font-size: 1.5rem; font-weight: 500; text-transform: none; letter-spacing: normal; margin-bottom: 10px; display: block;">
|
||||
Personalized Service Maintenance for Your Boat
|
||||
</span>
|
||||
Reliable Care Above and Below the Waterline. Servicing the Bay Area and Beyond!
|
||||
</p>
|
||||
|
||||
<div class="hero-actions animate-fade-up-delay-2">
|
||||
<button class="btn-primary-warm" @click="handlePhoneClick">
|
||||
<Icon name="lucide:phone" class="btn-icon" />
|
||||
Call (510) 701-2535
|
||||
</button>
|
||||
<button class="btn-secondary-warm" @click="handleServicesClick">
|
||||
<Icon name="lucide:wrench" class="btn-icon" />
|
||||
View Our Services
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scroll to explore indicator -->
|
||||
<div class="scroll-to-explore" @click="handleScrollToExplore">
|
||||
<span>Scroll to explore</span>
|
||||
<Icon name="lucide:chevron-down" class="chevron-icon" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useParallax } from '~/composables/useParallax'
|
||||
import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations'
|
||||
import { useRipple } from '~/composables/useRipple'
|
||||
import { useSmoothScroll } from '~/composables/useSmoothScroll'
|
||||
|
||||
const { scrollToElement } = useSmoothScroll()
|
||||
const videoLoaded = ref(false)
|
||||
const videoContainer = ref<HTMLElement | null>(null)
|
||||
const videoElement = ref<HTMLVideoElement | null>(null)
|
||||
const animatedCount = ref(0)
|
||||
|
||||
// Parallax will be disabled for injected video
|
||||
// useParallax(videoContainer, 0.5)
|
||||
useIntersectionAnimations()
|
||||
useRipple()
|
||||
|
||||
const handleVideoLoaded = () => {
|
||||
videoLoaded.value = true
|
||||
}
|
||||
|
||||
const handlePhoneClick = () => {
|
||||
window.location.href = 'tel:510-701-2535'
|
||||
}
|
||||
|
||||
const handleServicesClick = () => {
|
||||
scrollToElement('#services', 800) // Constant-speed smooth scrolling
|
||||
}
|
||||
|
||||
const handleScrollToExplore = () => {
|
||||
scrollToElement('#services', 800) // Constant-speed scroll to services
|
||||
}
|
||||
|
||||
// Video visibility handling removed - video is now injected directly into DOM
|
||||
|
||||
const initSmoothScroll = () => {
|
||||
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
|
||||
anchor.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
const href = anchor.getAttribute('href')
|
||||
if (!href) {
|
||||
return
|
||||
}
|
||||
scrollToElement(href, 800) // Constant-speed smooth scrolling
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const animateCount = () => {
|
||||
const duration = 1500 // 1.5 seconds
|
||||
const targetCount = 100
|
||||
const startTime = Date.now()
|
||||
|
||||
const updateCount = () => {
|
||||
const elapsed = Date.now() - startTime
|
||||
const progress = Math.min(elapsed / duration, 1)
|
||||
|
||||
// Easing function for smooth animation
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4)
|
||||
animatedCount.value = Math.floor(easeOutQuart * targetCount)
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCount)
|
||||
} else {
|
||||
animatedCount.value = targetCount
|
||||
}
|
||||
}
|
||||
|
||||
// Start animation after a small delay for better effect
|
||||
setTimeout(() => {
|
||||
updateCount()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// ULTIMATE FIX: Use inset: 0 and -webkit-fill-available to bypass env() bugs
|
||||
const videoHTML = `
|
||||
<style>
|
||||
#ios-video-bg {
|
||||
position: fixed !important;
|
||||
inset: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
height: -webkit-fill-available !important;
|
||||
z-index: -1 !important;
|
||||
background: #000 !important;
|
||||
}
|
||||
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
#ios-video-bg {
|
||||
min-height: -webkit-fill-available !important;
|
||||
}
|
||||
}
|
||||
|
||||
#bg-video-element {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
object-fit: cover !important;
|
||||
object-position: center !important;
|
||||
}
|
||||
</style>
|
||||
<div id="ios-video-bg">
|
||||
<video
|
||||
id="bg-video-element"
|
||||
autoplay
|
||||
muted
|
||||
playsinline
|
||||
loop>
|
||||
<source src="https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4" type="video/mp4">
|
||||
</video>
|
||||
<div style="
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(0, 31, 63, 0.3) 0%,
|
||||
rgba(0, 31, 63, 0.5) 50%,
|
||||
rgba(0, 31, 63, 0.7) 100%);
|
||||
"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Insert at the very beginning of body, outside #__nuxt
|
||||
document.body.insertAdjacentHTML('afterbegin', videoHTML);
|
||||
|
||||
// Get reference to injected video
|
||||
const injectedVideo = document.getElementById('bg-video-element') as HTMLVideoElement;
|
||||
|
||||
// Handle video load state
|
||||
if (injectedVideo) {
|
||||
injectedVideo.addEventListener('loadeddata', () => {
|
||||
videoLoaded.value = true;
|
||||
});
|
||||
|
||||
// iOS autoplay fallback
|
||||
const tryPlay = () => {
|
||||
if (injectedVideo.paused) {
|
||||
injectedVideo.play().catch(() => {});
|
||||
}
|
||||
};
|
||||
window.addEventListener('pointerdown', tryPlay, { once: true });
|
||||
window.addEventListener('touchstart', tryPlay, { once: true });
|
||||
}
|
||||
|
||||
initSmoothScroll()
|
||||
animateCount()
|
||||
|
||||
// Fallback: ensure video becomes visible after a delay
|
||||
setTimeout(() => {
|
||||
if (!videoLoaded.value) {
|
||||
videoLoaded.value = true
|
||||
}
|
||||
}, 1500)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up injected video on component unmount
|
||||
document.getElementById('ios-video-bg')?.remove();
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Hero video container - COMPLETE safe area formula (Apple/Netflix pattern) */
|
||||
.hero-video-container {
|
||||
position: fixed; /* Fixed with complete formula works better */
|
||||
/* Extend into ALL safe areas */
|
||||
top: calc(0px - env(safe-area-inset-top, 0px));
|
||||
left: calc(0px - env(safe-area-inset-left, 0px));
|
||||
/* CRITICAL: Include ALL safe area insets in width and height */
|
||||
width: calc(100vw + env(safe-area-inset-left, 0px) + env(safe-area-inset-right, 0px));
|
||||
height: calc(100vh + env(safe-area-inset-top, 0px) + env(safe-area-inset-bottom, 0px));
|
||||
z-index: 0; /* Base layer for video */
|
||||
pointer-events: none;
|
||||
overflow: hidden; /* Keep video content clipped */
|
||||
/* Apply parallax transform using CSS variable - delayed to prevent autoplay issues */
|
||||
transform: translateY(var(--parallax-offset, 0px));
|
||||
will-change: transform; /* Optimization hint */
|
||||
/* Initial state for autoplay compatibility */
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
/* iOS Safari safe area extension - removed to prevent interference with complete formula */
|
||||
/* The main .hero-video-container rule now handles all safe areas properly */
|
||||
|
||||
/* Support for dynamic viewport height - simplified */
|
||||
@supports (height: 100dvh) {
|
||||
.hero-video-container {
|
||||
/* Update height calc to include ALL safe areas for dvh */
|
||||
height: calc(100dvh + env(safe-area-inset-top, 0px) + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
/* Video fade-in transition to prevent flash */
|
||||
.hero-video {
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center top;
|
||||
/* Force GPU acceleration */
|
||||
-webkit-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.hero-video.video-loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* White overlay for smooth video loading transition */
|
||||
.video-white-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8f9fa 100%);
|
||||
z-index: 2;
|
||||
opacity: 1;
|
||||
transition: opacity 800ms ease-out;
|
||||
will-change: opacity;
|
||||
}
|
||||
|
||||
.video-white-overlay.fade-out {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Container for hero section */
|
||||
.hero-voyage {
|
||||
position: static; /* Remove containing block to allow video to escape */
|
||||
min-height: 100vh;
|
||||
/* Allow video to extend into safe area */
|
||||
overflow: visible;
|
||||
isolation: isolate; /* Create new stacking context */
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: fixed; /* Use fixed to match video container positioning */
|
||||
top: 0; /* Start at viewport top */
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 10; /* Ensure content is above video */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: env(safe-area-inset-top, 0px); /* Safe area padding */
|
||||
}
|
||||
|
||||
.hero-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
width: min(720px, 100%);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Scroll to explore indicator */
|
||||
|
||||
.scroll-to-explore {
|
||||
margin-top: auto;
|
||||
margin-bottom: calc(var(--safe-area-cover-bottom, 0px) + clamp(0.75rem, 2vh, 1.75rem));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
transition: all 0.3s ease;
|
||||
animation: bounce 2s infinite;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.scroll-to-explore:hover {
|
||||
opacity: 1;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
.scroll-to-explore span {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
animation: chevronBounce 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 20%, 50%, 80%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes chevronBounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide on mobile for better UX */
|
||||
@media (max-width: 768px) {
|
||||
.scroll-to-explore {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,79 +0,0 @@
|
||||
<template>
|
||||
<section class="fleet-showcase" id="services">
|
||||
<div class="container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Our Services</h2>
|
||||
<p class="section-subtitle">Professional boat maintenance tailored to your needs</p>
|
||||
</div>
|
||||
|
||||
<div class="services-grid" style="display: flex; flex-wrap: wrap; gap: 30px; max-width: 1200px; margin: 0 auto; justify-content: center; align-items: stretch;">
|
||||
<div class="service-card" style="background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); padding: 0; overflow: hidden; text-align: center; flex: 0 1 350px; min-width: 280px; display: flex; flex-direction: column;">
|
||||
<div style="width: 100%; height: 200px; overflow: hidden;">
|
||||
<img src="/diver_cleaning.jpg" alt="Professional hull cleaning and anode replacement service" style="width: 100%; height: 100%; object-fit: cover;" width="500" height="333" loading="lazy">
|
||||
</div>
|
||||
<div style="padding: 30px; display: flex; flex-direction: column; flex: 1;">
|
||||
<h3 style="font-size: 24px; margin-bottom: 15px; color: #1e3a5f; min-height: 65px; display: flex; align-items: flex-start; justify-content: center; text-align: center;">Hull Cleaning & Anode Change</h3>
|
||||
<p style="color: #666; margin-bottom: 20px; min-height: 80px;">
|
||||
Professional underwater hull cleaning and zinc anode maintenance for optimal performance and corrosion protection.
|
||||
</p>
|
||||
<ul style="list-style: none; padding: 0; margin: 20px 0; text-align: left; flex: 1;">
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Removes marine growth</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Improves fuel efficiency</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Prevents corrosion</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Marine-grade anodes</li>
|
||||
</ul>
|
||||
<button class="btn-primary-warm" style="width: 100%; margin-top: auto;" @click="handleQuote">Get Quote</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card" style="background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); padding: 0; overflow: hidden; text-align: center; flex: 0 1 350px; min-width: 280px; display: flex; flex-direction: column;">
|
||||
<div style="width: 100%; height: 200px; overflow: hidden;">
|
||||
<img src="/Washdown.jpg" alt="Professional boat exterior cleaning service" style="width: 100%; height: 100%; object-fit: cover;" width="500" height="333" loading="lazy">
|
||||
</div>
|
||||
<div style="padding: 30px; display: flex; flex-direction: column; flex: 1;">
|
||||
<h3 style="font-size: 24px; margin-bottom: 15px; color: #1e3a5f; min-height: 65px; display: flex; align-items: flex-start; justify-content: center; text-align: center;">Exterior Cleaning</h3>
|
||||
<p style="color: #666; margin-bottom: 20px; min-height: 80px; padding-top: 27px;">
|
||||
Complete exterior detailing to keep your boat looking pristine and protected.
|
||||
</p>
|
||||
<ul style="list-style: none; padding: 0; margin: 20px 0; text-align: left; flex: 1;">
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Deep cleaning wash</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Protective wax application</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> UV protection</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Gel coat restoration</li>
|
||||
</ul>
|
||||
<button class="btn-primary-warm" style="width: 100%; margin-top: auto;" @click="handleQuote">Get Quote</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="service-card" style="background: white; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); padding: 0; overflow: hidden; text-align: center; flex: 0 1 350px; min-width: 280px; display: flex; flex-direction: column;">
|
||||
<div style="width: 100%; height: 200px; overflow: hidden;">
|
||||
<img src="/Interior.jpg" alt="Professional interior detailing service" style="width: 100%; height: 100%; object-fit: cover;" width="500" height="333" loading="lazy">
|
||||
</div>
|
||||
<div style="padding: 30px; display: flex; flex-direction: column; flex: 1;">
|
||||
<h3 style="font-size: 24px; margin-bottom: 15px; color: #1e3a5f; min-height: 65px; display: flex; align-items: flex-start; justify-content: center; text-align: center;">Interior Detailing</h3>
|
||||
<p style="color: #666; margin-bottom: 20px; min-height: 80px;">
|
||||
Thorough interior cleaning and conditioning for a fresh, comfortable cabin.
|
||||
</p>
|
||||
<ul style="list-style: none; padding: 0; margin: 20px 0; text-align: left; flex: 1;">
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Upholstery cleaning</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Mold & mildew treatment</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Surface conditioning</li>
|
||||
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Odor elimination</li>
|
||||
</ul>
|
||||
<button class="btn-primary-warm" style="width: 100%; margin-top: auto;" @click="handleQuote">Get Quote</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const handleQuote = () => {
|
||||
window.location.href = 'tel:510-701-2535'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Layout and styling provided by voyage-layout.css */
|
||||
</style>
|
||||
@@ -1,76 +0,0 @@
|
||||
<template>
|
||||
<section class="experience-stories" id="testimonials">
|
||||
<div class="story-container">
|
||||
<h2 class="section-title center">What Our Customers Say</h2>
|
||||
|
||||
<div class="testimonial-highlight" style="max-width: 800px; margin: 40px auto; padding: 40px; background: white; border-radius: 15px; box-shadow: 0 8px 30px rgba(0,0,0,0.1); text-align: center;">
|
||||
<div class="stars" style="margin-bottom: 20px;">
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 24px; height: 24px; display: inline-block;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 24px; height: 24px; display: inline-block;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 24px; height: 24px; display: inline-block;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 24px; height: 24px; display: inline-block;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 24px; height: 24px; display: inline-block;" />
|
||||
</div>
|
||||
<blockquote style="font-size: 22px; color: #1e3a5f; font-style: italic; line-height: 1.6; margin-bottom: 20px;">
|
||||
"They do an amazing job and are always reliable! I never have to worry about my boat's condition."
|
||||
</blockquote>
|
||||
<cite style="font-weight: 600; color: #4b7cb8; font-size: 18px;">- John D.</cite>
|
||||
</div>
|
||||
|
||||
<div class="stories-grid" style="display: flex; flex-wrap: wrap; gap: 30px; justify-content: center; max-width: 1200px; margin: 0 auto;">
|
||||
<div class="story-card" style="flex: 0 1 350px; min-width: 280px;">
|
||||
<div class="story-content" style="padding: 24px;">
|
||||
<div class="stars" style="margin-bottom: 15px;">
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
</div>
|
||||
<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 10px;">Professional Service</h3>
|
||||
<p style="color: #4a5568; margin-bottom: 12px;">"Harbor Smith keeps my boat in pristine condition. Their attention to detail is unmatched."</p>
|
||||
<span style="color: #4b7cb8; font-weight: 600;">- Michael R.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-card" style="flex: 0 1 350px; min-width: 280px;">
|
||||
<div class="story-content" style="padding: 24px;">
|
||||
<div class="stars" style="margin-bottom: 15px;">
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
</div>
|
||||
<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 10px;">Convenient & Reliable</h3>
|
||||
<p style="color: #4a5568; margin-bottom: 12px;">"Mobile service that comes to my dock - it doesn't get better than that! Highly recommended."</p>
|
||||
<span style="color: #4b7cb8; font-weight: 600;">- Sarah L.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="story-card" style="flex: 0 1 350px; min-width: 280px;">
|
||||
<div class="story-content" style="padding: 24px;">
|
||||
<div class="stars" style="margin-bottom: 15px;">
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
<LucideStar style="color: #fbbf24; fill: #fbbf24; width: 18px; height: 18px;" />
|
||||
</div>
|
||||
<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 10px;">Excellent Value</h3>
|
||||
<p style="color: #4a5568; margin-bottom: 12px;">"Fair pricing and exceptional quality. They've been maintaining my yacht for 5 years now."</p>
|
||||
<span style="color: #4b7cb8; font-weight: 600;">- David K.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Lucide icons handled globally
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Card hover effects controlled via voyage-layout.css */
|
||||
</style>
|
||||
@@ -1,52 +0,0 @@
|
||||
<template>
|
||||
<section class="services-section">
|
||||
<div class="container">
|
||||
<div class="service-stats trust-indicators-grid">
|
||||
<div class="stat-item">
|
||||
<LucideAward class="stat-icon" />
|
||||
<span class="stat-number">20+</span>
|
||||
<span class="stat-label">Years Experience</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<LucideWrench class="stat-icon" />
|
||||
<span class="stat-number">100%</span>
|
||||
<span class="stat-label">Customizable Service</span>
|
||||
</div>
|
||||
<div class="stat-item stat-item-last">
|
||||
<LucideShieldCheck class="stat-icon" />
|
||||
<span class="stat-number">100%</span>
|
||||
<span class="stat-label">Certified Experts</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Icons registered globally via lucide plugin
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Styling sourced from voyage-layout.css */
|
||||
.trust-indicators-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 40px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.trust-indicators-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 30px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.stat-item-last {
|
||||
grid-column: 1 / -1;
|
||||
justify-self: center;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +0,0 @@
|
||||
<template>
|
||||
<section class="welcome-section">
|
||||
<div class="container">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-text">
|
||||
<h2 class="section-title warm">Why Choose Harbor Smith?</h2>
|
||||
<p class="lead-text">
|
||||
We're the San Francisco Bay Area's premier mobile boat maintenance service.
|
||||
Our professional team provides expert care, ensuring your vessel stays in pristine condition year-round.
|
||||
</p>
|
||||
<div class="feature-list">
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">
|
||||
<LucideWrench />
|
||||
</span>
|
||||
<div>
|
||||
<h4>Tailored Service</h4>
|
||||
<p>We provide highly customized service that fits your needs.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">
|
||||
<LucideShieldCheck />
|
||||
</span>
|
||||
<div>
|
||||
<h4>Certified Professionals</h4>
|
||||
<p>Experienced technicians with marine industry certifications</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">
|
||||
<LucideCalendarCheck />
|
||||
</span>
|
||||
<div>
|
||||
<h4>Reliable & Consistent</h4>
|
||||
<p>Regular maintenance schedules tailored to your needs</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="welcome-image">
|
||||
<img src="/leah_1.jpeg" alt="Harbor Smith team member" class="rounded-image" width="600" height="400" loading="lazy">
|
||||
<div class="image-badge">
|
||||
<span>10+ Years</span>
|
||||
<span>of Excellence</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations'
|
||||
|
||||
onMounted(() => {
|
||||
useIntersectionAnimations()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Styling handled in voyage-layout.css */
|
||||
</style>
|
||||
@@ -1,96 +0,0 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
export const useIntersectionAnimations = (threshold = 0.1) => {
|
||||
const elements = ref([])
|
||||
let observer = null
|
||||
|
||||
const observerOptions = {
|
||||
threshold,
|
||||
rootMargin: '0px 0px -100px 0px'
|
||||
}
|
||||
|
||||
const animateElement = (entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('animate-in')
|
||||
entry.target.style.opacity = '1'
|
||||
entry.target.style.transform = 'translateY(0)'
|
||||
|
||||
// Unobserve after animation to improve performance
|
||||
if (observer) {
|
||||
observer.unobserve(entry.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observeElements = () => {
|
||||
// Check for reduced motion preference
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
// Skip animations for users who prefer reduced motion
|
||||
document.querySelectorAll('[data-animate]').forEach(el => {
|
||||
el.style.opacity = '1'
|
||||
el.style.transform = 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
animateElement(entry)
|
||||
})
|
||||
}, observerOptions)
|
||||
|
||||
// Find all elements with data-animate attribute
|
||||
document.querySelectorAll('[data-animate]').forEach(el => {
|
||||
// Set initial state
|
||||
el.style.opacity = '0'
|
||||
el.style.transition = 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||
|
||||
const animationType = el.dataset.animate
|
||||
|
||||
switch (animationType) {
|
||||
case 'fade-up':
|
||||
el.style.transform = 'translateY(30px)'
|
||||
break
|
||||
case 'fade-in':
|
||||
// Just opacity, no transform
|
||||
break
|
||||
case 'scale-in':
|
||||
el.style.transform = 'scale(0.95)'
|
||||
break
|
||||
case 'slide-left':
|
||||
el.style.transform = 'translateX(50px)'
|
||||
break
|
||||
case 'slide-right':
|
||||
el.style.transform = 'translateX(-50px)'
|
||||
break
|
||||
default:
|
||||
el.style.transform = 'translateY(20px)'
|
||||
}
|
||||
|
||||
// Add delay if specified
|
||||
if (el.dataset.animateDelay) {
|
||||
el.style.transitionDelay = el.dataset.animateDelay
|
||||
}
|
||||
|
||||
observer.observe(el)
|
||||
elements.value.push(el)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Wait for DOM to be ready
|
||||
setTimeout(observeElements, 100)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
elements
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
export const useParallax = (elementRef, speed = 0.5) => {
|
||||
const offset = ref(0)
|
||||
let ticking = false
|
||||
let isActive = true
|
||||
|
||||
const updateOffset = (value) => {
|
||||
offset.value = value
|
||||
if (elementRef.value) {
|
||||
elementRef.value.style.setProperty('--parallax-offset', `${value}px`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = () => {
|
||||
if (!isActive || !elementRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const scrolled = window.pageYOffset
|
||||
const rate = scrolled * speed
|
||||
updateOffset(rate)
|
||||
ticking = false
|
||||
})
|
||||
ticking = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!elementRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
elementRef.value.style.willChange = 'transform'
|
||||
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
const isNarrowViewport = window.innerWidth <= 768
|
||||
|
||||
isActive = !prefersReducedMotion && !isNarrowViewport
|
||||
|
||||
if (isActive) {
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
handleScroll()
|
||||
} else {
|
||||
updateOffset(0)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
if (elementRef.value) {
|
||||
elementRef.value.style.willChange = 'auto'
|
||||
elementRef.value.style.removeProperty('--parallax-offset')
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
transform: offset
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { onMounted, onBeforeUnmount } from 'vue'
|
||||
|
||||
export const useRipple = (buttonSelector = '.btn-primary-warm, .btn-secondary-warm, .btn-booking') => {
|
||||
let buttons = []
|
||||
|
||||
const createRipple = (event) => {
|
||||
const button = event.currentTarget
|
||||
|
||||
// Check for reduced motion preference
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
if (prefersReducedMotion) return
|
||||
|
||||
const circle = document.createElement('span')
|
||||
const diameter = Math.max(button.clientWidth, button.clientHeight)
|
||||
const radius = diameter / 2
|
||||
|
||||
// Calculate position relative to button
|
||||
const rect = button.getBoundingClientRect()
|
||||
const x = event.clientX - rect.left - radius
|
||||
const y = event.clientY - rect.top - radius
|
||||
|
||||
circle.style.width = circle.style.height = `${diameter}px`
|
||||
circle.style.left = `${x}px`
|
||||
circle.style.top = `${y}px`
|
||||
circle.classList.add('ripple')
|
||||
|
||||
// Remove any existing ripple
|
||||
const ripple = button.getElementsByClassName('ripple')[0]
|
||||
if (ripple) {
|
||||
ripple.remove()
|
||||
}
|
||||
|
||||
button.appendChild(circle)
|
||||
|
||||
// Remove ripple after animation
|
||||
setTimeout(() => {
|
||||
circle.remove()
|
||||
}, 600)
|
||||
}
|
||||
|
||||
const initRipple = () => {
|
||||
buttons = document.querySelectorAll(buttonSelector)
|
||||
|
||||
buttons.forEach(button => {
|
||||
// Ensure button has position relative and overflow hidden
|
||||
button.style.position = 'relative'
|
||||
button.style.overflow = 'hidden'
|
||||
|
||||
// Add event listener
|
||||
button.addEventListener('click', createRipple)
|
||||
})
|
||||
}
|
||||
|
||||
const destroyRipple = () => {
|
||||
buttons.forEach(button => {
|
||||
button.removeEventListener('click', createRipple)
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Wait for DOM to be ready
|
||||
setTimeout(initRipple, 100)
|
||||
|
||||
// Re-init if new buttons are added dynamically
|
||||
const observer = new MutationObserver(() => {
|
||||
destroyRipple()
|
||||
initRipple()
|
||||
})
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
destroyRipple()
|
||||
})
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
export const useSmoothScroll = () => {
|
||||
let animationFrameId: number | null = null
|
||||
|
||||
// Cancel any ongoing scroll animation
|
||||
const cancelScroll = () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animationFrameId = null
|
||||
}
|
||||
// Clean up event listeners
|
||||
window.removeEventListener('wheel', cancelScroll)
|
||||
window.removeEventListener('touchstart', cancelScroll)
|
||||
window.removeEventListener('keydown', cancelScroll)
|
||||
}
|
||||
|
||||
// Ease-out cubic: starts fast, slows down as it approaches target
|
||||
const easeOutCubic = (t: number): number => {
|
||||
return 1 - Math.pow(1 - t, 3)
|
||||
}
|
||||
|
||||
const smoothScrollTo = (target: number | Element, duration: number = 800) => {
|
||||
// Check for reduced motion preference
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
const targetPosition = typeof target === 'number'
|
||||
? target
|
||||
: target.getBoundingClientRect().top + window.pageYOffset
|
||||
|
||||
if (prefersReducedMotion) {
|
||||
// Instant scroll for accessibility
|
||||
window.scrollTo(0, targetPosition)
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel any previously running scroll animation
|
||||
cancelScroll()
|
||||
|
||||
const startPosition = window.pageYOffset
|
||||
const distance = targetPosition - startPosition
|
||||
let startTime: number | null = null
|
||||
|
||||
const animation = (currentTime: number) => {
|
||||
if (startTime === null) startTime = currentTime
|
||||
const timeElapsed = currentTime - startTime
|
||||
const progress = Math.min(timeElapsed / duration, 1)
|
||||
const easedProgress = easeOutCubic(progress)
|
||||
|
||||
window.scrollTo(0, startPosition + distance * easedProgress)
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrameId = requestAnimationFrame(animation)
|
||||
} else {
|
||||
cancelScroll() // Clean up when animation completes
|
||||
}
|
||||
}
|
||||
|
||||
// Add listeners to detect user interruption
|
||||
window.addEventListener('wheel', cancelScroll, { passive: true })
|
||||
window.addEventListener('touchstart', cancelScroll, { passive: true })
|
||||
window.addEventListener('keydown', cancelScroll, { once: true })
|
||||
|
||||
animationFrameId = requestAnimationFrame(animation)
|
||||
}
|
||||
|
||||
const scrollToElement = (selector: string, duration: number = 800) => {
|
||||
const element = document.querySelector(selector)
|
||||
if (element) {
|
||||
smoothScrollTo(element, duration)
|
||||
}
|
||||
}
|
||||
|
||||
const scrollToTop = (duration: number = 800) => {
|
||||
smoothScrollTo(0, duration)
|
||||
}
|
||||
|
||||
return {
|
||||
smoothScrollTo,
|
||||
scrollToElement,
|
||||
scrollToTop
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
harborsmith-website:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: harborsmith-website
|
||||
ports:
|
||||
- "3001:80"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- harborsmith-network
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.harborsmith-website.rule=Host(`localhost`)"
|
||||
- "traefik.http.services.harborsmith-website.loadbalancer.server.port=80"
|
||||
|
||||
networks:
|
||||
harborsmith-network:
|
||||
external: true
|
||||
name: harborsmith_default
|
||||
@@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<AppNavbar />
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
<AppFooter />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Layout components are auto-imported by Nuxt
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-layout {
|
||||
/* Allow background to be transparent for safe area extension */
|
||||
background-color: transparent;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
# Gzip Settings
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
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;
|
||||
gzip_disable "msie6";
|
||||
|
||||
# Cache Settings
|
||||
map $sent_http_content_type $expires {
|
||||
default off;
|
||||
text/html epoch;
|
||||
text/css max;
|
||||
application/javascript max;
|
||||
application/json off;
|
||||
~image/ max;
|
||||
~font/ max;
|
||||
}
|
||||
|
||||
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-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' https:; script-src 'self' 'unsafe-inline' 'unsafe-eval' https:; style-src 'self' 'unsafe-inline' https: fonts.googleapis.com; font-src 'self' https: fonts.gstatic.com data:; img-src 'self' https: data:; media-src 'self' https: *.pexels.com; connect-src 'self' https:;" always;
|
||||
|
||||
# Cache control
|
||||
expires $expires;
|
||||
|
||||
# Main location
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Static assets
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|otf|mp4|webm)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# API proxy (if needed for future API integration)
|
||||
location /api/ {
|
||||
proxy_pass http://api:3000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Error pages
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,291 +0,0 @@
|
||||
import { existsSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-04-03',
|
||||
devtools: { enabled: true },
|
||||
|
||||
// Static Site Generation
|
||||
ssr: true,
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: ['/'],
|
||||
crawlLinks: true
|
||||
}
|
||||
},
|
||||
|
||||
// Modules
|
||||
modules: [
|
||||
'@nuxt/image',
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@nuxtjs/google-fonts',
|
||||
// '@nuxtjs/seo', // Temporarily disabled - incompatible with Nuxt 3.19.2
|
||||
'@vueuse/nuxt',
|
||||
'@vueuse/motion/nuxt',
|
||||
'nuxt-icon'
|
||||
],
|
||||
|
||||
// Google Fonts
|
||||
googleFonts: {
|
||||
families: {
|
||||
'Inter': [300, 400, 500, 600, 700, 800],
|
||||
'Playfair+Display': [400, 700, 900]
|
||||
},
|
||||
display: 'swap',
|
||||
preload: true,
|
||||
prefetch: false,
|
||||
preconnect: true
|
||||
},
|
||||
|
||||
// SEO - disabled temporarily
|
||||
// site: {
|
||||
// url: 'https://harborsmith.com',
|
||||
// name: 'Harbor Smith',
|
||||
// description: 'Premium yacht charter and maintenance services in San Francisco Bay',
|
||||
// defaultLocale: 'en'
|
||||
// },
|
||||
|
||||
// ogImage: {
|
||||
// enabled: false
|
||||
// },
|
||||
|
||||
// App configuration with viewport meta tag for iOS Safari safe area
|
||||
app: {
|
||||
head: {
|
||||
meta: [
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' },
|
||||
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
||||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// Image optimization
|
||||
image: {
|
||||
quality: 90,
|
||||
format: ['webp', 'jpg'],
|
||||
screens: {
|
||||
xs: 320,
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280,
|
||||
xxl: 1536,
|
||||
'2xl': 1536
|
||||
}
|
||||
},
|
||||
|
||||
// Tailwind CSS
|
||||
tailwindcss: {
|
||||
exposeConfig: true,
|
||||
viewer: false,
|
||||
config: {
|
||||
content: [],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'harbor-blue': '#001f3f',
|
||||
'harbor-navy': '#1e3a5f',
|
||||
'harbor-gold': '#b48b4e',
|
||||
'harbor-amber': '#9d7943',
|
||||
'harbor-yellow': '#c9a56f',
|
||||
'harbor-light': '#f0f0f0'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
serif: ['Playfair Display', 'serif']
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.6s ease-out',
|
||||
'slide-up': 'slideUp 0.6s ease-out',
|
||||
'scale-in': 'scaleIn 0.5s ease-out'
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' }
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(30px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' }
|
||||
},
|
||||
scaleIn: {
|
||||
'0%': { transform: 'scale(0.9)', opacity: '0' },
|
||||
'100%': { transform: 'scale(1)', opacity: '1' }
|
||||
}
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-warm': 'linear-gradient(135deg, #b48b4e 0%, #c9a56f 100%)',
|
||||
'gradient-blue': 'linear-gradient(135deg, #001f3f 0%, #1e3a5f 100%)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// App configuration
|
||||
app: {
|
||||
head: {
|
||||
title: 'Harbor Smith - Premium Yacht Charter & Maintenance Services | San Francisco Bay',
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes, viewport-fit=cover' },
|
||||
{ name: 'description', content: 'Premium yacht charter and professional boat maintenance services in San Francisco Bay. Mobile service at your dock. Call (510) 701-2535 for personalized service.' },
|
||||
{ name: 'format-detection', content: 'telephone=no' },
|
||||
{ name: 'author', content: 'Harbor Smith' },
|
||||
{ name: 'keywords', content: 'yacht charter, boat maintenance, San Francisco Bay, mobile boat service, yacht repair, marine services, boat cleaning, yacht management' },
|
||||
{ name: 'robots', content: 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1' },
|
||||
{ name: 'theme-color', content: '#ffffff' },
|
||||
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
|
||||
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' },
|
||||
// Open Graph
|
||||
{ property: 'og:type', content: 'website' },
|
||||
{ property: 'og:url', content: 'https://harborsmith.com' },
|
||||
{ property: 'og:title', content: 'Harbor Smith - Premium Yacht Charter & Maintenance Services' },
|
||||
{ property: 'og:description', content: 'Experience luxury yacht charters and professional boat maintenance in San Francisco Bay. Personalized service at your dock.' },
|
||||
{ property: 'og:image', content: 'https://harborsmith.com/og-image.jpg' },
|
||||
{ property: 'og:image:width', content: '1200' },
|
||||
{ property: 'og:image:height', content: '630' },
|
||||
{ property: 'og:image:alt', content: 'Harbor Smith Yacht Services' },
|
||||
{ property: 'og:site_name', content: 'Harbor Smith' },
|
||||
{ property: 'og:locale', content: 'en_US' },
|
||||
// Twitter Card
|
||||
{ name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ name: 'twitter:url', content: 'https://harborsmith.com' },
|
||||
{ name: 'twitter:title', content: 'Harbor Smith - Premium Yacht Services' },
|
||||
{ name: 'twitter:description', content: 'Luxury yacht charters & professional maintenance in San Francisco Bay' },
|
||||
{ name: 'twitter:image', content: 'https://harborsmith.com/og-image.jpg' },
|
||||
// Geo tags for local SEO
|
||||
{ name: 'geo.region', content: 'US-CA' },
|
||||
{ name: 'geo.placename', content: 'San Francisco Bay Area' },
|
||||
{ name: 'geo.position', content: '37.7749;-122.4194' },
|
||||
{ name: 'ICBM', content: '37.7749, -122.4194' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/png', href: '/HARBOR-SMITH-navy.png' },
|
||||
{ rel: 'apple-touch-icon', href: '/HARBOR-SMITH-navy.png' },
|
||||
{ rel: 'canonical', href: 'https://harborsmith.com' },
|
||||
// Preconnect for performance
|
||||
{ rel: 'preconnect', href: 'https://fonts.googleapis.com' },
|
||||
{ rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: 'anonymous' },
|
||||
{ rel: 'dns-prefetch', href: 'https://videos.pexels.com' },
|
||||
// Video preload
|
||||
{ rel: 'preload', as: 'video', href: 'https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4', crossorigin: 'anonymous' }
|
||||
],
|
||||
script: [
|
||||
// Structured Data for Local Business
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'LocalBusiness',
|
||||
'@id': 'https://harborsmith.com',
|
||||
name: 'Harbor Smith',
|
||||
description: 'Premium yacht charter and professional boat maintenance services in San Francisco Bay',
|
||||
url: 'https://harborsmith.com',
|
||||
telephone: '+15107012535',
|
||||
address: {
|
||||
'@type': 'PostalAddress',
|
||||
addressLocality: 'San Francisco',
|
||||
addressRegion: 'CA',
|
||||
addressCountry: 'US'
|
||||
},
|
||||
geo: {
|
||||
'@type': 'GeoCoordinates',
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194
|
||||
},
|
||||
openingHoursSpecification: {
|
||||
'@type': 'OpeningHoursSpecification',
|
||||
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
|
||||
opens: '08:00',
|
||||
closes: '18:00'
|
||||
},
|
||||
sameAs: [
|
||||
'https://www.facebook.com/harborsmith',
|
||||
'https://www.instagram.com/harborsmith',
|
||||
'https://www.linkedin.com/company/harborsmith'
|
||||
],
|
||||
image: 'https://harborsmith.com/HARBOR-SMITH-navy.png',
|
||||
priceRange: '$$$'
|
||||
})
|
||||
},
|
||||
// Service Schema
|
||||
{
|
||||
type: 'application/ld+json',
|
||||
innerHTML: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Service',
|
||||
name: 'Yacht Charter & Maintenance Services',
|
||||
provider: {
|
||||
'@type': 'LocalBusiness',
|
||||
name: 'Harbor Smith'
|
||||
},
|
||||
areaServed: {
|
||||
'@type': 'Place',
|
||||
name: 'San Francisco Bay Area'
|
||||
},
|
||||
hasOfferCatalog: {
|
||||
'@type': 'OfferCatalog',
|
||||
name: 'Marine Services',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Yacht Charter',
|
||||
description: 'Luxury yacht charter services for events and leisure'
|
||||
}
|
||||
},
|
||||
{
|
||||
'@type': 'Offer',
|
||||
itemOffered: {
|
||||
'@type': 'Service',
|
||||
name: 'Boat Maintenance',
|
||||
description: 'Professional boat cleaning and maintenance at your dock'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
]
|
||||
},
|
||||
pageTransition: { name: 'page', mode: 'out-in' },
|
||||
layoutTransition: { name: 'layout', mode: 'out-in' }
|
||||
},
|
||||
|
||||
// CSS
|
||||
css: [
|
||||
'~/assets/css/voyage-layout.css',
|
||||
'~/assets/css/themes.css',
|
||||
'~/assets/css/main.css'
|
||||
],
|
||||
|
||||
// Runtime config
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'https://harborsmith.com'
|
||||
}
|
||||
},
|
||||
|
||||
hooks: {
|
||||
'prepare:types': (payload) => {
|
||||
const nuxt = payload?.nuxt ?? payload
|
||||
const buildDir = nuxt?.options?.buildDir ?? join(process.cwd(), '.nuxt')
|
||||
|
||||
const ensureTsconfig = (filename, extendsPath) => {
|
||||
const target = join(buildDir, filename)
|
||||
if (!existsSync(target)) {
|
||||
const json = JSON.stringify({ extends: extendsPath }, null, 2) + '\n'
|
||||
writeFileSync(target, json, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
ensureTsconfig('tsconfig.app.json', './tsconfig.json')
|
||||
ensureTsconfig('tsconfig.shared.json', './tsconfig.json')
|
||||
ensureTsconfig('tsconfig.node.json', './tsconfig.server.json')
|
||||
}
|
||||
}
|
||||
})
|
||||
14683
vue-archive/apps/website/package-lock.json
generated
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "@harborsmith/website",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "nuxt typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.68",
|
||||
"@vueuse/motion": "^2.2.6",
|
||||
"@vueuse/nuxt": "^11.3.0",
|
||||
"lucide-vue-next": "^0.544.0",
|
||||
"nuxt": "^3.15.0",
|
||||
"nuxt-icon": "^1.0.0-beta.7",
|
||||
"unenv": "^2.0.0-rc.21",
|
||||
"vue": "latest",
|
||||
"vue-router": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/image": "^1.8.1",
|
||||
"@nuxtjs/google-fonts": "^3.2.0",
|
||||
"@nuxtjs/seo": "^2.0.0",
|
||||
"@nuxtjs/tailwindcss": "^6.12.2",
|
||||
"@types/node": "^20",
|
||||
"ipx": "^3.1.1",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Hero Section with Video Background -->
|
||||
<HeroSection />
|
||||
|
||||
<!-- Main content wrapper with white background -->
|
||||
<div class="main-content">
|
||||
<!-- Welcome Section -->
|
||||
<WelcomeSection />
|
||||
|
||||
<!-- Services Section -->
|
||||
<ServicesSection />
|
||||
|
||||
<!-- Trust Indicators Section -->
|
||||
<TrustIndicators />
|
||||
|
||||
<!-- Testimonials Section -->
|
||||
<TestimonialsSection />
|
||||
|
||||
<!-- Gallery Section -->
|
||||
<GallerySection />
|
||||
|
||||
<!-- Booking Section -->
|
||||
<BookingSection />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// SEO meta tags aligned with static mockup
|
||||
useHead({
|
||||
title: 'Harbor Smith - Personalized Service Maintenance For Your Boat',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Reliable Care Above and Below the Waterline. Servicing the Bay Area and Beyond!'
|
||||
},
|
||||
{
|
||||
property: 'og:title',
|
||||
content: 'Harbor Smith - Personalized Service Maintenance For Your Boat'
|
||||
},
|
||||
{
|
||||
property: 'og:description',
|
||||
content: 'Reliable Care Above and Below the Waterline. Servicing the Bay Area and Beyond!'
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: '/HARBOR-SMITH_navy.png'
|
||||
},
|
||||
{
|
||||
name: 'twitter:card',
|
||||
content: 'summary_large_image'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// Structured data temporarily disabled pending nuxt-seo-utils update
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-content {
|
||||
background: #ffffff;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
import {
|
||||
Star,
|
||||
Phone,
|
||||
Wrench,
|
||||
ChevronDown,
|
||||
Ship,
|
||||
Award,
|
||||
Users,
|
||||
ShieldCheck,
|
||||
Calendar,
|
||||
CalendarCheck,
|
||||
Mail,
|
||||
MapPin,
|
||||
Clock,
|
||||
Facebook,
|
||||
Instagram,
|
||||
Twitter,
|
||||
Check,
|
||||
Menu,
|
||||
X,
|
||||
Truck
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
// Register Lucide icons as global components
|
||||
nuxtApp.vueApp.component('LucideStar', Star)
|
||||
nuxtApp.vueApp.component('LucidePhone', Phone)
|
||||
nuxtApp.vueApp.component('LucideWrench', Wrench)
|
||||
nuxtApp.vueApp.component('LucideChevronDown', ChevronDown)
|
||||
nuxtApp.vueApp.component('LucideShip', Ship)
|
||||
nuxtApp.vueApp.component('LucideAward', Award)
|
||||
nuxtApp.vueApp.component('LucideUsers', Users)
|
||||
nuxtApp.vueApp.component('LucideShieldCheck', ShieldCheck)
|
||||
nuxtApp.vueApp.component('LucideCalendar', Calendar)
|
||||
nuxtApp.vueApp.component('LucideCalendarCheck', CalendarCheck)
|
||||
nuxtApp.vueApp.component('LucideMail', Mail)
|
||||
nuxtApp.vueApp.component('LucideMapPin', MapPin)
|
||||
nuxtApp.vueApp.component('LucideClock', Clock)
|
||||
nuxtApp.vueApp.component('LucideFacebook', Facebook)
|
||||
nuxtApp.vueApp.component('LucideInstagram', Instagram)
|
||||
nuxtApp.vueApp.component('LucideTwitter', Twitter)
|
||||
nuxtApp.vueApp.component('LucideCheck', Check)
|
||||
nuxtApp.vueApp.component('LucideMenu', Menu)
|
||||
nuxtApp.vueApp.component('LucideX', X)
|
||||
nuxtApp.vueApp.component('LucideTruck', Truck)
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 6.5 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 5.1 MiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 3.7 MiB |
|
Before Width: | Height: | Size: 533 KiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 7.3 MiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 6.1 MiB |
|
Before Width: | Height: | Size: 4.4 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
@@ -1,2 +0,0 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
|
Before Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 8.1 MiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 6.4 MiB |
|
Before Width: | Height: | Size: 9.1 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 148 KiB |
@@ -1,267 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<title>Safe Area Fix Test - Harbor Smith</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #000;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Use @supports as recommended by Apple */
|
||||
@supports(padding: max(0px)) {
|
||||
.bg {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
/* Use max() function to ensure fallback */
|
||||
top: calc(-1 * max(env(safe-area-inset-top), 20px));
|
||||
/* Safari-specific: use constant as fallback */
|
||||
top: calc(-1 * max(constant(safe-area-inset-top), env(safe-area-inset-top), 20px));
|
||||
height: calc(100vh + max(env(safe-area-inset-top), 20px));
|
||||
height: calc(100vh + max(constant(safe-area-inset-top), env(safe-area-inset-top), 20px));
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback for browsers without @supports */
|
||||
.bg {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -44px; /* Standard notch height fallback */
|
||||
width: 100%;
|
||||
height: calc(100vh + 44px);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* iOS-specific using -webkit-touch-callout */
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.bg {
|
||||
/* For iPhone 14/15 Pro with Dynamic Island (59px) */
|
||||
top: -59px;
|
||||
height: calc(100vh + 59px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Gradient background */
|
||||
.bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg,
|
||||
#ff0000 0%, /* Red at very top */
|
||||
#ff6600 10%, /* Orange */
|
||||
#ffcc00 20%, /* Yellow */
|
||||
#33cc33 30%, /* Green */
|
||||
#0099ff 50%, /* Blue */
|
||||
#6633cc 70%, /* Purple */
|
||||
#cc33cc 90%, /* Magenta */
|
||||
#000000 100% /* Black at bottom */
|
||||
);
|
||||
}
|
||||
|
||||
/* Content with proper safe area padding using max() */
|
||||
@supports(padding: max(0px)) {
|
||||
.content {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding-top: max(env(safe-area-inset-top), 44px);
|
||||
padding-top: max(constant(safe-area-inset-top), env(safe-area-inset-top), 44px);
|
||||
padding-left: max(env(safe-area-inset-left), 0px);
|
||||
padding-right: max(env(safe-area-inset-right), 0px);
|
||||
padding-bottom: max(env(safe-area-inset-bottom), 0px);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fallback content padding */
|
||||
.content {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding-top: 44px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
padding-bottom: 0;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
.content {
|
||||
padding-top: 59px; /* Dynamic Island */
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 10px 0;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #FFC107;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #F44336;
|
||||
}
|
||||
|
||||
/* Force fullscreen for better safe area support */
|
||||
@media screen and (display-mode: standalone) {
|
||||
.bg {
|
||||
top: calc(-1 * env(safe-area-inset-top));
|
||||
height: calc(100vh + env(safe-area-inset-top));
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg"></div>
|
||||
<div class="content">
|
||||
<h1>Safe Area Fix Test v2</h1>
|
||||
<div class="info">
|
||||
<p class="success">✅ Using hardcoded fallback: -59px for Dynamic Island</p>
|
||||
<p class="success">✅ Using max() function for reliable fallbacks</p>
|
||||
<p class="success">✅ Supporting both env() and constant()</p>
|
||||
<p class="warning">⚠️ Safari bug: env() returns 0 in portrait mode</p>
|
||||
|
||||
<p style="margin-top: 20px; font-weight: bold;">Visual Test:</p>
|
||||
<p>🔴 Red should be visible in the notch area</p>
|
||||
<p>📝 This text should be below the notch</p>
|
||||
|
||||
<p style="margin-top: 20px; font-weight: bold;">Debug Values:</p>
|
||||
<p>Safe Area Top: <span id="safeTop">checking...</span></p>
|
||||
<p>Computed Top Padding: <span id="computedPadding">checking...</span></p>
|
||||
<p>Viewport Height: <span id="viewHeight">checking...</span></p>
|
||||
<p>User Agent: <span id="userAgent">checking...</span></p>
|
||||
|
||||
<p style="margin-top: 20px; font-weight: bold;">Workaround Status:</p>
|
||||
<p id="workaround" class="warning">Using hardcoded fallback values</p>
|
||||
</div>
|
||||
|
||||
<div class="info" style="margin-top: 20px;">
|
||||
<p style="font-weight: bold;">Try These Tests:</p>
|
||||
<p>1. Rotate to landscape and back to portrait</p>
|
||||
<p>2. Add to Home Screen for PWA mode</p>
|
||||
<p>3. Check if red gradient extends to top</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function detectDevice() {
|
||||
const ua = navigator.userAgent;
|
||||
const isIPhone = /iPhone/.test(ua);
|
||||
const isIPad = /iPad/.test(ua);
|
||||
const isIOS = isIPhone || isIPad;
|
||||
|
||||
// Detect specific iPhone models with notch/Dynamic Island
|
||||
const hasNotch = isIPhone && screen.height >= 812; // iPhone X and later
|
||||
const hasDynamicIsland = isIPhone && screen.height >= 852; // iPhone 14 Pro and later
|
||||
|
||||
return {
|
||||
isIOS,
|
||||
isIPhone,
|
||||
hasNotch,
|
||||
hasDynamicIsland,
|
||||
screenHeight: screen.height,
|
||||
screenWidth: screen.width
|
||||
};
|
||||
}
|
||||
|
||||
function updateDebugInfo() {
|
||||
const device = detectDevice();
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
|
||||
// Try to get safe area values
|
||||
let safeTop = styles.getPropertyValue('padding-top') || '0px';
|
||||
|
||||
// Get computed padding on content
|
||||
const content = document.querySelector('.content');
|
||||
const computedPadding = window.getComputedStyle(content).paddingTop;
|
||||
|
||||
document.getElementById('safeTop').textContent = safeTop || '0px (Safari bug)';
|
||||
document.getElementById('computedPadding').textContent = computedPadding;
|
||||
document.getElementById('viewHeight').textContent = window.innerHeight + 'px';
|
||||
document.getElementById('userAgent').textContent = device.isIPhone ?
|
||||
(device.hasDynamicIsland ? 'iPhone 14/15 Pro' :
|
||||
device.hasNotch ? 'iPhone with Notch' : 'iPhone') :
|
||||
'Not iPhone';
|
||||
|
||||
// Update workaround status
|
||||
const workaroundEl = document.getElementById('workaround');
|
||||
if (parseInt(safeTop) > 0) {
|
||||
workaroundEl.textContent = '✅ Safe area is working!';
|
||||
workaroundEl.className = 'success';
|
||||
} else if (device.hasDynamicIsland) {
|
||||
workaroundEl.textContent = '⚠️ Using 59px fallback for Dynamic Island';
|
||||
workaroundEl.className = 'warning';
|
||||
} else if (device.hasNotch) {
|
||||
workaroundEl.textContent = '⚠️ Using 44px fallback for notch';
|
||||
workaroundEl.className = 'warning';
|
||||
}
|
||||
}
|
||||
|
||||
// Update on load and orientation change
|
||||
window.addEventListener('load', updateDebugInfo);
|
||||
window.addEventListener('orientationchange', () => {
|
||||
setTimeout(updateDebugInfo, 100);
|
||||
});
|
||||
window.addEventListener('resize', updateDebugInfo);
|
||||
|
||||
// Check for standalone mode
|
||||
if (window.navigator.standalone) {
|
||||
document.body.classList.add('standalone');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,157 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Safe Area Test - Harbor Smith</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
background: #000;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Background that extends under the notch */
|
||||
.bg {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* Pull up by safe area to extend under notch */
|
||||
top: calc(-1 * env(safe-area-inset-top));
|
||||
/* Grow height to compensate */
|
||||
height: calc(100lvh + env(safe-area-inset-top));
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
/* Fallback for browsers without lvh */
|
||||
@supports not (height: 100lvh) {
|
||||
.bg {
|
||||
height: calc(100vh + env(safe-area-inset-top));
|
||||
}
|
||||
}
|
||||
|
||||
/* Gradient background to visualize extension */
|
||||
.bg::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg,
|
||||
#ff0000 0%, /* Red at very top (should be in notch) */
|
||||
#ff6600 5%, /* Orange */
|
||||
#ffcc00 10%, /* Yellow */
|
||||
#33cc33 20%, /* Green */
|
||||
#0099ff 40%, /* Blue */
|
||||
#6633cc 60%, /* Purple */
|
||||
#cc33cc 80%, /* Magenta */
|
||||
#000000 100% /* Black at bottom */
|
||||
);
|
||||
}
|
||||
|
||||
/* Content that stays in safe area */
|
||||
.content {
|
||||
position: relative;
|
||||
min-height: 100dvh;
|
||||
/* Padding to keep content below notch */
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
margin: 0;
|
||||
padding: 24px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.info {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin: 20px;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.info p {
|
||||
margin: 10px 0;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 10px;
|
||||
background: rgba(0, 255, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: rgba(255, 0, 0, 0.2);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg"></div>
|
||||
<div class="content">
|
||||
<h1>Safe Area Test</h1>
|
||||
<div class="info">
|
||||
<p>If the gradient extends into the notch/Dynamic Island:</p>
|
||||
<p>✅ You should see RED color in the notch area</p>
|
||||
<p>✅ This text should be BELOW the notch</p>
|
||||
<p>✅ The gradient should fill the entire screen</p>
|
||||
<p style="margin-top: 20px;">Debug Info:</p>
|
||||
<p>Safe Area Top: <span id="safeTop">checking...</span></p>
|
||||
<p>Viewport Height: <span id="viewHeight">checking...</span></p>
|
||||
<p>Screen Height: <span id="screenHeight">checking...</span></p>
|
||||
</div>
|
||||
<div class="status" id="status">
|
||||
Testing safe area extension...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Display safe area values for debugging
|
||||
function updateDebugInfo() {
|
||||
const safeTop = getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)') || '0px';
|
||||
const viewHeight = window.innerHeight + 'px';
|
||||
const screenHeight = screen.height + 'px';
|
||||
|
||||
document.getElementById('safeTop').textContent = safeTop;
|
||||
document.getElementById('viewHeight').textContent = viewHeight;
|
||||
document.getElementById('screenHeight').textContent = screenHeight;
|
||||
|
||||
// Check if safe area is working
|
||||
const computedTop = window.getComputedStyle(document.documentElement).getPropertyValue('padding-top');
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
if (parseInt(computedTop) > 0) {
|
||||
statusEl.textContent = '✅ Safe area is active! Video should extend into notch.';
|
||||
statusEl.classList.remove('error');
|
||||
} else {
|
||||
statusEl.textContent = '⚠️ Safe area not detected. Make sure viewport-fit=cover is set.';
|
||||
statusEl.classList.add('error');
|
||||
}
|
||||
}
|
||||
|
||||
// Update on load and orientation change
|
||||
window.addEventListener('load', updateDebugInfo);
|
||||
window.addEventListener('orientationchange', updateDebugInfo);
|
||||
window.addEventListener('resize', updateDebugInfo);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 427 KiB |
|
Before Width: | Height: | Size: 99 KiB |
@@ -1,18 +0,0 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
import { existsSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: '2024-04-03',
|
||||
devtools: { enabled: true },
|
||||
|
||||
// Static Site Generation
|
||||
ssr: true,
|
||||
nitro: {
|
||||
prerender: {
|
||||
routes: ['/'],
|
||||
crawlLinks: true
|
||||
}
|
||||
},
|
||||
|
||||
// Modules
|
||||
modules: [
|
||||
'@nuxt/image',
|
||||
'@nuxtjs/tailwindcss',
|
||||
'@nuxtjs/google-fonts',
|
||||
// '@nuxtjs/seo', // Temporarily disabled - incompatible with Nuxt 3.19.2
|
||||
'@vueuse/nuxt',
|
||||
'@vueuse/motion/nuxt'
|
||||
],
|
||||
|
||||
// Google Fonts
|
||||
googleFonts: {
|
||||
families: {
|
||||
'Inter': [300, 400, 500, 600, 700, 800],
|
||||
'Playfair+Display': [400, 700, 900]
|
||||
},
|
||||
display: 'swap',
|
||||
preload: true,
|
||||
prefetch: false,
|
||||
preconnect: true
|
||||
},
|
||||
|
||||
// SEO - disabled temporarily
|
||||
// site: {
|
||||
// url: 'https://harborsmith.com',
|
||||
// name: 'Harbor Smith',
|
||||
// description: 'Premium yacht charter and maintenance services in San Francisco Bay',
|
||||
// defaultLocale: 'en'
|
||||
// },
|
||||
|
||||
// ogImage: {
|
||||
// enabled: false
|
||||
// },
|
||||
|
||||
// Image optimization
|
||||
image: {
|
||||
quality: 90,
|
||||
format: ['webp', 'jpg'],
|
||||
screens: {
|
||||
xs: 320,
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280,
|
||||
xxl: 1536,
|
||||
'2xl': 1536
|
||||
}
|
||||
},
|
||||
|
||||
// Tailwind CSS
|
||||
tailwindcss: {
|
||||
exposeConfig: true,
|
||||
viewer: false,
|
||||
config: {
|
||||
content: [],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'harbor-blue': '#001f3f',
|
||||
'harbor-navy': '#1e3a5f',
|
||||
'harbor-gold': '#b48b4e',
|
||||
'harbor-amber': '#9d7943',
|
||||
'harbor-yellow': '#c9a56f',
|
||||
'harbor-light': '#f0f0f0'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
serif: ['Playfair Display', 'serif']
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.6s ease-out',
|
||||
'slide-up': 'slideUp 0.6s ease-out',
|
||||
'scale-in': 'scaleIn 0.5s ease-out'
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' }
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(30px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' }
|
||||
},
|
||||
scaleIn: {
|
||||
'0%': { transform: 'scale(0.9)', opacity: '0' },
|
||||
'100%': { transform: 'scale(1)', opacity: '1' }
|
||||
}
|
||||
},
|
||||
backgroundImage: {
|
||||
'gradient-warm': 'linear-gradient(135deg, #b48b4e 0%, #c9a56f 100%)',
|
||||
'gradient-blue': 'linear-gradient(135deg, #001f3f 0%, #1e3a5f 100%)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// App configuration
|
||||
app: {
|
||||
head: {
|
||||
title: 'Harbor Smith - Premium Yacht Charter & Maintenance',
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||
{ name: 'description', content: 'Experience luxury yacht charters and professional maintenance services in San Francisco Bay with Harbor Smith.' },
|
||||
{ name: 'format-detection', content: 'telephone=no' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
|
||||
]
|
||||
},
|
||||
pageTransition: { name: 'page', mode: 'out-in' },
|
||||
layoutTransition: { name: 'layout', mode: 'out-in' }
|
||||
},
|
||||
|
||||
// CSS
|
||||
css: [
|
||||
'~/assets/css/voyage-layout.css',
|
||||
'~/assets/css/themes.css',
|
||||
'~/assets/css/main.css'
|
||||
],
|
||||
|
||||
// Runtime config
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
siteUrl: process.env.NUXT_PUBLIC_SITE_URL || 'https://harborsmith.com'
|
||||
}
|
||||
},
|
||||
|
||||
hooks: {
|
||||
'prepare:types': () => {
|
||||
const buildDir = join(process.cwd(), '.nuxt')
|
||||
const content = JSON.stringify({ extends: './tsconfig.json' }, null, 2)
|
||||
for (const file of ['tsconfig.app.json', 'tsconfig.shared.json']) {
|
||||
const target = join(buildDir, file)
|
||||
if (!existsSync(target)) {
|
||||
writeFileSync(target, content + '\n', 'utf8')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,75 +0,0 @@
|
||||
{
|
||||
"name": "harborsmith",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "harborsmith",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz",
|
||||
"integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.55.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz",
|
||||
"integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.55.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.55.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz",
|
||||
"integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "harborsmith",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"dependencies": {
|
||||
"@playwright/test": "^1.55.0"
|
||||
}
|
||||
}
|
||||