Initial import of HarborSmith website
Some checks failed
build-website / build (push) Failing after 1m2s

This commit is contained in:
2025-09-18 22:20:01 +02:00
commit ec72c5d62b
168 changed files with 65020 additions and 0 deletions

24
apps/website/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# 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

38
apps/website/Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# 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;"]

75
apps/website/README.md Normal file
View File

@@ -0,0 +1,75 @@
# 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

@@ -0,0 +1,371 @@
/* Harbor Smith Main Styles */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Smooth Scroll Behavior */
html {
scroll-behavior: smooth;
}
/* 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;
}
.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%);
}
}
/* Typography */
@layer base {
body {
@apply font-sans text-gray-800 antialiased;
}
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 */
.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;
}
.btn-secondary-warm {
@apply px-8 py-3 bg-transparent border-2 border-harbor-gold text-harbor-gold font-semibold rounded-full;
@apply hover:bg-harbor-gold hover:text-white transition-all duration-300;
}
.btn-booking {
@apply inline-flex items-center justify-center px-6 py-3 rounded-lg font-semibold;
@apply transition-all duration-300 transform hover:-translate-y-1;
}
.btn-booking.primary {
@apply bg-gradient-to-r from-harbor-gold to-harbor-yellow text-white;
@apply hover:shadow-2xl hover:from-harbor-amber hover:to-harbor-gold;
}
.btn-booking.secondary {
@apply bg-white text-harbor-navy border-2 border-harbor-gold;
@apply hover:bg-harbor-gold hover:text-white hover:border-harbor-gold;
}
/* 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 */
.hero-video-container {
@apply absolute inset-0 w-full h-full overflow-hidden;
}
.hero-overlay {
@apply absolute inset-0 bg-gradient-to-b from-black/40 via-black/20 to-black/40;
}
.gradient-warm {
background: linear-gradient(135deg, rgba(180, 139, 78, 0.2) 0%, rgba(201, 165, 111, 0.1) 100%);
}
.gradient-depth {
background: linear-gradient(to top, rgba(30, 58, 95, 0.3) 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-4xl md:text-5xl lg:text-6xl font-serif font-bold text-harbor-navy;
@apply mb-4;
}
.section-subtitle {
@apply text-xl md:text-2xl text-gray-600 mb-12;
}
}
/* 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;
transform: scale(1.1);
}
/* Loading States */
.skeleton {
@apply bg-gray-200 animate-pulse rounded;
}
/* Responsive Typography Scale */
@media (max-width: 768px) {
html {
font-size: 14px;
}
}
@media (min-width: 1536px) {
html {
font-size: 18px;
}
}

View File

