Initial commit: Port Amador landing site with Docker deployment setup

This commit is contained in:
2025-09-22 14:23:01 +02:00
parent c40315e86c
commit 05c3fe71a7
26 changed files with 1611 additions and 124 deletions

View File

@@ -0,0 +1,76 @@
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
try {
const data = await request.json();
// Validate required fields
const { firstName, lastName, email, phone, message } = data;
if (!firstName || !lastName || !email || !phone) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
// Email validation regex
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return NextResponse.json(
{ error: 'Invalid email address' },
{ status: 400 }
);
}
// Log the submission (in production, you would:
// 1. Send an email notification using SendGrid, Postmark, or Resend
// 2. Store in a database like Supabase or PostgreSQL
// 3. Integrate with CRM like HubSpot or Salesforce)
console.log('Contact form submission received:', {
firstName,
lastName,
email,
phone,
message: message || '(No message provided)',
timestamp: new Date().toISOString()
});
// Simulate email sending (in production, replace with actual email service)
// Example with Resend:
// await resend.emails.send({
// from: 'onboarding@portamador.com',
// to: 'am@portamador.com',
// subject: `New Contact Form Submission from ${firstName} ${lastName}`,
// html: `
// <h2>New Contact Form Submission</h2>
// <p><strong>Name:</strong> ${firstName} ${lastName}</p>
// <p><strong>Email:</strong> ${email}</p>
// <p><strong>Phone:</strong> ${phone}</p>
// <p><strong>Message:</strong> ${message || 'No message provided'}</p>
// `
// });
// Return success response
return NextResponse.json(
{
success: true,
message: 'Thank you for your submission. We will contact you soon.',
data: {
firstName,
lastName,
email
}
},
{ status: 200 }
);
} catch (error) {
console.error('Error processing contact form:', error);
return NextResponse.json(
{ error: 'Internal server error. Please try again later.' },
{ status: 500 }
);
}
}

596
src/app/contact/page.tsx Normal file
View File

