Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
8f5fe42062 Salote Massage Portfolio 2026-02-06 05:11:37 +00:00
11 changed files with 627 additions and 164 deletions

View File

@ -0,0 +1,55 @@
'use strict';
module.exports = {
async up(queryInterface, Sequelize) {
const [roles] = await queryInterface.sequelize.query(
`SELECT id FROM "roles" WHERE name = 'Public' LIMIT 1;`
);
if (!roles || roles.length === 0) {
console.error("Public role not found");
return;
}
const publicRoleId = roles[0].id;
const permissionsToGrant = [
'READ_SERVICES',
'READ_TESTIMONIALS',
'READ_PRACTITIONERS',
'READ_SERVICE_CATEGORIES',
'READ_PRICING_OPTIONS',
'READ_PACKAGES',
'READ_SERVICE_ADD_ONS',
'CREATE_INQUIRIES'
];
const [permissions] = await queryInterface.sequelize.query(
`SELECT id, name FROM "permissions" WHERE name IN (${permissionsToGrant.map(p => `'${p}'`).join(',')});`
);
const records = permissions.map(p => ({
roles_permissionsId: publicRoleId,
permissionId: p.id,
createdAt: new Date(),
updatedAt: new Date()
}));
if (records.length > 0) {
await queryInterface.bulkInsert('rolesPermissionsPermissions', records);
}
},
async down(queryInterface, Sequelize) {
const [roles] = await queryInterface.sequelize.query(
`SELECT id FROM "roles" WHERE name = 'Public' LIMIT 1;`
);
if (roles && roles.length > 0) {
const publicRoleId = roles[0].id;
await queryInterface.sequelize.query(
`DELETE FROM "rolesPermissionsPermissions" WHERE "roles_permissionsId" = '${publicRoleId}';`
);
}
}
};

View File

@ -1,4 +1,3 @@
const express = require('express');
const cors = require('cors');
const app = express();
@ -113,27 +112,27 @@ app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoute
app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes);
app.use('/api/practitioners', passport.authenticate('jwt', {session: false}), practitionersRoutes);
app.use('/api/practitioners', practitionersRoutes);
app.use('/api/service_categories', passport.authenticate('jwt', {session: false}), service_categoriesRoutes);
app.use('/api/service_categories', service_categoriesRoutes);
app.use('/api/services', passport.authenticate('jwt', {session: false}), servicesRoutes);
app.use('/api/services', servicesRoutes);
app.use('/api/service_add_ons', passport.authenticate('jwt', {session: false}), service_add_onsRoutes);
app.use('/api/service_add_ons', service_add_onsRoutes);
app.use('/api/pricing_options', passport.authenticate('jwt', {session: false}), pricing_optionsRoutes);
app.use('/api/pricing_options', pricing_optionsRoutes);
app.use('/api/packages', passport.authenticate('jwt', {session: false}), packagesRoutes);
app.use('/api/packages', packagesRoutes);
app.use('/api/testimonials', passport.authenticate('jwt', {session: false}), testimonialsRoutes);
app.use('/api/testimonials', testimonialsRoutes);
app.use('/api/inquiries', passport.authenticate('jwt', {session: false}), inquiriesRoutes);
app.use('/api/inquiries', inquiriesRoutes);
app.use('/api/appointments', passport.authenticate('jwt', {session: false}), appointmentsRoutes);
app.use('/api/pages', passport.authenticate('jwt', {session: false}), pagesRoutes);
app.use('/api/site_settings', passport.authenticate('jwt', {session: false}), site_settingsRoutes);
app.use('/api/site_settings', site_settingsRoutes);
app.use(
'/api/openai',
@ -179,4 +178,4 @@ db.sequelize.sync().then(function () {
});
});
module.exports = app;
module.exports = app;

View File