@@ -0,0 +1,174 @@
/* 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

@@ -0,0 +1,38 @@
<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">
<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" /> (510) 701-2535</p>
<p><LucideMail class="footer-icon" /> hello@harborsmith.co</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

@@ -0,0 +1,85 @@
<template>
<nav
id="voyageNav"
:class="['voyage-nav', { scrolled: isScrolled }]"
>
<div class="nav-container">
<div class="nav-brand">
<img
:src="navLogo"
:alt="logoAlt"
class="nav-logo"
id="navLogo"
>
<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'
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 = () => {
isScrolled.value = window.pageYOffset > 50
}
const handleSmoothScroll = (event: Event, href: string) => {
if (!href.startsWith('#')) {
return
}
const target = document.querySelector(href)
if (!target) {
return
}
event.preventDefault()
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
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

@@ -0,0 +1,59 @@
<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">
<div class="booking-card">
<span class="booking-icon">
<LucideCalendar />
</span>
<h3>Schedule Service</h3>
<p>Book your maintenance appointment</p>
<button class="btn-booking" @click="handleBookNow">Book Now</button>
</div>
<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

@@ -0,0 +1,56 @@
<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">
<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">
<div class="gallery-overlay">
<span class="gallery-caption">Detailed Cleaning</span>
</div>
</div>
<div class="gallery-item">
<img src="/Washdown2.jpg" alt="Professional washdown">
<div class="gallery-overlay">
<span class="gallery-caption">Thorough Washdown</span>
</div>
</div>
<div class="gallery-item">
<img src="/Helm.jpg" alt="Interior maintenance">
<div class="gallery-overlay">
<span class="gallery-caption">Interior Care</span>
</div>
</div>
<div class="gallery-item">
<img src="/Foredeck.jpg" alt="Deck maintenance">
<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">
<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

@@ -0,0 +1,148 @@
<template>
<section class="hero-voyage" id="heroSection">
<div ref="videoContainer" class="hero-video-container">
<video
ref="videoElement"
autoplay
loop
muted
playsinline
class="hero-video"
@loadeddata="handleVideoLoaded"
>
<source
src="https://videos.pexels.com/video-files/3571264/3571264-uhd_2560_1440_30fps.mp4"
type="video/mp4"
>
</video>
<div
v-if="!videoLoaded"
class="hero-image-fallback"
:style="{ backgroundImage: 'url(/golden_gate.jpg)' }"
/>
<div class="hero-overlay gradient-warm" />
<div class="hero-overlay gradient-depth" />
</div>
<div class="hero-content">
<div class="hero-logo animate-fade-in">
<img src="/HARBOR-SMITH-white.png" alt="Harbor Smith" style="height: 250px; margin: 40px 0;">
</div>
<div class="trust-badge animate-fade-in">
<div class="stars">
<span class="stars-icons">
<LucideStar class="star-filled" />
<LucideStar class="star-filled" />
<LucideStar class="star-filled" />
<LucideStar class="star-filled" />
<LucideStar class="star-filled" />
</span>
</div>
<span>Trusted by 0+ seafarers</span>
</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>
Keep your vessel pristine with San Francisco Bay's premier mobile boat maintenance service.
</p>
<div class="hero-actions animate-fade-up-delay-2">
<button class="btn-primary-warm" @click="handlePhoneClick">
<LucidePhone class="btn-icon" />
Call (510) 701-2535
</button>
<button class="btn-secondary-warm" @click="handleServicesClick">
<LucideWrench class="btn-icon" />
View Our Services
</button>
</div>
<div class="scroll-indicator">
<span>Scroll to explore</span>
<div class="scroll-arrow">
<LucideChevronDown />
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useParallax } from '~/composables/useParallax'
import { useIntersectionAnimations } from '~/composables/useIntersectionAnimations'
import { useRipple } from '~/composables/useRipple'
const videoLoaded = ref(false)
const videoContainer = ref<HTMLElement | null>(null)
const videoElement = ref<HTMLVideoElement | null>(null)
useParallax(videoContainer, 0.5)
useIntersectionAnimations()
useRipple()
const handleVideoLoaded = () => {
videoLoaded.value = true
}
const handlePhoneClick = () => {
window.location.href = 'tel:510-701-2535'
}
const handleServicesClick = () => {
const element = document.querySelector('#services')
if (element) {
element.scrollIntoView({ behavior: 'smooth' })
}
}
const handleVideoVisibility = () => {
if (!videoElement.value) {
return
}
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
videoElement.value?.play()
} else {
videoElement.value?.pause()
}
})
}, { threshold: 0.25 })
observer.observe(videoElement.value)
return () => observer.disconnect()
}
const initSmoothScroll = () => {
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener('click', (event) => {
event.preventDefault()
const href = anchor.getAttribute('href')
if (!href) {
return
}
const target = document.querySelector(href)
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
})
})
}
onMounted(() => {
handleVideoVisibility()
initSmoothScroll()
})
</script>
<style scoped>
/* Additional animations defined in voyage-layout.css */
</style>

View File

@@ -0,0 +1,94 @@
<template>
<section class="fleet-showcase" id="services">
<div class="container">
<div class="section-header">
<h2 class="section-title">Our Premium 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;">
<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;">
<div style="width: 100%; height: 200px; overflow: hidden;">
<img src="/diver_cleaning.jpg" alt="Professional hull cleaning service" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<div style="padding: 30px;">
<h3 style="font-size: 24px; margin-bottom: 15px; color: #1e3a5f;">Hull Cleaning</h3>
<p style="color: #666; margin-bottom: 20px;">
Professional underwater hull cleaning to maintain your boat's performance and fuel efficiency.
</p>
<ul style="list-style: none; padding: 0; margin: 20px 0; text-align: left;">
<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" /> Extends hull life</li>
</ul>
<button class="btn-primary-warm" style="width: 100%;" @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;">
<div style="width: 100%; height: 200px; overflow: hidden;">
<img src="/Washdown.jpg" alt="Professional boat wash and wax service" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<div style="padding: 30px;">
<h3 style="font-size: 24px; margin-bottom: 15px; color: #1e3a5f;">Exterior Wash &amp; Wax</h3>
<p style="color: #666; margin-bottom: 20px;">
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;">
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Deep cleaning wash</li>
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> UV protection wax</li>
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Gel coat restoration</li>
</ul>
<button class="btn-primary-warm" style="width: 100%;" @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;">
<div style="width: 100%; height: 200px; overflow: hidden;">
<img src="/Anodes.jpg" alt="Zinc anode replacement service" style="width: 100%; height: 100%; object-fit: cover;">
</div>
<div style="padding: 30px;">
<h3 style="font-size: 24px; margin-bottom: 15px; color: #1e3a5f;">Anode Changes</h3>
<p style="color: #666; margin-bottom: 20px;">
Essential corrosion protection with regular zinc anode inspection and replacement.
</p>
<ul style="list-style: none; padding: 0; margin: 20px 0; text-align: left;">
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Prevents corrosion</li>
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Regular inspection</li>
<li style="padding: 8px 0;"><LucideCheck class="spec-icon" /> Marine-grade materials</li>
</ul>
<button class="btn-primary-warm" style="width: 100%;" @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;">
<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;">
</div>
<div style="padding: 30px;">
<h3 style="font-size: 24px; margin-bottom: 15px; color: #1e3a5f;">Interior Detailing</h3>
<p style="color: #666; margin-bottom: 20px;">
Thorough interior cleaning and conditioning for a fresh, comfortable cabin.
</p>
<ul style="list-style: none; padding: 0; margin: 20px 0; text-align: left;">
<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>
</ul>
<button class="btn-primary-warm" style="width: 100%;" @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

