From d3caefad534c6f8b30cdd5dcdc6b793c81613bf4 Mon Sep 17 00:00:00 2001 From: Dimitri Ntempos Date: Sun, 5 Jan 2025 02:29:02 +0200 Subject: [PATCH] Add useBabyTransitions hook and utility functions for date validation and transitions --- README.md | 7 +- logo192.png => public/logo192.png | Bin logo512.png => public/logo512.png | Bin manifest.json => public/manifest.json | 0 src/App.css | 151 ++++++----- ...mbossed_texture.png => header_texture.png} | Bin src/components/AboutPage.js | 110 +++++--- src/components/BabyWeeksCalculator.js | 252 ++++++------------ src/hooks/useBabyTransitions.js | 72 +++++ src/index.css | 29 +- src/utils/utils.js | 100 +++++++ 11 files changed, 433 insertions(+), 288 deletions(-) rename logo192.png => public/logo192.png (100%) rename logo512.png => public/logo512.png (100%) rename manifest.json => public/manifest.json (100%) rename src/assets/{purple_background_embossed_texture.png => header_texture.png} (100%) create mode 100644 src/hooks/useBabyTransitions.js create mode 100644 src/utils/utils.js diff --git a/README.md b/README.md index d76b933..ec9f0b4 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ # Babyweeks -Babyweeks is a React app designed to help users track their baby's development milestones by calculating the number of weeks since a given birthdate and providing relevant insights. The idea started back in 2022 using PyScript and transitioned to React in early 2025. The app is hosted on GitHub Pages. +Babyweeks is a React app designed to help users track their baby's development milestones by calculating the number of weeks since a given birthdate. It started back in 2021 using PyScript and transitioned to React in early 2025. The app is hosted on GitHub Pages. -## Features +## What a user can do - Input a birthdate to calculate the weeks since birth. -- View developmental milestones content for specific week ranges. -- Navigate through transitions using a simple and intuitive interface. +- View developmental milestones content for a baby's first year. --- diff --git a/logo192.png b/public/logo192.png similarity index 100% rename from logo192.png rename to public/logo192.png diff --git a/logo512.png b/public/logo512.png similarity index 100% rename from logo512.png rename to public/logo512.png diff --git a/manifest.json b/public/manifest.json similarity index 100% rename from manifest.json rename to public/manifest.json diff --git a/src/App.css b/src/App.css index c2f7e49..94ffd33 100644 --- a/src/App.css +++ b/src/App.css @@ -1,124 +1,131 @@ .App { - align-items: center; display: flex; - flex-direction: column; - justify-content: center; - width: 100%; + flex-direction: column; /* Stack header, main, and footer vertically */ + min-height: 100vh; /* Full viewport height */ } + .App-logo { + width: 160px; height: 160px; - pointer-events: none; transition: transform var(--transition-speed) ease; - width: 160px; + pointer-events: none; } + .App-header { - align-items: center; - background-repeat: repeat; - background-size: 50px 50px; /* Adjust the size of the repeated tiles */ - box-shadow: 0 4px 12px var(--shadow-light); - color: var(--text-light); display: flex; flex-direction: column; + align-items: center; width: 100%; - - /* Apply the gradient first, then the baby texture */ - background-image: - url('./assets/baby_texture.png'), /* Texture image */ - linear-gradient(to bottom right, rgba(0, 0, 0, 0.4), rgba(255, 0, 255, 0.5)); /* Gradient */ + background-image: + url('./assets/baby_texture.png'), /* Texture image */ + linear-gradient(to bottom right, rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.9)); /* Light gradient */ + background-repeat: repeat; + background-size: 50px 50px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + color: var(--text-light); } - .container { + width: 100%; max-width: 1200px; padding: 0 1rem; text-align: center; - width: 100%; -} -.App-link { - color: var(--primary-color); - font-weight: 500; - text-decoration: none; - transition: color var(--transition-speed) ease; -} -.App-link:hover { - color: #8a80ff; -} -@keyframes App-logo-spin { - 0% { - transform: rotate(0deg); - } - to { - transform: rotate(1turn); - } } + .calculator { - align-items: center; - /* background-color: var(--light-bg-color); */ - border-radius: var(--border-radius); - /* box-shadow: 0 4px 8px var(--shadow-light); */ display: flex; flex-direction: column; - gap: rem; + align-items: center; + gap: 1rem; max-width: 500px; padding: 1rem; - transition: background-color var(--transition-speed); width: 100%; } + input[type="date"] { + width: 100%; + max-width: 300px; + padding: 0.75rem 1rem; + font-size: 1rem; + color: var(--text-color); background-color: var(--light-bg-color); - border: 10px solid #ddd; + border: 2px solid #ddd; border-radius: var(--border-radius); - box-shadow: 0 2px 5px var(--shadow-light); - color: var(--text-color); - font-size: 1rem; - max-width: 300px; - padding: 0.75rem 3rem 0.75rem 1rem; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); transition: all var(--transition-speed) ease; - width: 100%; } + input[type="date"]:focus { background-color: #fff; border-color: var(--primary-color); - box-shadow: 0 4px 8px var(--shadow-dark); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } + input[type="date"]:hover { border-color: #bbb; } + input[type="date"]::placeholder { color: #aaa; } -.input-wrapper { - margin: auto; - max-width: 300px; + +.message-slider { + width: 100%; + max-width: 600px; + overflow: hidden; position: relative; + margin: 0 auto; + padding-bottom: 30px; /* Space for dots */ } -.input-wrapper .icon { - color: #bbb; - pointer-events: none; + +.slick-dots { position: absolute; - right: 1rem; - top: 50%; - transform: translateY(-50%); + bottom: 10px; + left: 0; + right: 0; + display: flex !important; + justify-content: center; + list-style: none; + margin: 0; + padding: 0; +} + +.slick-dots li button:before { + font-size: 0.75rem; + color: var(--primary-color); + opacity: 0.5; } -.navigation .btn { - margin: 0 5px; + +.slick-dots li.slick-active button:before { + opacity: 1; } + .message { - border-radius: var(--border-radius); - box-shadow: 0 2px 6px var(--shadow-light); - color: var(--text-color); - font-size: 1.1em; - line-height: 1.5; - margin: 20px auto 0; + margin: 0 auto; max-width: 80%; - height: 400px; /* Set a fixed height */ - overflow: hidden; /* Ensures that long text doesn't break the layout */ - opacity: 0; - padding: 15px 20px; + padding: 15px 20px; /* Adjusted for original typography */ + font-size: 1.1rem; /* Restored original typography */ + line-height: 1.5; /* Restored original line-height */ + color: var(--text-color); + background-color: #fff; /* Bright white background */ + /* border: 1px solid rgba(0, 0, 0, 0.1); Subtle border */ + border-radius: var(--border-radius); /* Smooth corners */ + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1); /* Soft shadow for modern look */ text-align: center; - transition: opacity 0.5s ease-in-out; + min-height: 150px; /* Prevent collapse for short messages */ + transition: box-shadow 0.3s ease, transform 0.3s ease; /* Add hover animation */ } .message.show { - opacity: 1; + opacity: 1; /* Animations for Message Visibility */ } + +footer { + width: 100%; + /* text-align: center; */ + font-size: 0.9rem; + /* color: var(--text-color); */ + /* background-color: var(--light-bg-color); */ + padding: 1rem 0; + box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1); +} \ No newline at end of file diff --git a/src/assets/purple_background_embossed_texture.png b/src/assets/header_texture.png similarity index 100% rename from src/assets/purple_background_embossed_texture.png rename to src/assets/header_texture.png diff --git a/src/components/AboutPage.js b/src/components/AboutPage.js index 7aa3e7b..6e6f218 100644 --- a/src/components/AboutPage.js +++ b/src/components/AboutPage.js @@ -1,50 +1,78 @@ -// src/pages/About.js import React from 'react'; -import '../App.css'; import babyImage from '../assets/baby-girl.svg'; +import headerImage from '../assets/baby_texture.png'; -const AboutPage = () => { - return ( -
-
-
- Illustration of a baby girl -

