diff --git a/backend/src/db/migrations/20260206045615-grant-public-permissions.js b/backend/src/db/migrations/20260206045615-grant-public-permissions.js
new file mode 100644
index 0000000..55b1f84
--- /dev/null
+++ b/backend/src/db/migrations/20260206045615-grant-public-permissions.js
@@ -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}';`
+ );
+ }
+ }
+};
\ No newline at end of file
diff --git a/backend/src/index.js b/backend/src/index.js
index d0ee0e4..9eedc20 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -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;
\ No newline at end of file
diff --git a/frontend/src/components/Landing/About.tsx b/frontend/src/components/Landing/About.tsx
new file mode 100644
index 0000000..77996d4
--- /dev/null
+++ b/frontend/src/components/Landing/About.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+
+const About = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Meet Salote
+
+ A steady presence for transformative times.
+
+
+
+ Salote Toilolo is a Hawaii-based licensed massage therapist and certified doula offering nurturing, trauma-informed bodywork for pregnancy, postpartum, and women's wellness.
+
+
+ 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.
+
+
+ Salote'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.
+
+
+
+
+
+
Licensed LMT
+
HI-LMT-48291
+
+
+
Certified Doula
+
Birth & Postpartum
+
+
+
+
+
+
+ );
+};
+
+export default About;
\ No newline at end of file
diff --git a/frontend/src/components/Landing/Contact.tsx b/frontend/src/components/Landing/Contact.tsx
new file mode 100644
index 0000000..6a53bbc
--- /dev/null
+++ b/frontend/src/components/Landing/Contact.tsx
@@ -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 (
+
+ );
+};
+
+export default Contact;
\ No newline at end of file
diff --git a/frontend/src/components/Landing/Footer.tsx b/frontend/src/components/Landing/Footer.tsx
new file mode 100644
index 0000000..d4ee530
--- /dev/null
+++ b/frontend/src/components/Landing/Footer.tsx
@@ -0,0 +1,30 @@
+
+import React from 'react';
+
+const Footer = () => {
+ return (
+
+
+
+
+
Salote Toilolo
+
Nurturing Touch & Maternal Support
+
+
+
+
+
+ © 2026 Salote Toilolo. All rights reserved.
+
+
+
+
+ );
+};
+
+export default Footer;
diff --git a/frontend/src/components/Landing/Header.tsx b/frontend/src/components/Landing/Header.tsx
new file mode 100644
index 0000000..71093b6
--- /dev/null
+++ b/frontend/src/components/Landing/Header.tsx
@@ -0,0 +1,24 @@
+
+import React from 'react';
+import Link from 'next/link';
+
+const Header = () => {
+ return (
+
+ );
+};
+
+export default Header;
diff --git a/frontend/src/components/Landing/Hero.tsx b/frontend/src/components/Landing/Hero.tsx
new file mode 100644
index 0000000..1f160ba
--- /dev/null
+++ b/frontend/src/components/Landing/Hero.tsx
@@ -0,0 +1,42 @@
+
+import React from 'react';
+
+const Hero = () => {
+ return (
+
+ {/* Background Decorative Elements */}
+
+
+
+
+
+ Sacred Touch for the Sacred Journey
+
+
+ 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.
+
+
+
+
+ {/* Visual metaphor of water/waves */}
+
+
+ );
+};
+
+export default Hero;
diff --git a/frontend/src/components/Landing/Pricing.tsx b/frontend/src/components/Landing/Pricing.tsx
new file mode 100644
index 0000000..ce012cc
--- /dev/null
+++ b/frontend/src/components/Landing/Pricing.tsx
@@ -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 (
+
+
+
+
Care Bundles
+
Packages for Continuity
+
+ Investing in ongoing support allows for deeper physiological release and
+ consistency in your healing journey.
+
+
+
+
+ {packages.map((pkg: any) => (
+
+
{pkg.name}
+
{pkg.included_sessions} Sessions Included
+
+
+
{pkg.description}
+
+
+ ✓ {pkg.what_is_included}
+
+
+ ✓ {pkg.ideal_for}
+
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+export default Pricing;
diff --git a/frontend/src/components/Landing/Services.tsx b/frontend/src/components/Landing/Services.tsx
new file mode 100644
index 0000000..30621a4
--- /dev/null
+++ b/frontend/src/components/Landing/Services.tsx
@@ -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 ;
+
+ return (
+
+
+
+
Holistic Offerings
+
Nurturing Bodywork & Support
+
+ Each session is uniquely adapted to your physical needs and emotional landscape,
+ prioritizing comfort, safety, and deep restoration.
+
+
+
+
+ {services.map((service: any) => (
+
+
+
{service.name}
+
+ {service.short_description || service.full_description}
+
+
+ {service.default_duration_minutes} mins
+ ${service.base_price}
+
+
+ ))}
+
+
+
+ );
+};
+
+export default Services;
diff --git a/frontend/src/components/Landing/Testimonials.tsx b/frontend/src/components/Landing/Testimonials.tsx
new file mode 100644
index 0000000..c9683e8
--- /dev/null
+++ b/frontend/src/components/Landing/Testimonials.tsx
@@ -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 (
+
+ {/* Abstract wave pattern */}
+
+
+
+
+ Shared Experiences
+
Kind Words from Clients
+
+
+
+ {testimonials.map((t: any) => (
+
+
+ {[...Array(t.rating || 5)].map((_, i) => (
+ ★
+ ))}
+
+
+ “{t.quote}”
+
+
+
{t.client_name}
+
{t.title}
+
+
+ ))}
+
+
+
+ );
+};
+
+export default Testimonials;
\ No newline at end of file
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index d031f12..fa7f8f9 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -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) => (
-
- );
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
- };
+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 (
-
+
-
{getPageTitle('Starter Page')}
+
Salote Toilolo | Prenatal Massage & Doula Support
+
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
-
-
+
+
+
+
+
+
+
+
+
+
+
);
}
-Starter.getLayout = function getLayout(page: ReactElement) {
+Home.getLayout = function getLayout(page: ReactElement) {
return
{page} ;
-};
-
+};
\ No newline at end of file