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>
This commit is contained in:
Matt
2025-09-26 19:46:46 +02:00
parent 634257442c
commit e0e334852e
68 changed files with 13 additions and 20114 deletions

View File

@@ -47,3 +47,7 @@ docker-compose.test.yml
tmp
temp
*.tmp
# Archived code
vue-archive
harborsmith-nextjs

7
.gitignore vendored
View File

@@ -32,3 +32,10 @@ Website-PDF-Mockups/
apps/website/nul
# Next.js
.next
# Archived/temp directories
harborsmith-nextjs
vue-archive

View File

@@ -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

View File

@@ -24,5 +24,5 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "vue-archive", "harborsmith-nextjs"]
}

View File

@@ -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

View File

@@ -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;"]

View File

@@ -1,2 +0,0 @@
FROM nginx:1.27-alpine
COPY .output/public /usr/share/nginx/html

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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>&copy; 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 &amp; 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 &amp; 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>

View File

@@ -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 &amp; 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>

View File

@@ -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>

View File

@@ -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 &amp; 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>

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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()
})
}

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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')
}
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

View File

@@ -1,2 +0,0 @@
User-Agent: *
Disallow:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -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>

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -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"
}
]
}

View File

@@ -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')
}
}
}
}
})

View File

@@ -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"
}
}
}
}

View File

@@ -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"
}
}