About Growth Leaps

-

- Why construction built this app. -

-
- - - -
- -
-

For the past couple of years, we’ve been experiencing parenthood. During our kids' first year, their pediatrician linked unexpected mood changes to transitions between growth cycle stages. While these weeks vary by child and are more empirical than scientific, the app helps my partner quickly calculate the current week of our kids' growth cycles, saving the need to count manually in her calendar.

-

Who can use the app

-

Anyone! The app is free, open-source and runs on your browser. Meaning it doesn't store any personal or sensitive data such as emails and your baby's birthdate so that you are targeted with ads later on. The app uses essential cookies to track information like location, browser and button clicks exploited by the creator to optimize the app's behavior and functionality. Don't want to share any information? Here is how to opt-out.

-

Upcoming improvements

+function preloadImage(url) { + const img = new Image(); + img.src = url; +} -

Babyweeks is currently in Beta version, might be slow and/or break. If any of those happen, keep calm and support me by clicking the green button on the lower right corner. I’m still working on it.

-
    -
  • Translate into more languages
  • -
  • Add year 1+ transitions content
  • -
  • Optimize for small screens
  • -
- -

Have an idea? Send me an mail_outline

-
+preloadImage(babyImage); +preloadImage(headerImage); +const AboutPage = () => { + return ( +
+
+
+ Baby +

Growth Leaps

+

Why I built this app.{' '} + + construction + {' '} + +

+
+ + + +
+
+ +

+ During a child’s first year, unexpected short-term mood changes, without other medical signs, may be linked to transitions between growth cycle stages. These periods, often referred to as Growth Leaps, vary from child to child and are rather based on observation than in scientific evidence. Understanding transitions can help parents navigate their child's infancy with greater patience. +

+

How it works

+

+ The app helped my partner quickly calculate the current week of our kids' growth cycles, + saving the need to count manually in the calendar. The app is free,{' '} + + open-source + {' '} + and runs on your browser. +

+ +

Upcoming improvements

+
    +
  • Translate into more languages
  • +
  • Add year 1+ transitions content
  • +
+

+ Have an idea? Send me an{' '} + + email + + . +

+
-
- ); +
+

© 2025 · Crafted with ❤️ by ntemposd

+
+
+ ); }; export default AboutPage; diff --git a/src/components/BabyWeeksCalculator.js b/src/components/BabyWeeksCalculator.js index cfc8994..c9ee3f0 100644 --- a/src/components/BabyWeeksCalculator.js +++ b/src/components/BabyWeeksCalculator.js @@ -1,201 +1,113 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useRef } from 'react'; +import { Link } from 'react-router-dom'; // Import the Link component import '../App.css'; import babyImage from '../assets/baby-girl.svg'; +import headerImage from '../assets/baby_texture.png'; import transitionsData from '../assets/transitions.json'; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; +import Slider from "react-slick"; +import "slick-carousel/slick/slick.css"; +import "slick-carousel/slick/slick-theme.css"; + +// Import the custom hook +import { useBabyTransitions } from '../hooks/useBabyTransitions'; +import { formatMessage } from '../utils/utils'; function preloadImage(url) { const img = new Image(); img.src = url; } -preloadImage(babyImage); // Preload the image +preloadImage(babyImage); +preloadImage(headerImage); function BabyWeeksCalculator() { - const [startDate, setStartDate] = useState(new Date()); - const [weekDifference, setWeekDifference] = useState(null); - const [currentMessageIndex, setCurrentMessageIndex] = useState(null); - const [displayedMessage, setDisplayedMessage] = useState(''); - const [allTransitions, setAllTransitions] = useState([]); - - // Function to calculate weeks since the selected date - function calculateWeeks(selectedDate) { - if (!selectedDate) return; // Safety check - const today = new Date(); - const differenceInTime = today - selectedDate; - const weeks = Math.floor(differenceInTime / (1000 * 60 * 60 * 24 * 7)); - - // Check for future dates (baby not born yet) - if (differenceInTime < 0) { - setWeekDifference(null); // Clear any previous value - setDisplayedMessage("Your baby is not born yet! Please select a valid date."); - setAllTransitions([]); // Clear transitions - setCurrentMessageIndex(null); - return; - } - - // Check if the baby is older than 1 year - if (weeks > 52) { - setWeekDifference(null); // Clear any previous value - setDisplayedMessage( - "Your baby is over a year old! This app focuses on development during the first year." - ); - setAllTransitions([]); // Clear transitions - setCurrentMessageIndex(null); - return; - } - - // If the date is valid, proceed with the calculation - setWeekDifference(weeks); - - console.log("Weeks:", weeks); // Log the calculated weeks - - // Find the specific leap or interval - let currentTransition = null; - let genericMessage = null; - - const foundTransition = transitionsData.transitions.find(({ minWeeks, maxWeeks }) => - weeks >= minWeeks && weeks <= maxWeeks - ); - - if (foundTransition) { - currentTransition = foundTransition; - } else { - // Handle intervals between leaps - const previousLeap = transitionsData.transitions - .filter(({ maxWeeks }) => weeks > maxWeeks) - .sort((a, b) => b.maxWeeks - a.maxWeeks)[0]; // Find the last completed leap - - const nextLeap = transitionsData.transitions - .filter(({ minWeeks }) => weeks < minWeeks) - .sort((a, b) => a.minWeeks - b.minWeeks)[0]; // Find the upcoming leap - - if (previousLeap && nextLeap) { - genericMessage = `Your baby is growing steadily. Stay tuned for the next leap at week ${nextLeap.minWeeks}!`; - } else if (!previousLeap && nextLeap) { - // If no previous leap (baby is too young) - genericMessage = `Your baby is growing steadily. The first leap starts at week ${nextLeap.minWeeks}.`; - } else if (previousLeap && !nextLeap) { - // If no next leap (baby is older than last leap) - genericMessage = "Your baby has completed all leaps! Celebrate their growth!"; - } - } - - // Prepare the ordered list of transitions for navigation - const orderedTransitions = [...transitionsData.transitions].sort( - (a, b) => a.minWeeks - b.minWeeks - ); - - setAllTransitions(orderedTransitions); - - // Set the default displayed message - if (currentTransition) { - setCurrentMessageIndex(orderedTransitions.indexOf(currentTransition)); - } else { - setCurrentMessageIndex(null); // No specific leap - setDisplayedMessage(genericMessage); - } - - console.log("Ordered transitions:", orderedTransitions); - } - - // Function to format and display transition message - function formatMessage(transition) { - return ( - <> -
{transition.title}
{/* Smaller Title */} -