@ -0,0 +1,56 @@
import React from 'react';
const About = () => {
return (
<section id="about" className="py-24 bg-white">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row items-center gap-16">
<div className="md:w-1/2">
<div className="relative">
<div className="aspect-[4/5] rounded-2xl bg-stone-100 overflow-hidden shadow-2xl relative z-10">
<img
src="https://images.pexels.com/photos/3757942/pexels-photo-3757942.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1"
alt="Salote Toilolo"
className="w-full h-full object-cover grayscale-[0.2] sepia-[0.1]"
/>
</div>
<div className="absolute -top-6 -left-6 w-24 h-24 bg-teal-50 rounded-full z-0"></div>
<div className="absolute -bottom-6 -right-6 w-32 h-32 bg-sky-50 rounded-full z-0"></div>
</div>
</div>
<div className="md:w-1/2">
<span className="text-rose-400 font-medium tracking-widest uppercase text-sm mb-4 block">Meet Salote</span>
<h2 className="text-4xl font-serif text-sky-900 mb-8 leading-snug">
A steady presence for <br /> transformative times.
</h2>
<div className="space-y-6 text-sky-800/80 leading-relaxed text-lg">
<p>
Salote Toilolo is a Hawaii-based licensed massage therapist and certified doula offering nurturing, trauma-informed bodywork for pregnancy, postpartum, and women&apos;s wellness.
</p>
<p>
Her sessions blend skilled therapeutic touch with a calm, grounded presence inspired by ocean rhythms and Hawaiian healing traditions. She works slowly and with consent, listening to the nervous system and honoring the seasons of pregnancy and postpartum.
</p>
<p>
Salote&apos;s goal is to help you breathe deeper, soften tension, and feel held from the inside out. Every body deserves to feel safe, especially during the most vulnerable and powerful times of life.
</p>
</div>
<div className="mt-10 pt-10 border-t border-stone-100 grid grid-cols-2 gap-8">
<div>
<h4 className="font-serif text-sky-900 font-bold mb-1">Licensed LMT</h4>
<p className="text-sm text-sky-800/60 uppercase tracking-wider">HI-LMT-48291</p>
</div>
<div>
<h4 className="font-serif text-sky-900 font-bold mb-1">Certified Doula</h4>
<p className="text-sm text-sky-800/60 uppercase tracking-wider">Birth & Postpartum</p>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
export default About;

View File

@ -0,0 +1,179 @@
import React, { useState } from 'react';
import axios from 'axios';
const Contact = () => {
const [formData, setFormData] = useState({
full_name: '',
email: '',
phone: '',
message: '',
preferred_contact_method: 'Email',
source_page: 'Homepage'
});
const [status, setStatus] = useState({ type: '', message: '' });
const [loading, setLoading] = useState(false);
const handleChange = (e: any) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e: any) => {
e.preventDefault();
setLoading(true);
setStatus({ type: '', message: '' });
try {
await axios.post('/inquiries', { data: formData });
setStatus({ type: 'success', message: 'Thank you! Salote will reach out to you soon.' });
setFormData({
full_name: '',
email: '',
phone: '',
message: '',
preferred_contact_method: 'Email',
source_page: 'Homepage'
});
} catch (error) {
console.error('Error submitting inquiry:', error);
setStatus({ type: 'error', message: 'Something went wrong. Please try again or email directly.' });
} finally {
setLoading(false);
}
};
return (
<section id="contact" className="py-24 bg-stone-50">
<div className="container mx-auto px-6">
<div className="flex flex-col lg:flex-row gap-16 items-start">
<div className="lg:w-1/3">
<span className="text-rose-400 font-medium tracking-widest uppercase text-sm mb-4 block">Get in Touch</span>
<h2 className="text-4xl font-serif text-sky-900 mb-6">Begin Your Journey</h2>
<p className="text-sky-800/70 mb-10 leading-relaxed">
Ready to book a session or have questions about how I can support you?
Fill out the form, and I&apos;ll get back to you within 24-48 hours.
</p>
<div className="space-y-6">
<div className="flex items-center space-x-4">
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center text-sky-600 shadow-sm">
</div>
<div>
<p className="text-xs text-sky-800/40 uppercase font-bold tracking-widest">Email</p>
<p className="text-sky-900 font-medium">salote@salotemassage.com</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center text-sky-600 shadow-sm">
</div>
<div>
<p className="text-xs text-sky-800/40 uppercase font-bold tracking-widest">Phone</p>
<p className="text-sky-900 font-medium">+1 (808) 555-0142</p>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="w-10 h-10 bg-white rounded-full flex items-center justify-center text-sky-600 shadow-sm">
📍
</div>
<div>
<p className="text-xs text-sky-800/40 uppercase font-bold tracking-widest">Location</p>
<p className="text-sky-900 font-medium">Honolulu, Oahu</p>
</div>
</div>
</div>
</div>
<div className="lg:w-2/3 w-full">
<div className="bg-white p-10 md:p-12 rounded-3xl shadow-xl shadow-sky-900/5">
{status.message && (
<div className={`mb-8 p-4 rounded-xl text-center font-medium ${status.type === 'success' ? 'bg-teal-50 text-teal-700' : 'bg-rose-50 text-rose-700'}`}>
{status.message}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-bold text-sky-900 mb-2 uppercase tracking-wide">Full Name</label>
<input
type="text"
name="full_name"
required
value={formData.full_name}
onChange={handleChange}
className="w-full px-4 py-3 rounded-xl border border-stone-100 bg-stone-50 focus:bg-white focus:ring-2 focus:ring-sky-100 focus:border-sky-300 outline-none transition-all"
placeholder="Your name"
/>
</div>
<div>
<label className="block text-sm font-bold text-sky-900 mb-2 uppercase tracking-wide">Email Address</label>
<input
type="email"
name="email"
required
value={formData.email}
onChange={handleChange}
className="w-full px-4 py-3 rounded-xl border border-stone-100 bg-stone-50 focus:bg-white focus:ring-2 focus:ring-sky-100 focus:border-sky-300 outline-none transition-all"
placeholder="email@example.com"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-bold text-sky-900 mb-2 uppercase tracking-wide">Phone Number</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleChange}
className="w-full px-4 py-3 rounded-xl border border-stone-100 bg-stone-50 focus:bg-white focus:ring-2 focus:ring-sky-100 focus:border-sky-300 outline-none transition-all"
placeholder="(808) 000-0000"
/>
</div>
<div>
<label className="block text-sm font-bold text-sky-900 mb-2 uppercase tracking-wide">Preferred Contact</label>
<select
name="preferred_contact_method"
value={formData.preferred_contact_method}
onChange={handleChange}
className="w-full px-4 py-3 rounded-xl border border-stone-100 bg-stone-50 focus:bg-white focus:ring-2 focus:ring-sky-100 focus:border-sky-300 outline-none transition-all"
>
<option>Email</option>
<option>Phone</option>
<option>Text</option>
</select>
</div>
</div>
<div>
<label className="block text-sm font-bold text-sky-900 mb-2 uppercase tracking-wide">Message / Service Interest</label>
<textarea
name="message"
rows={4}
required
value={formData.message}
onChange={handleChange}
className="w-full px-4 py-3 rounded-xl border border-stone-100 bg-stone-50 focus:bg-white focus:ring-2 focus:ring-sky-100 focus:border-sky-300 outline-none transition-all"
placeholder="Tell me a bit about what you&apos;re looking for..."
></textarea>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-4 bg-sky-600 text-white rounded-xl font-bold text-lg hover:bg-sky-700 transition-all disabled:opacity-50"
>
{loading ? 'Sending...' : 'Send Inquiry'}
</button>
</form>
</div>
</div>
</div>
</div>
</section>
);
};
export default Contact;

View File

@ -0,0 +1,30 @@
import React from 'react';
const Footer = () => {
return (
<footer className="py-12 bg-white border-t border-stone-100">
<div className="container mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-8">
<div>
<span className="text-xl font-serif text-sky-900 font-bold">Salote Toilolo</span>
<p className="text-sm text-sky-800/50 mt-2">Nurturing Touch & Maternal Support</p>
</div>
<div className="flex space-x-8 text-sm font-medium text-sky-800">
<a href="#about" className="hover:text-rose-400">About</a>
<a href="#services" className="hover:text-rose-400">Services</a>
<a href="#pricing" className="hover:text-rose-400">Pricing</a>
<a href="#contact" className="hover:text-rose-400">Contact</a>
</div>
<div className="text-sm text-sky-800/40">
© 2026 Salote Toilolo. All rights reserved.
</div>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@ -0,0 +1,24 @@
import React from 'react';
import Link from 'next/link';
const Header = () => {
return (
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 backdrop-blur-md border-b border-sky-100">
<div className="container mx-auto px-6 py-4 flex justify-between items-center">
<Link href="/" className="text-2xl font-serif text-sky-900 font-bold tracking-tight">
Salote Toilolo
</Link>
<nav className="hidden md:flex space-x-8 text-sm font-medium text-sky-800">
<a href="#about" className="hover:text-rose-400 transition-colors">About</a>
<a href="#services" className="hover:text-rose-400 transition-colors">Services</a>
<a href="#pricing" className="hover:text-rose-400 transition-colors">Pricing</a>
<a href="#testimonials" className="hover:text-rose-400 transition-colors">Testimonials</a>
<a href="#contact" className="px-4 py-2 bg-sky-600 text-white rounded-full hover:bg-sky-700 transition-colors">Book a Session</a>
</nav>
</div>
</header>
);
};
export default Header;

View File

@ -0,0 +1,42 @@
import React from 'react';
const Hero = () => {
return (
<section className="relative min-h-screen flex items-center justify-center pt-20 overflow-hidden bg-stone-50">
{/* Background Decorative Elements */}
<div className="absolute top-0 right-0 w-1/3 h-1/3 bg-sky-100 rounded-full blur-3xl opacity-50 -translate-y-1/2 translate-x-1/2"></div>
<div className="absolute bottom-0 left-0 w-1/4 h-1/4 bg-teal-50 rounded-full blur-3xl opacity-60 translate-y-1/2 -translate-x-1/4"></div>
<div className="container mx-auto px-6 relative z-10 text-center">
<h1 className="text-5xl md:text-7xl font-serif text-sky-900 mb-6 leading-tight">
Sacred Touch for the <br /> <span className="italic text-rose-400">Sacred Journey</span>
</h1>
<p className="text-lg md:text-xl text-sky-800/80 max-w-2xl mx-auto mb-10 font-sans leading-relaxed">
Nurturing, trauma-informed bodywork and doula care for pregnancy, postpartum, and the seasons of womanhood.
Inspired by Hawaiian healing traditions and the rhythms of the ocean.
</p>
<div className="flex flex-col md:flex-row justify-center items-center space-y-4 md:space-y-0 md:space-x-6">
<a
href="#contact"
className="px-10 py-4 bg-sky-600 text-white rounded-full text-lg font-medium hover:bg-sky-700 transition-all shadow-lg shadow-sky-200"
>
Book a Session
</a>
<a
href="#services"
className="text-sky-800 font-medium hover:text-rose-400 transition-colors flex items-center group"
>
Explore Services
<span className="ml-2 group-hover:translate-x-1 transition-transform"></span>
</a>
</div>
</div>
{/* Visual metaphor of water/waves */}
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-white to-transparent"></div>
</section>
);
};
export default Hero;

View File

@ -0,0 +1,78 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const Pricing = () => {
const [packages, setPackages] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchPackages = async () => {
try {
const response = await axios.get('/packages');
setPackages(response.data.rows || []);
} catch (error) {
console.error('Error fetching packages:', error);
} finally {
setLoading(false);
}
};
fetchPackages();
}, []);
if (loading) return null;
return (
<section id="pricing" className="py-24 bg-white">
<div className="container mx-auto px-6">
<div className="text-center max-w-2xl mx-auto mb-20">
<span className="text-rose-400 font-medium tracking-widest uppercase text-sm mb-4 block">Care Bundles</span>
<h2 className="text-4xl font-serif text-sky-900 mb-6">Packages for Continuity</h2>
<p className="text-sky-800/70">
Investing in ongoing support allows for deeper physiological release and
consistency in your healing journey.
</p>
</div>
<div className="grid md:grid-cols-2 gap-8 max-w-5xl mx-auto">
{packages.map((pkg: any) => (
<div
key={pkg.id}
className="group bg-stone-50 p-10 rounded-3xl border-2 border-transparent hover:border-sky-100 transition-all flex flex-col"
>
<h3 className="text-2xl font-serif text-sky-900 mb-2">{pkg.name}</h3>
<p className="text-sky-800/60 mb-8 text-sm uppercase tracking-widest">{pkg.included_sessions} Sessions Included</p>
<div className="mb-8 flex-grow">
<p className="text-sky-800/80 mb-6 italic">{pkg.description}</p>
<div className="space-y-3">
<p className="text-sm text-sky-900 font-medium flex items-start">
<span className="text-rose-400 mr-2"></span> {pkg.what_is_included}
</p>
<p className="text-sm text-sky-800/70 flex items-start">
<span className="text-rose-400 mr-2"></span> {pkg.ideal_for}
</p>
</div>
</div>
<div className="pt-8 border-t border-sky-100 flex items-baseline justify-between">
<div>
<span className="text-4xl font-serif text-sky-900 font-bold">${pkg.price}</span>
<span className="text-sky-800/50 ml-2 text-sm">/ total</span>
</div>
<a
href="#contact"
className="px-6 py-2 bg-white text-sky-900 border border-sky-900/10 rounded-full font-medium group-hover:bg-sky-600 group-hover:text-white transition-all"
>
Choose Package
</a>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default Pricing;

View File

@ -0,0 +1,62 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const Services = () => {
const [services, setServices] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchServices = async () => {
try {
const response = await axios.get('/services');
setServices(response.data.rows || []);
} catch (error) {
console.error('Error fetching services:', error);
} finally {
setLoading(false);
}
};
fetchServices();
}, []);
if (loading) return <section id="services" className="py-24 text-center">Loading services...</section>;
return (
<section id="services" className="py-24 bg-stone-50">
<div className="container mx-auto px-6">
<div className="text-center max-w-2xl mx-auto mb-20">
<span className="text-rose-400 font-medium tracking-widest uppercase text-sm mb-4 block">Holistic Offerings</span>
<h2 className="text-4xl font-serif text-sky-900 mb-6">Nurturing Bodywork & Support</h2>
<p className="text-sky-800/70">
Each session is uniquely adapted to your physical needs and emotional landscape,
prioritizing comfort, safety, and deep restoration.
</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{services.map((service: any) => (
<div
key={service.id}
className="bg-white p-8 rounded-2xl border border-sky-50 shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300"
>
<div className="w-12 h-12 bg-sky-50 rounded-full flex items-center justify-center mb-6">
<div className="w-6 h-6 bg-sky-200 rounded-full opacity-60"></div>
</div>
<h3 className="text-2xl font-serif text-sky-900 mb-4">{service.name}</h3>
<p className="text-sky-800/70 mb-6 line-clamp-3">
{service.short_description || service.full_description}
</p>
<div className="flex justify-between items-center pt-6 border-t border-stone-50">
<span className="text-sm font-medium text-sky-900">{service.default_duration_minutes} mins</span>
<span className="text-sky-900 font-bold">${service.base_price}</span>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default Services;

View File

@ -0,0 +1,65 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const Testimonials = () => {
const [testimonials, setTestimonials] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchTestimonials = async () => {
try {
const response = await axios.get('/testimonials');
setTestimonials((response.data.rows || []).filter((t: any) => t.is_published));
} catch (error) {
console.error('Error fetching testimonials:', error);
} finally {
setLoading(false);
}
};
fetchTestimonials();
}, []);
if (loading || testimonials.length === 0) return null;
return (
<section id="testimonials" className="py-24 bg-sky-900 text-white overflow-hidden relative">
{/* Abstract wave pattern */}
<div className="absolute top-0 left-0 w-full h-full opacity-5 pointer-events-none">
<svg viewBox="0 0 1440 320" className="absolute bottom-0 w-full">
<path fill="#ffffff" d="M0,160L48,176C96,192,192,224,288,213.3C384,203,480,149,576,144C672,139,768,181,864,202.7C960,224,1056,224,1152,197.3C1248,171,1344,117,1392,90.7L1440,64L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path>
</svg>
</div>
<div className="container mx-auto px-6 relative z-10">
<div className="text-center mb-16">
<span className="text-rose-300 font-medium tracking-widest uppercase text-sm mb-4 block">Shared Experiences</span>
<h2 className="text-4xl font-serif mb-4">Kind Words from Clients</h2>
</div>
<div className="flex flex-wrap justify-center gap-8">
{testimonials.map((t: any) => (
<div
key={t.id}
className="w-full md:w-[calc(50%-1rem)] lg:w-[calc(33.333%-1.5rem)] bg-white/10 backdrop-blur-sm p-10 rounded-3xl border border-white/10 flex flex-col"
>
<div className="flex text-rose-300 mb-6">
{[...Array(t.rating || 5)].map((_, i) => (
<span key={i} className="text-xl"></span>
))}
</div>
<p className="text-lg leading-relaxed mb-8 italic text-sky-50 flex-grow">
&ldquo;{t.quote}&rdquo;
</p>
<div>
<p className="font-serif text-xl font-bold">{t.client_name}</p>
<p className="text-sky-300 text-sm uppercase tracking-widest">{t.title}</p>
</div>
</div>
))}
</div>
</div>
</section>
);
};
export default Testimonials;

View File

@ -1,166 +1,39 @@
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import React, { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
export default function Starter() {
const [illustrationImage, setIllustrationImage] = useState({
src: undefined,
photographer: undefined,
photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('image');
const [contentPosition, setContentPosition] = useState('background');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'Salote Massage Portfolio'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
const imageBlock = (image) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
const videoBlock = (video) => {
if (video?.video_files?.length > 0) {
return (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
import Header from '../components/Landing/Header';
import Hero from '../components/Landing/Hero';
import About from '../components/Landing/About';
import Services from '../components/Landing/Services';
import Pricing from '../components/Landing/Pricing';
import Testimonials from '../components/Landing/Testimonials';
import Contact from '../components/Landing/Contact';
import Footer from '../components/Landing/Footer';
export default function Home() {
return (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<div className="min-h-screen bg-white">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>Salote Toilolo | Prenatal Massage & Doula Support</title>
<meta name="description" content="Nurturing, trauma-informed bodywork and doula care for pregnancy and postpartum in Honolulu, Oahu." />
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Salote Massage Portfolio app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
</CardBox>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<Header />
<main>
<Hero />
<About />
<Services />
<Pricing />
<Testimonials />
<Contact />
</main>
<Footer />
</div>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
Home.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
};