Initial import of HarborSmith website
Some checks failed
build-website / build (push) Failing after 1m2s
24
apps/website/.gitignore
vendored
Normal 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
@@ -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
@@ -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.
|
||||
371
apps/website/assets/css/main.css
Normal 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;
|
||||
}
|
||||
}
|
||||
174
apps/website/assets/css/themes.css
Normal 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);
|
||||
}
|
||||
1659
apps/website/assets/css/voyage-layout.css
Normal file
38
apps/website/components/AppFooter.vue
Normal 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>© 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>
|
||||
85
apps/website/components/AppNavbar.vue
Normal 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>
|
||||
59
apps/website/components/BookingSection.vue
Normal 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>
|
||||
56
apps/website/components/GallerySection.vue
Normal 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>
|
||||
148
apps/website/components/HeroSection.vue
Normal 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>
|
||||
94
apps/website/components/ServicesSection.vue
Normal 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 & 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 & 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>
|
||||
76
apps/website/components/TestimonialsSection.vue
Normal 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 & 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>
|
||||
36
apps/website/components/TrustIndicators.vue
Normal 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>
|
||||
64
apps/website/components/WelcomeSection.vue
Normal 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 & 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>
|
||||
96
apps/website/composables/useIntersectionAnimations.js
Normal 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
|
||||
}
|
||||
}
|
||||
47
apps/website/composables/useParallax.js
Normal 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
|
||||
}
|
||||
}
|
||||
79
apps/website/composables/useRipple.js
Normal 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()
|
||||
})
|
||||
}
|
||||
22
apps/website/docker-compose.yml
Normal 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
|
||||
13
apps/website/layouts/default.vue
Normal 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
@@ -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
@@ -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
33
apps/website/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
55
apps/website/pages/index.vue
Normal 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>
|
||||
47
apps/website/plugins/lucide.client.js
Normal 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)
|
||||
})
|
||||
|
||||
BIN
apps/website/public/Anodes.jpg
Normal file
|
After Width: | Height: | Size: 6.5 MiB |
BIN
apps/website/public/Calendar.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
apps/website/public/ExtCleaning.jpg
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
apps/website/public/Foredeck.jpg
Normal file
|
After Width: | Height: | Size: 5.1 MiB |
BIN
apps/website/public/HARBOR-SMITH-white.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
apps/website/public/HARBOR-SMITH_navy.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
apps/website/public/Helm.jpg
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
apps/website/public/Hull Clean Pricing.jpg
Normal file
|
After Width: | Height: | Size: 533 KiB |
BIN
apps/website/public/Interior.jpg
Normal file
|
After Width: | Height: | Size: 4.5 MiB |
BIN
apps/website/public/Licensed.jpg
Normal file
|
After Width: | Height: | Size: 7.3 MiB |
BIN
apps/website/public/QRCode-White-sm.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
apps/website/public/QRCode-White.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
apps/website/public/QRCode.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
apps/website/public/SpecialRequest.jpg
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
BIN
apps/website/public/Washdown.jpg
Normal file
|
After Width: | Height: | Size: 6.1 MiB |
BIN
apps/website/public/Washdown2.jpg
Normal file
|
After Width: | Height: | Size: 4.4 MiB |
BIN
apps/website/public/Waxing.jpg
Normal file
|
After Width: | Height: | Size: 3.1 MiB |
2
apps/website/public/_robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
BIN
apps/website/public/diver_cleaning.jpg
Normal file
|
After Width: | Height: | Size: 4.1 MiB |
BIN
apps/website/public/diver_cleaning_2.jpg
Normal file
|
After Width: | Height: | Size: 8.1 MiB |
BIN
apps/website/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
apps/website/public/golden_gate.jpg
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
apps/website/public/iStock-2189089654.jpg
Normal file
|
After Width: | Height: | Size: 5.5 MiB |
BIN
apps/website/public/iStock-504868014.jpg
Normal file
|
After Width: | Height: | Size: 6.4 MiB |
BIN
apps/website/public/iStock-923244752.jpg
Normal file
|
After Width: | Height: | Size: 9.1 MiB |
BIN
apps/website/public/iStock-923244752b.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
apps/website/public/kid_1.jpeg
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
apps/website/public/leah_1.jpeg
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
apps/website/public/sausalito-boat-show-2024.jpg
Normal file
|
After Width: | Height: | Size: 427 KiB |
BIN
apps/website/public/sf_bay_exposure.jpg
Normal file
|
After Width: | Height: | Size: 99 KiB |
18
apps/website/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||