- Weeks: {transition.minWeeks}-{transition.maxWeeks} -

{/* Smaller Weeks */} -

{transition.description}

{/* Regular Description */} - - ); - } - - // Navigate to the next or previous message - function navigateMessage(direction) { - console.log("Navigating... Current Index:", currentMessageIndex); - - if (!allTransitions || allTransitions.length === 0) return; - - setCurrentMessageIndex((prevIndex) => { - let newIndex = prevIndex + direction; - - if (newIndex < 0) newIndex = allTransitions.length - 1; // Wrap to the last message - if (newIndex >= allTransitions.length) newIndex = 0; // Wrap to the first message - - console.log("New Index after navigation:", newIndex); - return newIndex; - }); - } - - // Effect to update the displayed message when the currentMessageIndex changes - useEffect(() => { - if (currentMessageIndex !== null && allTransitions.length > 0) { - setDisplayedMessage(formatMessage(allTransitions[currentMessageIndex])); - } - }, [currentMessageIndex, allTransitions]); // Only re-run when the index or matching transitions change + const [startDate, setStartDate] = useState(null); // Default to null + const sliderRef = useRef(null); + + const { + weekDifference, + currentMessageIndex, + displayedMessage, + allTransitions, + setCurrentMessageIndex, + } = useBabyTransitions(startDate, transitionsData); + + // Show slider if valid transitions exist + const showSlider = + startDate && + weekDifference !== null && + allTransitions.length > 0 && + currentMessageIndex !== null; return (
- Baby + Baby

