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 ( +
+
+
+
+
+
+ Salote Toilolo +
+
+
+
+
+ +
+ 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 ( +
+
+
+
+ Get in Touch +

Begin Your Journey

+

+ Ready to book a session or have questions about how I can support you? + Fill out the form, and I'll get back to you within 24-48 hours. +

+ +
+
+
+ ✉ +
+
+

Email

+

salote@salotemassage.com

+
+
+
+
+ ☏ +
+
+

Phone

+

+1 (808) 555-0142

+
+
+
+
+ 📍 +
+
+

Location

+

Honolulu, Oahu

+
+
+
+
+ +
+
+ {status.message && ( +
+ {status.message} +
+ )} + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+
+
+
+
+
+ ); +}; + +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 ( + + ); +}; + +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 ( +
+
+ + Salote Toilolo + + +
+
+ ); +}; + +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. +

+
+ + Book a Session + + + Explore Services + + +
+
+ + {/* 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} +

+
+
+ +
+
+ ${pkg.price} + / total +
+ + Choose Package + +
+
+ ))} +
+
+
+ ); +}; + +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
Loading services...
; + + 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) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
-
- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; +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} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
-
-
-
-
-

© 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