From 819057ed8bcc33b30455d55e74a5fbfca94044b1 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sat, 24 Jan 2026 16:24:26 +0000 Subject: [PATCH] feat(settings): add sub-section handling for settings navigation and deep linking --- src/components/Elements/MainModal/Main.jsx | 25 ++++++++++++ .../MainModal/components/ModalTopBar.jsx | 39 +++++++++++++++---- .../background/options/BackgroundOptions.jsx | 31 +++++++-------- .../greeting/options/GreetingOptions.jsx | 15 +++---- src/features/misc/sections/Advanced.jsx | 13 ++++--- src/features/misc/sections/Appearance.jsx | 13 ++++--- src/features/misc/views/Settings.jsx | 8 +++- src/features/quote/options/QuoteOptions.jsx | 17 ++++---- src/i18n/locales/arz.json | 4 +- src/i18n/locales/el.json | 4 +- src/i18n/locales/en_GB.json | 4 +- src/i18n/locales/en_US.json | 4 +- src/i18n/locales/fa.json | 4 +- src/i18n/locales/hu.json | 4 +- src/i18n/locales/ja.json | 4 +- src/i18n/locales/peo.json | 4 +- src/i18n/locales/sv.json | 4 +- src/i18n/locales/vi.json | 4 +- src/i18n/locales/zh_Hant.json | 4 +- src/utils/deepLinking.js | 35 ++++++++++++++--- 20 files changed, 170 insertions(+), 70 deletions(-) diff --git a/src/components/Elements/MainModal/Main.jsx b/src/components/Elements/MainModal/Main.jsx index 07145829..c9a1c704 100644 --- a/src/components/Elements/MainModal/Main.jsx +++ b/src/components/Elements/MainModal/Main.jsx @@ -23,6 +23,8 @@ function MainModal({ modalClose, deepLinkData }) { const initialTab = deepLinkData?.tab || TAB_TYPES.SETTINGS; const [currentTab, setCurrentTab] = useState(initialTab); const [currentSection, setCurrentSection] = useState(''); + const [currentSectionName, setCurrentSectionName] = useState(''); + const [currentSubSection, setCurrentSubSection] = useState(null); const [productView, setProductView] = useState(null); const [resetDiscoverToAll, setResetDiscoverToAll] = useState(false); const [navigationTrigger, setNavigationTrigger] = useState(null); @@ -61,6 +63,8 @@ function MainModal({ modalClose, deepLinkData }) { data: linkData.section, timestamp: Date.now(), }); + // Set sub-section if present in hash + setCurrentSubSection(linkData.subSection || null); return; } @@ -131,6 +135,9 @@ function MainModal({ modalClose, deepLinkData }) { const handleSectionChange = (section, sectionName) => { setCurrentSection(section); + setCurrentSectionName(sectionName); + // Clear sub-section when changing sections + setCurrentSubSection(null); // Update URL hash when section changes if (currentTab === TAB_TYPES.DISCOVER) { // For Discover tab, update with the section type @@ -151,6 +158,19 @@ function MainModal({ modalClose, deepLinkData }) { } }; + const handleSubSectionChange = (subSection, sectionName) => { + setCurrentSubSection(subSection); + // Update URL hash when sub-section changes + if (currentTab === TAB_TYPES.SETTINGS && sectionName) { + if (subSection) { + updateHash(`#${currentTab}/${sectionName}/${subSection}`); + } else { + // Going back to section, remove sub-section from hash + updateHash(`#${currentTab}/${sectionName}`); + } + } + }; + const handleProductView = (product) => { setProductView(product); // URL hash is already updated by child components (Browse.jsx) @@ -182,9 +202,12 @@ function MainModal({ modalClose, deepLinkData }) { { + if (!subSection || !sectionName) return subSection; + + // Use the same translation pattern as the section components + const translationKey = `modals.main.settings.sections.${sectionName}.${subSection}.title`; + const translated = variables.getMessage(translationKey); + + // If translation key is returned as-is or empty, it means translation doesn't exist + // Fall back to capitalized sub-section name + if (!translated || translated === translationKey) { + return subSection.charAt(0).toUpperCase() + subSection.slice(1); + } + + return translated; + }; + // Determine breadcrumb path with click handlers - let breadcrumbPath = []; + const breadcrumbPath = []; if (currentTabLabel) { breadcrumbPath.push({ @@ -63,9 +83,6 @@ function ModalTopBar({ onClick: null, // Current item - not clickable }); } else if (productView) { - console.log('ModalTopBar productView:', productView); - console.log('fromCollection:', productView.fromCollection, 'isCollection:', productView.isCollection, 'collectionTitle:', productView.collectionTitle); - // If viewing a collection page itself (not a product within it) if (productView.isCollection) { // Show: Discover > Collection Name @@ -77,14 +94,12 @@ function ModalTopBar({ // Viewing a product // Show: Discover > Collection/Category > Product if (productView.fromCollection && productView.collectionTitle) { - console.log('Showing collection breadcrumb:', productView.collectionTitle); // If from a collection, show collection name breadcrumbPath.push({ label: productView.collectionTitle, onClick: productView.onBack || null, }); } else { - console.log('Showing category breadcrumb'); // Otherwise show category const categoryKey = MARKETPLACE_TYPE_TO_KEY[productView.type]; if (categoryKey) { @@ -101,11 +116,19 @@ function ModalTopBar({ }); } } else if (currentSection) { - // Show: Tab > Section + // Show: Tab > Section or Tab > Section > Sub-Section breadcrumbPath.push({ label: currentSection, - onClick: null, // Current section - not clickable + onClick: currentSubSection ? () => onSubSectionChange(null) : null, // Clickable if sub-section is active }); + + // Add sub-section if present + if (currentSubSection) { + breadcrumbPath.push({ + label: getSubSectionLabel(currentSubSection, currentSectionName), + onClick: null, // Current sub-section - not clickable + }); + } } } diff --git a/src/features/background/options/BackgroundOptions.jsx b/src/features/background/options/BackgroundOptions.jsx index b69d4e84..237edbbe 100644 --- a/src/features/background/options/BackgroundOptions.jsx +++ b/src/features/background/options/BackgroundOptions.jsx @@ -15,7 +15,7 @@ import NavigationCard from './sections/NavigationCard'; import { getBackgroundOptionItems } from './optionTypes'; -const BackgroundOptions = memo(() => { +const BackgroundOptions = memo(({ currentSubSection, onSubSectionChange, sectionName }) => { const [backgroundType, setBackgroundType] = useState( localStorage.getItem('backgroundType') || 'api', ); @@ -28,21 +28,18 @@ const BackgroundOptions = memo(() => { const [backgroundCategoriesOG, setBackgroundCategoriesOG] = useState([]); const [backgroundAPI, setBackgroundAPI] = useState(localStorage.getItem('backgroundAPI') || 'mue'); const [marketplaceEnabled] = useState(localStorage.getItem('photo_packs')); - const [effects, setEffects] = useState(false); // Auto-show source section for types without effects/display settings const shouldShowSourceByDefault = ['colour', 'random_colour', 'random_gradient'].includes(backgroundType); - const [backgroundSettingsSection, setBackgroundSettingsSection] = useState(shouldShowSourceByDefault); const controllerRef = useRef(null); // Auto-navigate to source section when switching to colour/random types useEffect(() => { - if (shouldShowSourceByDefault) { - setBackgroundSettingsSection(true); - setEffects(false); + if (shouldShowSourceByDefault && currentSubSection !== 'source') { + onSubSectionChange('source', sectionName); } - }, [shouldShowSourceByDefault]); + }, [shouldShowSourceByDefault, currentSubSection, onSubSectionChange, sectionName]); const getBackgroundCategories = useCallback(async () => { const data = await ( @@ -139,26 +136,26 @@ const BackgroundOptions = memo(() => { const showEffects = backgroundType === 'api' || backgroundType === 'custom' || marketplaceEnabled; const getHeader = () => { - if (effects) { + if (currentSubSection === 'effects') { return (
setEffects(false)} + goBack={() => onSubSectionChange(null, sectionName)} /> ); } - if (backgroundSettingsSection) { + if (currentSubSection === 'source') { return (
setBackgroundSettingsSection(false)} + goBack={() => onSubSectionChange(null, sectionName)} /> ); } @@ -177,7 +174,7 @@ const BackgroundOptions = memo(() => { <> {getHeader()} - {!backgroundSettingsSection && !effects && ( + {!currentSubSection && ( <> { subtitle={variables.getMessage( 'modals.main.settings.sections.background.source.subtitle', )} - onClick={() => setBackgroundSettingsSection(true)} + onClick={() => onSubSectionChange('source', sectionName)} action={ { subtitle={variables.getMessage( 'modals.main.settings.sections.background.effects.subtitle', )} - onClick={() => setEffects(true)} + onClick={() => onSubSectionChange('effects', sectionName)} /> )} )} - {!backgroundSettingsSection && !effects && showEffects && ( + {!currentSubSection && showEffects && ( )} - {backgroundSettingsSection && ( + {currentSubSection === 'source' && ( <> { )} - {showEffects && effects && ( + {showEffects && currentSubSection === 'effects' && ( setBackgroundFilter(value)} diff --git a/src/features/greeting/options/GreetingOptions.jsx b/src/features/greeting/options/GreetingOptions.jsx index bd137257..bf1e5beb 100644 --- a/src/features/greeting/options/GreetingOptions.jsx +++ b/src/features/greeting/options/GreetingOptions.jsx @@ -18,11 +18,10 @@ import defaultEvents from '../events.json'; import { MdEventNote, MdAdd, MdCancel, MdRefresh } from 'react-icons/md'; -const GreetingOptions = () => { +const GreetingOptions = ({ currentSubSection, onSubSectionChange, sectionName }) => { const [customEvents, setCustomEvents] = useState( JSON.parse(localStorage.getItem('customEvents')) || [], ); - const [events, setEvents] = useState(false); const [birthday, setBirthday] = useState(() => { const stored = localStorage.getItem('birthday'); @@ -266,13 +265,15 @@ const GreetingOptions = () => { ); }; + const isEventsSection = currentSubSection === 'events'; + let header; - if (events) { + if (isEventsSection) { header = (
setEvents(false)} + secondaryTitle={variables.getMessage(`${GREETING_SECTION}.events.title`)} + goBack={() => onSubSectionChange(null, sectionName)} report={false} /> ); @@ -292,7 +293,7 @@ const GreetingOptions = () => { return ( <> {header} - {events ? ( + {isEventsSection ? ( <> {
setEvents(true)} + onClick={() => onSubSectionChange('events', sectionName)} icon={} /> diff --git a/src/features/misc/sections/Advanced.jsx b/src/features/misc/sections/Advanced.jsx index dc1a23f5..2c8a9199 100644 --- a/src/features/misc/sections/Advanced.jsx +++ b/src/features/misc/sections/Advanced.jsx @@ -18,9 +18,8 @@ import { Header, Section, Row, Content, Action } from 'components/Layout/Setting import time_zones from 'features/time/timezones.json'; -function AdvancedOptions() { +function AdvancedOptions({ currentSubSection, onSubSectionChange, sectionName }) { const [resetModal, setResetModal] = useState(false); - const [data, setData] = useState(false); const ADVANCED_SECTION = 'modals.main.settings.sections.advanced'; const Data = () => { @@ -76,13 +75,15 @@ function AdvancedOptions() { ); }; + const isDataSection = currentSubSection === 'data'; + let header; - if (data) { + if (isDataSection) { header = (
setData(false)} + goBack={() => onSubSectionChange(null, sectionName)} report={false} /> ); @@ -93,7 +94,7 @@ function AdvancedOptions() { return ( <> {header} - {data ? ( + {isDataSection ? ( <> setData(true)} + onClick={() => onSubSectionChange('data', sectionName)} icon={} /> diff --git a/src/features/misc/sections/Appearance.jsx b/src/features/misc/sections/Appearance.jsx index e3a3b1d3..6c51435b 100644 --- a/src/features/misc/sections/Appearance.jsx +++ b/src/features/misc/sections/Appearance.jsx @@ -9,8 +9,7 @@ import { MdAccessibility } from 'react-icons/md'; import values from 'utils/data/slider_values.json'; -function AppearanceOptions() { - const [accessibility, setAccessibility] = useState(false); +function AppearanceOptions({ currentSubSection, onSubSectionChange, sectionName }) { const ThemeSelection = () => { return ( @@ -239,15 +238,17 @@ function AppearanceOptions() { ); }; + const isAccessibilitySection = currentSubSection === 'accessibility'; + let header; - if (accessibility) { + if (isAccessibilitySection) { header = (
setAccessibility(false)} + goBack={() => onSubSectionChange(null, sectionName)} report={false} /> ); @@ -262,7 +263,7 @@ function AppearanceOptions() { return ( <> {header} - {accessibility ? ( + {isAccessibilitySection ? ( ) : ( <> @@ -274,7 +275,7 @@ function AppearanceOptions() { 'modals.main.settings.sections.appearance.accessibility.description', )} icon={} - onClick={() => setAccessibility(true)} + onClick={() => onSubSectionChange('accessibility', sectionName)} /> diff --git a/src/features/misc/views/Settings.jsx b/src/features/misc/views/Settings.jsx index e72fbcda..86822e27 100644 --- a/src/features/misc/views/Settings.jsx +++ b/src/features/misc/views/Settings.jsx @@ -95,13 +95,19 @@ function Settings(props) { current="settings" currentTab={props.currentTab} onSectionChange={props.onSectionChange} + onSubSectionChange={props.onSubSectionChange} + currentSubSection={props.currentSubSection} deepLinkData={props.deepLinkData} navigationTrigger={props.navigationTrigger} sections={sections} > {sections.map(({ label, name, component: Component }) => (
- +
))} diff --git a/src/features/quote/options/QuoteOptions.jsx b/src/features/quote/options/QuoteOptions.jsx index a80e3a7f..260be648 100644 --- a/src/features/quote/options/QuoteOptions.jsx +++ b/src/features/quote/options/QuoteOptions.jsx @@ -14,7 +14,7 @@ import { import { Checkbox, Dropdown } from 'components/Form/Settings'; import { Button } from 'components/Elements'; -const QuoteOptions = () => { +const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) => { const getCustom = () => { let data = JSON.parse(localStorage.getItem('customQuote')); if (data === null) { @@ -25,7 +25,6 @@ const QuoteOptions = () => { const [quoteType, setQuoteType] = useState(localStorage.getItem('quoteType') || 'api'); const [customQuote, setCustomQuote] = useState(getCustom()); - const [sourceSection, setSourceSection] = useState(false); const handleCustomQuote = (e, text, index, type) => { const result = text === true ? e.target.value : e.target.result; @@ -134,8 +133,10 @@ const QuoteOptions = () => { ); }; + const isSourceSection = currentSubSection === 'source'; + let customSettings; - if (quoteType === 'custom' && sourceSection === true) { + if (quoteType === 'custom' && isSourceSection) { customSettings = ( <> @@ -221,13 +222,13 @@ const QuoteOptions = () => { return ( <> - {sourceSection ? ( + {isSourceSection ? (
setSourceSection(false)} + goBack={() => onSubSectionChange(null, sectionName)} report={false} /> ) : ( @@ -240,7 +241,7 @@ const QuoteOptions = () => { visibilityToggle={true} /> )} - {sourceSection && ( + {isSourceSection && ( { )} - {!sourceSection && ( + {!isSourceSection && ( { icon={} title={variables.getMessage('modals.main.settings.sections.background.source.title')} subtitle={variables.getMessage(`${QUOTE_SECTION}.source_subtitle`)} - onClick={() => setSourceSection(true)} + onClick={() => onSubSectionChange('source', sectionName)} >
diff --git a/src/i18n/locales/arz.json b/src/i18n/locales/arz.json index f9f7ff3a..0f0984e7 100644 --- a/src/i18n/locales/arz.json +++ b/src/i18n/locales/arz.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/el.json b/src/i18n/locales/el.json index 1b66742c..9871d072 100644 --- a/src/i18n/locales/el.json +++ b/src/i18n/locales/el.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/en_GB.json b/src/i18n/locales/en_GB.json index 85c20f81..351afab1 100644 --- a/src/i18n/locales/en_GB.json +++ b/src/i18n/locales/en_GB.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/en_US.json b/src/i18n/locales/en_US.json index 684414b9..ae799dda 100644 --- a/src/i18n/locales/en_US.json +++ b/src/i18n/locales/en_US.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/fa.json b/src/i18n/locales/fa.json index 51d5e54c..59796e42 100644 --- a/src/i18n/locales/fa.json +++ b/src/i18n/locales/fa.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/hu.json b/src/i18n/locales/hu.json index 85c20f81..351afab1 100644 --- a/src/i18n/locales/hu.json +++ b/src/i18n/locales/hu.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 03c6f606..ad7c7534 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/peo.json b/src/i18n/locales/peo.json index a04b445d..1fb2662a 100644 --- a/src/i18n/locales/peo.json +++ b/src/i18n/locales/peo.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/sv.json b/src/i18n/locales/sv.json index 0078982b..6d5f6835 100644 --- a/src/i18n/locales/sv.json +++ b/src/i18n/locales/sv.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/vi.json b/src/i18n/locales/vi.json index 7c7f5207..c53aa096 100644 --- a/src/i18n/locales/vi.json +++ b/src/i18n/locales/vi.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/i18n/locales/zh_Hant.json b/src/i18n/locales/zh_Hant.json index 7ed085c6..651de479 100644 --- a/src/i18n/locales/zh_Hant.json +++ b/src/i18n/locales/zh_Hant.json @@ -183,7 +183,9 @@ }, "greeting": { "title": "Greeting", - "events": "Events", + "events": { + "title": "Events" + }, "events_description": "Control events on Mue such as birthdays.", "enable_events": "Show messages on special days", "default": "Default greeting message", diff --git a/src/utils/deepLinking.js b/src/utils/deepLinking.js index d6a23e40..7c15bf38 100644 --- a/src/utils/deepLinking.js +++ b/src/utils/deepLinking.js @@ -16,6 +16,7 @@ import { TAB_TYPES } from '../components/Elements/MainModal/constants/tabConfig' * #discover/collection/featured/f41219846700 -> { tab: 'discover', collection: 'featured', itemId: 'f41219846700' } * #marketplace/74ef53ceed0b -> { tab: 'discover', itemId: '74ef53ceed0b' } (marketplace is aliased to discover) * #settings/appearance -> { tab: 'settings', section: 'appearance' } + * #settings/background/source -> { tab: 'settings', section: 'background', subSection: 'source' } * #addons -> { tab: 'addons' } * * Legacy format (still supported): @@ -33,7 +34,8 @@ export const parseDeepLink = (hash = window.location.hash) => { const result = { tab: parts[0], section: parts[1], - itemId: parts[2], + subSection: parts[2], + itemId: parts[3], }; // Handle marketplace as an alias for discover (for backward compatibility with external URLs) @@ -47,14 +49,26 @@ export const parseDeepLink = (hash = window.location.hash) => { return null; } + // Handle settings-specific parsing for sub-sections + if (result.tab === 'settings') { + // If we have a third part, it's a sub-section + if (result.subSection && !result.itemId) { + // Keep subSection as is, clear itemId + result.itemId = null; + } + // Otherwise section only, no sub-section + else if (!result.subSection) { + result.subSection = null; + } + } + // Handle discover-specific parsing (marketplace URLs are aliased to discover) if (result.tab === 'discover') { // Check if it's a collection if (result.section === 'collection') { - result.collection = result.itemId; + result.collection = result.subSection; // Check if there's a 4th part (item ID within collection) - if (parts[3]) { - result.itemId = parts[3]; + if (result.itemId) { result.fromCollection = true; } else { result.itemId = null; @@ -63,10 +77,14 @@ export const parseDeepLink = (hash = window.location.hash) => { // Check if section is a category (preset_settings, photo_packs, quote_packs) else if (['preset_settings', 'photo_packs', 'quote_packs', 'all'].includes(result.section)) { result.category = result.section; - // Third part is the item ID (already in result.itemId) + // Third part is the item ID (now in subSection due to earlier parsing) + if (result.subSection) { + result.itemId = result.subSection; + result.subSection = null; + } } // If only one part after tab, assume it's an item ID - else if (result.section && !result.itemId) { + else if (result.section && !result.subSection) { result.itemId = result.section; result.section = null; } @@ -84,6 +102,7 @@ export const parseDeepLink = (hash = window.location.hash) => { * @param {string} options.collection - Collection name for discover/marketplace * @param {boolean} options.fromCollection - If item is being viewed from within a collection * @param {string} options.section - Section within the tab + * @param {string} options.subSection - Sub-section within a settings section * @returns {string} Hash string */ export const createDeepLink = (tab, options = {}) => { @@ -112,6 +131,10 @@ export const createDeepLink = (tab, options = {}) => { } } else if (options.section) { hash += `/${options.section}`; + // Add sub-section for settings tab + if (options.subSection) { + hash += `/${options.subSection}`; + } } return hash;