From f031618cf5611354a1d292248707252560a6fe52 Mon Sep 17 00:00:00 2001 From: aniket Date: Tue, 10 Feb 2026 20:35:03 +0530 Subject: [PATCH 1/3] fix: testimonials carousel swipe navigation stuck on mobile (#1667) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add touchEventsTarget='container' to Swiper so touch listeners attach to container instead of wrapper - Add grabCursor for visual drag feedback on desktop - Fix className bug: home && 'h-32' → home ? 'h-32' : '' to prevent 'false' string in DOM - Add touchAction: 'pan-y' on blockquote to allow horizontal swipes to pass through to Swiper Closes #1667 --- src/common/Testimonial/TestimonialCard.jsx | 5 ++++- src/common/Testimonial/TestimonialSection.jsx | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/common/Testimonial/TestimonialCard.jsx b/src/common/Testimonial/TestimonialCard.jsx index 099961a016..c2e0c29ce0 100644 --- a/src/common/Testimonial/TestimonialCard.jsx +++ b/src/common/Testimonial/TestimonialCard.jsx @@ -53,7 +53,10 @@ const TestimonialCard = ({ home, quote, name, avatarUrl, category, created_at, e
-
+

{testimonials && testimonials.map((testimonial) => ( From 845da22f31e723624352780c2f8b04939304e8b8 Mon Sep 17 00:00:00 2001 From: aniket Date: Wed, 11 Feb 2026 15:06:53 +0530 Subject: [PATCH 2/3] fix: mobile drawer, navbar layout, testimonials error handling, leaderboard UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix mobile drawer not closing on outside click (useRef + click-outside listener) - Fix CSS transform creating containing block issue (translateY(0) → none) - Add global padding-top for fixed navbar offset (64px, excluding footer) - Add extra home hero padding for activity banner - Add try/catch error handling in testimonial fetches - Fix ESLint jsx-sort-props (shorthand booleans first) - Center leaderboard loading spinner - Remove overflow-hidden from leaderboard page - Import leaderBoard.css in LeaderBoard component --- package.json | 2 +- src/common/Testimonial/TestimonialSection.jsx | 11 ++++++--- src/common/Testimonial/Testimonials.jsx | 9 ++++++-- src/common/header/HeaderNav.jsx | 23 ++++++++++++++++++- src/common/header/header.css | 7 +++++- src/common/home/home.css | 1 + src/common/playleaderboard/LeaderBoard.jsx | 3 ++- src/common/playleaderboard/leaderBoard.css | 7 ++++++ 8 files changed, 54 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index b93fd03b76..3452393ee8 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,6 @@ "react-snap": "^1.23.0", "tailwind-scrollbar": "^2.1.0", "tailwindcss": "^3.4.1", - "typescript": "^5.3.3" + "typescript": "^5.9.3" } } diff --git a/src/common/Testimonial/TestimonialSection.jsx b/src/common/Testimonial/TestimonialSection.jsx index 5404f01419..6740015d6e 100644 --- a/src/common/Testimonial/TestimonialSection.jsx +++ b/src/common/Testimonial/TestimonialSection.jsx @@ -18,8 +18,13 @@ function TestimonialSection() { const [testimonials, setTestimonials] = useState([]); const fetchTestimonials = async () => { - const res = await submit(fetchTestimonialsHomePage()); - setTestimonials(res); + try { + const res = await submit(fetchTestimonialsHomePage()); + setTestimonials(res || []); + } catch (error) { + console.warn('Failed to fetch testimonials:', error.message); + setTestimonials([]); + } }; useEffect(() => { @@ -30,8 +35,8 @@ function TestimonialSection() { <>

{ const isAuthenticated = useAuthenticated(); const fetchTestimonials = async () => { - const res = await submit(fetchAllTestimonials()); - setTestimonials(res); + try { + const res = await submit(fetchAllTestimonials()); + setTestimonials(res || []); + } catch (error) { + console.warn('Failed to fetch testimonials:', error.message); + setTestimonials([]); + } }; const handleLogin = (value) => { diff --git a/src/common/header/HeaderNav.jsx b/src/common/header/HeaderNav.jsx index 3952f8af71..dadcf16a68 100644 --- a/src/common/header/HeaderNav.jsx +++ b/src/common/header/HeaderNav.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Link } from 'react-router-dom'; import { BsGithub, BsTrophyFill } from 'react-icons/bs'; import { FaLightbulb } from 'react-icons/fa'; @@ -16,6 +16,26 @@ const HeaderNav = ({ showBrowse }) => { const { showShareModal, setShowShareModal } = useSearchContext(); const [showToggleMenu, setShowToggleMenu] = useState(false); + const menuRef = useRef(null); + + // Close drawer when clicking outside the menu panel + useEffect(() => { + if (!showToggleMenu) return; + + const handleClickOutside = (event) => { + if (menuRef.current && !menuRef.current.contains(event.target)) { + setShowToggleMenu(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('touchstart', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('touchstart', handleClickOutside); + }; + }, [showToggleMenu]); const [anchorEl, setAnchorEl] = useState(null); @@ -129,6 +149,7 @@ const HeaderNav = ({ showBrowse }) => {
    e.stopPropagation()} >
  • diff --git a/src/common/header/header.css b/src/common/header/header.css index 75f9a7edba..af27735658 100644 --- a/src/common/header/header.css +++ b/src/common/header/header.css @@ -29,7 +29,12 @@ } .nav--visible { - transform: translateY(0); + transform: none; +} + +/* Push page content below the fixed header, but not the footer */ +.nav-wrapper ~ *:not(footer) { + padding-top: 64px; } diff --git a/src/common/home/home.css b/src/common/home/home.css index 359d036884..3a12a3df95 100644 --- a/src/common/home/home.css +++ b/src/common/home/home.css @@ -13,6 +13,7 @@ width: 100%; overflow-x: hidden; min-height: 100vh; + padding-top: 32px; /* extra offset for activity banner on home page */ } .app-home-body .app-home-body-content { diff --git a/src/common/playleaderboard/LeaderBoard.jsx b/src/common/playleaderboard/LeaderBoard.jsx index 32856f0706..9c38583321 100644 --- a/src/common/playleaderboard/LeaderBoard.jsx +++ b/src/common/playleaderboard/LeaderBoard.jsx @@ -6,6 +6,7 @@ import TopPlayCreators from './TopPlayCreators'; import { Watch } from 'react-loader-spinner'; import { groupBy } from 'lodash'; import { format, lastDayOfMonth } from 'date-fns'; +import './leaderBoard.css'; const LeaderBoard = () => { const [top10Contributors, updateTop10Contributors] = useState([]); @@ -69,7 +70,7 @@ const LeaderBoard = () => { }, [publishedPlays]); return ( -
    +
    {publishedPlays.length && (topContributorOfTheMonth || top10Contributors) ? (
    {topContributorOfTheMonth && ( diff --git a/src/common/playleaderboard/leaderBoard.css b/src/common/playleaderboard/leaderBoard.css index bab6f4799f..3342155873 100644 --- a/src/common/playleaderboard/leaderBoard.css +++ b/src/common/playleaderboard/leaderBoard.css @@ -38,6 +38,13 @@ height: 100vh; } */ +.leaderboard-loader { + display: flex; + align-items: center; + justify-content: center; + min-height: calc(100vh - 160px); +} + .leaderboard-wrapper { background-color: #ffffff; border-radius: 16px; From 5b10188ce88fca0dee740f558ea3f0928c716217 Mon Sep 17 00:00:00 2001 From: aniket Date: Sun, 15 Feb 2026 23:37:26 +0530 Subject: [PATCH 3/3] fix: sanitize invalid export aliases in plays/index.js before build Adds a sanitize-play-exports script that converts invalid JavaScript identifiers (e.g. Bmr-TdeeCalculator) to valid PascalCase identifiers. The script runs automatically as part of start, start:nolint, and build commands, fixing the SyntaxError that caused Netlify deploy failures. --- package.json | 6 +-- scripts/sanitize-play-exports.cjs | 86 +++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 scripts/sanitize-play-exports.cjs diff --git a/package.json b/package.json index 3452393ee8..e893799dc9 100644 --- a/package.json +++ b/package.json @@ -86,9 +86,9 @@ }, "scripts": { "dev": "react-scripts start", - "start:nolint": "npx --yes create-react-play@latest -p && react-scripts start", - "start": "npx --yes create-react-play@latest -p && npm run lint && react-scripts start", - "build": "npx --yes create-react-play@latest -p && react-scripts build", + "start:nolint": "npx --yes create-react-play@latest -p && node scripts/sanitize-play-exports.cjs && react-scripts start", + "start": "npx --yes create-react-play@latest -p && node scripts/sanitize-play-exports.cjs && npm run lint && react-scripts start", + "build": "npx --yes create-react-play@latest -p && node scripts/sanitize-play-exports.cjs && react-scripts build", "snap": "react-snap", "test": "react-scripts test", "eject": "react-scripts eject", diff --git a/scripts/sanitize-play-exports.cjs b/scripts/sanitize-play-exports.cjs new file mode 100644 index 0000000000..93c0f8b813 --- /dev/null +++ b/scripts/sanitize-play-exports.cjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const indexPath = path.join(process.cwd(), 'src', 'plays', 'index.js'); + +if (!fs.existsSync(indexPath)) { + console.warn(`[sanitize-play-exports] Skipped: file not found at ${indexPath}`); + process.exit(0); +} + +const source = fs.readFileSync(indexPath, 'utf8'); +const newline = source.includes('\r\n') ? '\r\n' : '\n'; +const hasTrailingNewline = source.endsWith('\n'); +const lines = source.split(/\r?\n/); + +const exportLinePattern = /^(\s*export\s*\{\s*default\s+as\s+)([^}]+?)(\s*\}\s*from\s*['"][^'"]+['"]\s*;?\s*)$/; +const isValidIdentifier = (value) => /^[$A-Z_a-z][$0-9A-Z_a-z]*$/.test(value); + +const toPascalCase = (value) => { + const chunks = value + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .split(/[^0-9A-Z_a-z$]+/) + .filter(Boolean); + + let identifier = chunks + .map((chunk) => chunk.charAt(0).toUpperCase() + chunk.slice(1)) + .join(''); + + if (identifier.length === 0) { + identifier = 'Play'; + } + + if (!/^[$A-Z_a-z]/.test(identifier)) { + identifier = `Play${identifier}`; + } + + return identifier; +}; + +const usedAliases = new Set(); +let updateCount = 0; + +const nextLines = lines.map((line) => { + const match = line.match(exportLinePattern); + if (match == null) { + return line; + } + + const [, prefix, rawAlias, suffix] = match; + const currentAlias = rawAlias.trim(); + let nextAlias = currentAlias; + + if (!isValidIdentifier(currentAlias)) { + nextAlias = toPascalCase(currentAlias); + } + + const aliasBase = nextAlias; + let duplicateIndex = 2; + while (usedAliases.has(nextAlias)) { + nextAlias = `${aliasBase}${duplicateIndex}`; + duplicateIndex += 1; + } + usedAliases.add(nextAlias); + + if (nextAlias !== currentAlias) { + updateCount += 1; + return `${prefix}${nextAlias}${suffix}`; + } + + return line; +}); + +let nextSource = nextLines.join(newline); +if (hasTrailingNewline) { + nextSource += newline; +} + +if (nextSource !== source) { + fs.writeFileSync(indexPath, nextSource, 'utf8'); + console.log(`[sanitize-play-exports] Updated ${updateCount} export alias(es).`); +} else { + console.log('[sanitize-play-exports] No invalid aliases found.'); +}