@@ -0,0 +1,596 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import Image from 'next/image';
import { useMediaQuery } from '@react-hook/media-query';
import { ChevronDown, Phone, Mail } from 'lucide-react';
import { z } from 'zod';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import {
Form,
FormControl,
FormField,
FormItem,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
const formSchema = z.object({
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
email: z.string().email('Invalid email address'),
phone: z.string().min(1, 'Phone is required'),
message: z.string().optional(),
});
export default function ContactPage() {
const [mounted, setMounted] = useState(false);
const [logoPosition, setLogoPosition] = useState('center');
const [logoStyle, setLogoStyle] = useState<React.CSSProperties>({});
const [buttonOpacity, setButtonOpacity] = useState(1);
const [chevronOpacity, setChevronOpacity] = useState(1);
const [contactTop, setContactTop] = useState(0);
const [windowHeight, setWindowHeight] = useState(0);
const contactSectionRef = useRef<HTMLDivElement>(null);
const logoRef = useRef<HTMLDivElement>(null);
const animationFrameRef = useRef<number | null>(null);
const lastScrollY = useRef(0);
const isMobile = useMediaQuery("(max-width: 768px)");
const isDesktop = useMediaQuery("(min-width: 1280px)");
// Manage form state
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
phone: "",
message: "",
},
});
async function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
}
// Logo dimensions based on screen size
const logoWidth = isMobile ? 240 : isDesktop ? 316 : 280;
const logoHeight = isMobile ? 115 : isDesktop ? 151 : 134;
useEffect(() => {
setMounted(true);
// Set initial dimensions on mount
setWindowHeight(window.innerHeight);
if (contactSectionRef.current) {
setContactTop(contactSectionRef.current.offsetTop);
}
}, []);
const updateLogoPosition = () => {
if (!contactSectionRef.current) return;
const scrollY = window.scrollY;
const currentWindowHeight = window.innerHeight;
const currentContactTop = contactSectionRef.current.offsetTop;
// Update cached values if needed
if (currentContactTop !== contactTop) {
setContactTop(currentContactTop);
}
if (currentWindowHeight !== windowHeight) {
setWindowHeight(currentWindowHeight);
}
// Calculate positions - adjusted for scaled logo
const targetTopPosition = isMobile ? 10 : 20; // Adjusted for smaller scaled logo
if (isMobile) {
// For mobile, calculate where the logo should end up
const startY = currentWindowHeight * 0.35; // Logo starts higher - 35% from top
const endY = targetTopPosition + (logoHeight * 0.5) / 2; // Account for 50% scale
const totalDistance = startY - endY;
// Keep animation ending at the full contact section position
const animationEndScroll = currentContactTop;
if (scrollY >= animationEndScroll) {
// Logo has reached destination - keep it fixed but move with scroll
setLogoPosition('top');
// Calculate position to simulate being part of the page
const scrollPastEnd = scrollY - animationEndScroll;
const fixedY = endY - scrollPastEnd;
const mobileScale = 0.5; // Final scale for mobile
setLogoStyle({
position: 'fixed',
top: `${fixedY}px`,
left: '50%',
transform: `translate3d(-50%, 0, 0) scale3d(${mobileScale}, ${mobileScale}, 1)`,
transformOrigin: 'center',
willChange: 'transform',
transition: 'none',
zIndex: 50
});
} else if (scrollY > 0) {
// Animate logo from center to destination - starts immediately at any scroll
const progress = scrollY / animationEndScroll;
const currentY = startY - (totalDistance * progress);
const mobileScale = 1 - (0.5 * progress); // Scale from 1.0 to 0.5
setLogoPosition('animating');
setLogoStyle({
position: 'fixed',
top: `${currentY}px`,
left: '50%',
transform: `translate3d(-50%, 0, 0) scale3d(${mobileScale}, ${mobileScale}, 1)`,
transformOrigin: 'center',
willChange: 'transform',
transition: 'none',
zIndex: 50
});
} else {
// At the top - logo at starting position
setLogoPosition('center');
setLogoStyle({
position: 'fixed',
top: `${startY}px`,
left: '50%',
transform: 'translate3d(-50%, 0, 0) scale3d(1, 1, 1)',
transformOrigin: 'center',
willChange: 'transform',
transition: 'none',
zIndex: 50
});
}
} else {
// Desktop - standard animation with scaling
const logoSpeed = 0.4;
const centerY = currentWindowHeight / 2;
const targetTopPosition = 20; // Reduced from 100px to account for smaller logo
const totalDistance = centerY - targetTopPosition - logoHeight / 2;
const logoYPosition = -(scrollY * logoSpeed);
const maxUpwardMovement = -totalDistance;
const animatedY = Math.max(logoYPosition, maxUpwardMovement);
// Calculate scale based on scroll progress
const maxScroll = 500; // Scroll distance at which scaling completes
const scrollProgress = Math.min(scrollY / maxScroll, 1);
const scale = 1 - (0.6 * scrollProgress); // Scale from 1.0 to 0.4
// Update state based on scroll
if (scrollY > 10) {
setLogoPosition('animating');
} else {
setLogoPosition('center');
}
// Fixed positioning with animation and scaling
setLogoStyle({
position: 'fixed',
top: '50%',
left: '50%',
transform: `translate3d(-50%, calc(-50% + ${animatedY}px), 0) scale3d(${scale}, ${scale}, 1)`,
transformOrigin: 'center',
willChange: 'transform',
transition: 'none',
zIndex: 50
});
}
// Hide button and chevron - faster on mobile
const fadeThreshold = isMobile ? 5 : 10; // Fade out at just 5px scroll on mobile
if (scrollY > fadeThreshold) {
setButtonOpacity(0);
setChevronOpacity(0);
} else {
setButtonOpacity(1);
setChevronOpacity(1);
}
};
// Animated scroll to form
const scrollToForm = () => {
if (!contactSectionRef.current) {
console.error('Contact section ref not found');
return;
}
console.log('Starting scroll animation to:', contactSectionRef.current.offsetTop);
// Use native smooth scrolling
window.scrollTo({
top: contactSectionRef.current.offsetTop,
behavior: 'smooth'
});
};
// Add scroll listener for bidirectional animation with RAF
useEffect(() => {
let ticking = false;
const handleScroll = () => {
lastScrollY.current = window.scrollY;
if (!ticking) {
animationFrameRef.current = requestAnimationFrame(() => {
updateLogoPosition();
ticking = false;
});
ticking = true;
}
};
const handleResize = () => {
setWindowHeight(window.innerHeight);
if (contactSectionRef.current) {
setContactTop(contactSectionRef.current.offsetTop);
}
updateLogoPosition();
};
// Initial position update
updateLogoPosition();
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [isMobile, isDesktop, logoHeight, windowHeight]);
// Don't render until mounted to avoid hydration mismatch
if (!mounted) {
return null;
}
return (
<div className="w-full bg-[#1b233b]">
{/* Hero Section - Full Viewport Height */}
<section className="relative h-screen flex flex-col items-center justify-center">
{/* Single Port Amador Logo with dynamic positioning - Always rendered */}
<div
ref={logoRef}
className="z-50"
style={logoStyle}
>
<Image
src="/logo.png"
alt="Port Amador"
width={logoWidth}
height={logoHeight}
priority
style={{
width: `${logoWidth}px`,
height: `${logoHeight}px`,
objectFit: 'contain'
}}
/>
</div>
{/* Button with fade out on scroll */}
<button
onClick={scrollToForm}
className={`fixed z-30 px-8 py-3 bg-[#C6AE97] text-[#1B233B] font-['bill_corporate_medium'] font-normal text-base uppercase tracking-wider rounded-md hover:bg-[#D4C1AC] transition-all`}
style={{
bottom: '120px',
left: '50%',
transform: 'translateX(-50%)',
opacity: buttonOpacity,
pointerEvents: buttonOpacity > 0 ? 'auto' : 'none',
transition: 'opacity 0.3s ease-out'
}}
>
CONNECT WITH US
</button>
{/* Chevron Down - with fade out on scroll */}
<div
className="fixed z-20"
style={{
bottom: '40px',
left: '50%',
transform: 'translateX(-50%)',
opacity: chevronOpacity,
transition: 'opacity 0.3s ease-out'
}}
>
<ChevronDown
className="text-[#C6AE97] animate-bounce"
size={32}
/>
</div>
</section>
{/* Contact Section - Desktop Layout with Marina Image */}
<section
ref={contactSectionRef}
className="w-full relative"
>
{isMobile ? (
// Mobile Layout - Stacked with Image
<div className="flex flex-col min-h-screen pt-[150px]">
{/* Form Section */}
<div className="px-8 pb-12">
<h2 className="font-['Palatino',_serif] text-[#C6AE97] text-[40px] mb-8 font-normal text-center">
Connect with us
</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="First Name*"
{...field}
className="bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 py-2 font-['bill_corporate_medium'] font-light text-[16px]"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Last Name*"
{...field}
className="bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 py-2 font-['bill_corporate_medium'] font-light text-[16px]"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Email*"
type="email"
{...field}
className="bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 py-2 font-['bill_corporate_medium'] font-light text-[16px]"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
placeholder="Phone number*"
{...field}
className="bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 py-2 font-['bill_corporate_medium'] font-light text-[16px]"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
placeholder="Message"
{...field}
className="min-h-[80px] bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 py-2 font-['bill_corporate_medium'] font-light text-[16px] resize-none"
/>
</FormControl>
</FormItem>
)}
/>
<div className="pt-6">
<Button
type="submit"
className="w-full bg-[#C6AE97] text-[#1B233B] hover:bg-[#D4C1AC] font-['bill_corporate_medium'] font-medium text-[16px] uppercase tracking-wider py-3 rounded-[5px]"
>
SUBMIT
</Button>
</div>
</form>
</Form>
</div>
{/* Marina Image Section */}
<div className="relative w-full h-[300px] mt-8 px-4">
<div className="relative w-full h-full">
<Image
src="/marina.png"
alt="Port Amador Marina"
fill
className="object-cover object-center"
sizes="(max-width: 768px) 90vw, 100vw"
priority
/>
</div>
</div>
{/* Footer Section */}
<div className="px-8 py-8 mt-auto">
<div className="flex justify-between items-end">
<div className="flex flex-col space-y-0">
<a href="tel:+13109132597" className="font-['bill_corporate_medium'] font-light text-[14px] text-[#C6AE97] hover:text-[#D4C1AC] transition-colors">
+1 310 913 2597
</a>
<a href="mailto:am@portamador.com" className="font-['bill_corporate_medium'] font-light text-[14px] text-[#C6AE97] hover:text-[#D4C1AC] transition-colors">
am@portamador.com
</a>
</div>
<div className="text-[#C6AE97] text-[14px] font-['bill_corporate_medium'] font-light">
© Port Amador 2025
</div>
</div>
</div>
</div>
) : (
// Desktop Layout - Matching Figma exactly
<div className="min-h-screen flex flex-col relative pt-[200px]">
<div className="w-full max-w-[1600px] mx-auto flex items-stretch flex-1">
{/* Left Side - Marina Image - Aligned with form content */}
<div className="w-[45%] relative">
<div className="absolute top-0 bottom-0 left-[80px] right-[40px]">
<Image
src="/marina.png"
alt="Port Amador Marina"
fill
className="object-cover object-center"
sizes="45vw"
priority
/>
</div>
</div>
{/* Right Side - Form Section */}
<div className="w-[55%] flex flex-col justify-center py-[40px] pl-[40px] pr-[80px]">
{/* Heading */}
<h2 className="font-['Palatino',_serif] text-[#C6AE97] text-[72px] leading-none mb-12 font-normal">
Connect with us
</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* First Row - First Name and Last Name */}
<div className="grid grid-cols-2 gap-6">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
placeholder="First Name*"
className="bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 pb-1 pt-0 font-['bill_corporate_medium'] font-light text-[16px] focus:outline-none focus:ring-0"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
placeholder="Last Name*"
className="bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 pb-1 pt-0 font-['bill_corporate_medium'] font-light text-[16px] focus:outline-none focus:ring-0"
/>
</FormControl>
</FormItem>
)}
/>
</div>
{/* Second Row - Email and Phone */}
<div className="grid grid-cols-2 gap-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
type="email"
{...field}
placeholder="Email*"
className="bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 pb-1 pt-0 font-['bill_corporate_medium'] font-light text-[16px] focus:outline-none focus:ring-0"
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
{...field}
placeholder="Phone number*"
className="bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 pb-1 pt-0 font-['bill_corporate_medium'] font-light text-[16px] focus:outline-none focus:ring-0"
/>
</FormControl>
</FormItem>
)}
/>
</div>
{/* Message Field */}
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea
{...field}
placeholder="Message"
className="min-h-[60px] bg-transparent border-b border-t-0 border-l-0 border-r-0 border-white/60 text-white placeholder:text-white/70 focus:border-white rounded-none px-0 pb-1 pt-0 font-['bill_corporate_medium'] font-light text-[16px] resize-none focus:outline-none focus:ring-0"
/>
</FormControl>
</FormItem>
)}
/>
{/* Submit Button */}
<div className="pt-4">
<Button
type="submit"
className="w-full bg-[#C6AE97] text-[#1B233B] hover:bg-[#D4C1AC] font-['bill_corporate_medium'] font-medium text-[18px] uppercase tracking-[0.05em] h-[50px] rounded-[3px]"
>
SUBMIT
</Button>
</div>
</form>
</Form>
</div>
</div>
{/* Footer - at bottom of page */}
<div className="w-full max-w-[1600px] mx-auto px-[80px] pb-8 pt-12">
<div className="flex justify-between items-end">
<div className="flex flex-col space-y-0 text-[#C6AE97]">
<a href="tel:+13109132597" className="font-['bill_corporate_medium'] font-light text-[14px] hover:text-[#D4C1AC] transition-colors">
+1 310 913 2597
</a>
<a href="mailto:am@portamador.com" className="font-['bill_corporate_medium'] font-light text-[14px] hover:text-[#D4C1AC] transition-colors">
am@portamador.com
</a>
</div>
<div className="text-[#C6AE97] text-[14px] font-['bill_corporate_medium'] font-light">
© Port Amador 2025
</div>
</div>
</div>
</div>
)}
</section>
</div>
);
}