@@ -0,0 +1,76 @@
<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

@@ -0,0 +1,36 @@
<template>
<section class="services-section">
<div class="container">
<div class="service-stats">
<div class="stat-item">
<LucideShip class="stat-icon" />
<span class="stat-number">200+</span>
<span class="stat-label">Vessels Maintained</span>
</div>
<div class="stat-item">
<LucideAward class="stat-icon" />
<span class="stat-number">10+</span>
<span class="stat-label">Years Experience</span>
</div>
<div class="stat-item">
<LucideUsers class="stat-icon" />
<span class="stat-number">500+</span>
<span class="stat-label">Happy Clients</span>
</div>
<div class="stat-item">
<LucideShieldCheck class="stat-icon" />
<span class="stat-number">100%</span>
<span class="stat-label">Mobile Service</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 */
</style>

View File

@@ -0,0 +1,64 @@
<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 brings expert care directly to your dock, ensuring your vessel stays in pristine condition year-round.
</p>
<div class="feature-list">
<div class="feature-item">
<span class="feature-icon">
<LucideTruck />
</span>
<div>
<h4>Mobile Service</h4>
<p>We come to you - convenient service at your dock or marina</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">
<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

@@ -0,0 +1,96 @@
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

@@ -0,0 +1,47 @@
import { ref, onMounted, onBeforeUnmount } from 'vue'
export const useParallax = (elementRef, speed = 0.5) => {
const transform = ref('')
let ticking = false
const handleScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
if (elementRef.value) {
const scrolled = window.pageYOffset
const rate = scrolled * speed
transform.value = `translateY(${rate}px)`
elementRef.value.style.transform = transform.value
}
ticking = false
})
ticking = true
}
}
onMounted(() => {
// Add will-change for performance
if (elementRef.value) {
elementRef.value.style.willChange = 'transform'
}
// Check for reduced motion preference
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
if (!prefersReducedMotion) {
window.addEventListener('scroll', handleScroll, { passive: true })
handleScroll() // Initial calculation
}
})
onBeforeUnmount(() => {
window.removeEventListener('scroll', handleScroll)
if (elementRef.value) {
elementRef.value.style.willChange = 'auto'
}
})
return {
transform
}
}

View File

@@ -0,0 +1,79 @@
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

@@ -0,0 +1,22 @@
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

@@ -0,0 +1,13 @@
<template>
<div class="min-h-screen flex flex-col">
<AppNavbar />
<main class="flex-grow">
<slot />
</main>
<AppFooter />
</div>
</template>
<script setup lang="ts">
// Layout components are auto-imported by Nuxt
</script>

99
apps/website/nginx.conf Normal file
View File

@@ -0,0 +1,99 @@
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;
}
}
}

159
apps/website/nuxt.config.ts Normal file
View File

@@ -0,0 +1,159 @@
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')
}
}
}
}
})

13939
apps/website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
apps/website/package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"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",
"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",
"typescript": "^5.7.2"
}
}

View File

@@ -0,0 +1,55 @@
<template>
<div>
<!-- Hero Section with Video Background -->
<HeroSection />
<!-- Welcome Section -->
<WelcomeSection />
<!-- Services Section -->
<ServicesSection />
<!-- Trust Indicators Section -->
<TrustIndicators />
<!-- Testimonials Section -->
<TestimonialsSection />
<!-- Gallery Section -->
<GallerySection />
<!-- Booking Section -->
<BookingSection />
</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: 'Keep your vessel pristine with San Francisco Bay\'s premier mobile boat maintenance service.'
},
{
property: 'og:title',
content: 'Harbor Smith - Personalized Service Maintenance For Your Boat'
},
{
property: 'og:description',
content: 'Keep your vessel pristine with San Francisco Bay\'s premier mobile boat maintenance service.'
},
{
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>

View File

@@ -0,0 +1,47 @@
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.

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,18 @@
{
// 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"
}
]
}