Growth Leaps

-
Testing GH Actions
-

tips_and_updates Explore Your Infant's Development tips_and_updates

+

Explore Your Infant's Development info +

- {/* Add the SVG line here */}
-
- - south -
- { - setStartDate(date); - calculateWeeks(date); // Call calculateWeeks whenever the date is selected - }} - className="my-2 form-control text-center lead" - dateFormat="yyyy-MM-dd" - /> -
- - {/* Always show buttons */} - {allTransitions.length > 0 && weekDifference !== null && ( -
- - +
+
+ +
+
- )} -
- {displayedMessage} + + {/* Show fallback message only when no valid transitions exist */} + {!showSlider && displayedMessage && ( +
+ {displayedMessage} +
+ )} + + {/* Render slider when transitions exist */} + {showSlider && ( +
+ { + console.log("Slider Changed to Index:", index); + setCurrentMessageIndex(index); // Update index + }} + > + {allTransitions.map((transition, index) => ( +
+ {formatMessage(transition)} +
+ ))} +
+
+ )}
-
+
+ + {/* Add a link to the About Page */} +
+

© 2025 · Crafted with ❤️ by ntemposd

+
+ ); } diff --git a/src/hooks/useBabyTransitions.js b/src/hooks/useBabyTransitions.js new file mode 100644 index 0000000..91285d8 --- /dev/null +++ b/src/hooks/useBabyTransitions.js @@ -0,0 +1,72 @@ +import { useState, useEffect } from "react"; +import { validateSelectedDate, findTransitions, formatMessage } from "../utils/utils"; + +export function useBabyTransitions(selectedDate, transitionsData) { + const [weekDifference, setWeekDifference] = useState(null); + const [currentMessageIndex, setCurrentMessageIndex] = useState(null); + const [displayedMessage, setDisplayedMessage] = useState(''); + const [allTransitions, setAllTransitions] = useState([]); + const [lastSelectedDate, setLastSelectedDate] = useState(null); + + useEffect(() => { + if (!selectedDate) { + if (lastSelectedDate !== null) { + console.log("No date selected. Resetting state."); + } + + setLastSelectedDate(null); + setWeekDifference(null); + setDisplayedMessage(''); // Reset fallback message + setAllTransitions([]); + setCurrentMessageIndex(null); + return; + } + + if (selectedDate === lastSelectedDate) { + return; + } + + setLastSelectedDate(selectedDate); + const today = new Date(); + + const { isValid, weeks, message } = validateSelectedDate(selectedDate, today); + + if (!isValid) { + console.log("Validation failed:", message); + setWeekDifference(null); + setDisplayedMessage(message); // Show fallback message for invalid dates + setAllTransitions([]); + setCurrentMessageIndex(null); + return; + } + + console.log("Validation succeeded. Weeks:", weeks); + setWeekDifference(weeks); + + const { foundTransition, orderedTransitions, fallbackMessage } = findTransitions( + weeks, + transitionsData.transitions, + selectedDate + ); + + setAllTransitions(orderedTransitions); + + if (foundTransition) { + console.log("Found Transition:", foundTransition); + setCurrentMessageIndex(orderedTransitions.indexOf(foundTransition)); + setDisplayedMessage(''); // Clear fallback message when transition exists + } else { + console.log("No Transition. Fallback Message:", fallbackMessage); + setCurrentMessageIndex(null); + setDisplayedMessage(fallbackMessage); // Show fallback message + } + }, [selectedDate, transitionsData, lastSelectedDate]); + + return { + weekDifference, + currentMessageIndex, + displayedMessage, + allTransitions, + setCurrentMessageIndex, + }; +} diff --git a/src/index.css b/src/index.css index e7e80da..393e01b 100644 --- a/src/index.css +++ b/src/index.css @@ -15,9 +15,36 @@ body { --border-radius: 8px; --transition-speed: 0.3s; } + +header { + width: 100%; /* Ensure header spans the full width */ + text-align: center; /* Center text inside the header */ +} + +main { + flex: 1; /* Allow main content to grow and push the footer down */ + display: flex; + flex-direction: column; + align-items: center; /* Center main content horizontally */ + justify-content: flex-start; /* Align content to the top of the main area */ + /* padding-top: 2rem; Add spacing below the header */ + width: 100%; +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, Courier New, monospace; } + +/* General Layout */ +a { + color: var(--text-light); + text-decoration: underline; +} + +a:hover { + color: initial; +} + .material-icons, .material-icons-outlined { -webkit-font-smoothing: antialiased; @@ -33,4 +60,4 @@ code { text-rendering: optimizeLegibility; vertical-align: top; /* white-space: nowrap; */ -} +} \ No newline at end of file diff --git a/src/utils/utils.js b/src/utils/utils.js new file mode 100644 index 0000000..f94b073 --- /dev/null +++ b/src/utils/utils.js @@ -0,0 +1,100 @@ +// Function to calculate weeks and time difference +export function calculateWeeks(selectedDate, today = new Date()) { + const differenceInTime = today - selectedDate; + const weeks = Math.floor(differenceInTime / (1000 * 60 * 60 * 24 * 7)); + return { weeks, differenceInTime }; + } + + // Function to validate selected date and handle edge cases + export function validateSelectedDate(selectedDate, today = new Date()) { + const { weeks, differenceInTime } = calculateWeeks(selectedDate, today); + + if (differenceInTime < 0) { + console.log("Validation failed: Baby is not born yet!"); + return { + isValid: false, + message: "Your baby is not born yet! Please select a valid date.", + }; + } + + if (weeks > 52) { + console.log("Validation failed: Baby is over a year old!"); + return { + isValid: false, + message: "Your baby is over a year old! This app focuses on development during the first year.", + }; + } + + return { isValid: true, weeks }; + } + + // Function to find transitions and generate fallback messages + export function findTransitions(weeks, transitionsData, selectedDate) { + const orderedTransitions = [...transitionsData].sort((a, b) => a.minWeeks - b.minWeeks); + + // Find the current transition + const foundTransition = orderedTransitions.find( + ({ minWeeks, maxWeeks }) => weeks >= minWeeks && weeks <= maxWeeks + ); + + if (foundTransition) { + console.log("Found Transition:", foundTransition); + return { + foundTransition, + orderedTransitions, + fallbackMessage: '', // No fallback needed + nextLeap: null, + previousLeap: null, + }; + } + + // Determine next and previous leaps + const previousLeap = orderedTransitions + .filter(({ maxWeeks }) => weeks > maxWeeks) + .sort((a, b) => b.maxWeeks - a.maxWeeks)[0]; + + const nextLeap = orderedTransitions + .filter(({ minWeeks }) => weeks < minWeeks) + .sort((a, b) => a.minWeeks - b.minWeeks)[0]; + + // Generate fallback message + const fallbackMessage = generateFallbackMessage(weeks, previousLeap, nextLeap, selectedDate); + + console.log("No Transition. Fallback Message:", fallbackMessage); + return { foundTransition: null, orderedTransitions, fallbackMessage, nextLeap, previousLeap }; + } + + // Function to generate fallback message for "no transition" periods + export function generateFallbackMessage(weeks, previousLeap, nextLeap, selectedDate) { + if (nextLeap) { + const nextLeapStartDate = new Date(selectedDate.getTime() + nextLeap.minWeeks * 7 * 24 * 60 * 60 * 1000); + const daysUntilNextLeap = Math.ceil((nextLeapStartDate - new Date()) / (1000 * 60 * 60 * 24)); + return `You are all set, your baby is growing but no transition happens right now. The next leap starts at week ${nextLeap.minWeeks}, ${daysUntilNextLeap} days from today.`; + } + + if (previousLeap) { + return "Your baby has completed all leaps! Celebrate their growth!"; + } + + return "You are all set, your baby is growing but no transition happens right now."; + } + + // Function to format transition message + export function formatMessage(transition) { + if (!transition) { + console.error("Invalid Transition Passed to formatMessage"); + return null; + } + + console.log("Formatting Transition Message:", transition); + return ( + <> +
{transition.title}
+

+ Weeks: {transition.minWeeks}-{transition.maxWeeks} +

+

{transition.description}

+ + ); + } + \ No newline at end of file