View File

@@ -1,26 +1,171 @@
@import "tailwindcss";
@import "tw-animate-css";
:root {
--background: #ffffff;
--foreground: #171717;
/* Custom fonts */
@font-face {
font-family: 'Palatino';
src: local('Palatino'), local('Palatino Linotype'), local('Book Antiqua');
font-weight: normal;
font-style: normal;
}
/* Bill Corporate Medium - Book weight */
@font-face {
font-family: 'bill corporate medium';
src: url('/fonts/Bill corporate medium book.woff') format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}
/* Bill Corporate Medium - Roman/Regular weight */
@font-face {
font-family: 'bill corporate medium';
src: url('/fonts/Bill corporate medium roman.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* Bill Corporate Medium - Medium weight (using roman as fallback) */
@font-face {
font-family: 'bill corporate medium';
src: url('/fonts/Bill corporate medium roman.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-serif: 'Palatino', 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
--font-mono: var(--font-geist-mono);
/* Port Amador Brand Colors */
--color-navy: #1B233B;
--color-gold: #C6AE97;
--color-navy-light: #2A3550;
--color-gold-light: #D4C1AC;
--color-gold-dark: #B89D84;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
:root {
--radius: 0.313rem; /* 5px from Figma design */
--background: #ffffff;
--foreground: #1B233B; /* Navy */
--card: #ffffff;
--card-foreground: #1B233B;
--popover: #ffffff;
--popover-foreground: #1B233B;
--primary: #C6AE97; /* Gold */
--primary-foreground: #1B233B;
--secondary: #1B233B; /* Navy */
--secondary-foreground: #ffffff;
--muted: #f4f4f4;
--muted-foreground: #6c6c6c;
--accent: #C6AE97; /* Gold */
--accent-foreground: #1B233B;
--destructive: #ef4444;
--border: #e5e5e5;
--input: #e5e5e5;
--ring: #C6AE97; /* Gold focus ring */
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: #1B233B; /* Navy background */
--foreground: #ffffff;
--card: #2A3550; /* Lighter navy */
--card-foreground: #ffffff;
--popover: #2A3550;
--popover-foreground: #ffffff;
--primary: #C6AE97; /* Gold remains primary */
--primary-foreground: #1B233B;
--secondary: #D4C1AC; /* Light gold */
--secondary-foreground: #1B233B;
--muted: #2A3550;
--muted-foreground: #a0a0a0;
--accent: #C6AE97; /* Gold accent */
--accent-foreground: #1B233B;
--destructive: #ef4444;
--border: rgba(198, 174, 151, 0.2); /* Gold border with transparency */
--input: rgba(198, 174, 151, 0.15);
--ring: #C6AE97; /* Gold focus ring */
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
/* Enable smooth scrolling */
html {
scroll-behavior: smooth;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

BIN
src/app/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@@ -13,9 +13,9 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
title: "Port Amador",
description: "Premium marine equipment and services",
};;
export default function RootLayout({
children,
@@ -25,7 +25,7 @@ export default function RootLayout({
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-[#1b233b]`}
>
{children}
</body>

View File

@@ -1,103 +1,18 @@
import Image from "next/image";
import Link from 'next/link';
export default function Home() {
return (
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
<div className="min-h-screen bg-[#1b233b] flex items-center justify-center">
<div className="text-center">
<h1 className="text-6xl font-['Palatino',_serif] text-[#C6AE97] mb-4">PORT AMADOR</h1>
<p className="text-xl text-white font-['bill_corporate_medium'] font-light mb-8">PANAMA</p>
<Link
href="/contact"
className="inline-block bg-[#C6AE97] text-[#1B233B] px-8 py-3 rounded-[5px] font-['bill_corporate_medium'] font-medium text-lg hover:bg-[#D4C1AC] transition-colors"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
VIEW CONTACT PAGE
</Link>
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

167
src/components/ui/form.tsx Normal file
View File

@@ -0,0 +1,167 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
FormProvider,
useFormContext,
useFormState,
type ControllerProps,
type FieldPath,
type FieldValues,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div
data-slot="form-item"
className={cn("grid gap-2", className)}
{...props}
/>
</FormItemContext.Provider>
)
}
function FormLabel({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField()
return (
<Label
data-slot="form-label"
data-error={!!error}
className={cn("data-[error=true]:text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
}
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
data-slot="form-control"
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
}
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
const { formDescriptionId } = useFormField()
return (
<p
data-slot="form-description"
id={formDescriptionId}
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message ?? "") : props.children
if (!body) {
return null
}
return (
<p
data-slot="form-message"
id={formMessageId}
className={cn("text-destructive text-sm", className)}
{...props}
>
{body}
</p>
)
}
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@@ -0,0 +1,21 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "@/lib/utils"
function Label({
className,
...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
return (
<LabelPrimitive.Root
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}