From b2efb8286b72c79ea4c7113bb6419b400c537063 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Thu, 19 Jun 2025 15:20:30 -0400 Subject: [PATCH 01/13] chore: 2025-08 content build --- .../assets/css/sprites/spritesmith-main.css | 30 +++++++++++++++++++ website/common/locales/en/backgrounds.json | 4 +++ website/common/locales/en/gear.json | 8 +++++ website/common/locales/en/subscriber.json | 1 + .../script/content/appearance/backgrounds.js | 3 ++ .../script/content/constants/releaseDates.js | 1 + .../script/content/gear/sets/armoire.js | 10 +++++++ .../script/content/gear/sets/mystery.js | 2 ++ 8 files changed, 59 insertions(+) diff --git a/website/client/src/assets/css/sprites/spritesmith-main.css b/website/client/src/assets/css/sprites/spritesmith-main.css index 4480a8f538..9246bc59c9 100644 --- a/website/client/src/assets/css/sprites/spritesmith-main.css +++ b/website/client/src/assets/css/sprites/spritesmith-main.css @@ -2191,6 +2191,11 @@ width: 141px; height: 147px; } +.background_sunny_street_with_shops { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_sunny_street_with_shops.png'); + width: 141px; + height: 147px; +} .background_sunset_meadow { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_sunset_meadow.png'); width: 141px; @@ -29900,6 +29905,11 @@ width: 90px; height: 90px; } +.broad_armor_armoire_redWaistcoat { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_redWaistcoat.png'); + width: 114px; + height: 90px; +} .broad_armor_armoire_robeOfDiamonds { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_robeOfDiamonds.png'); width: 114px; @@ -30535,6 +30545,11 @@ width: 90px; height: 90px; } +.head_armoire_redNewsieHat { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_redNewsieHat.png'); + width: 114px; + height: 90px; +} .head_armoire_regalCrown { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_regalCrown.png'); width: 114px; @@ -31475,6 +31490,11 @@ width: 90px; height: 90px; } +.slim_armor_armoire_redWaistcoat { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_redWaistcoat.png'); + width: 114px; + height: 90px; +} .slim_armor_armoire_robeOfDiamonds { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_robeOfDiamonds.png'); width: 114px; @@ -35635,6 +35655,16 @@ width: 114px; height: 90px; } +.shield_mystery_202508 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202508.png'); + width: 117px; + height: 120px; +} +.weapon_mystery_202508 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202508.png'); + width: 117px; + height: 120px; +} .broad_armor_mystery_301404 { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_301404.png'); width: 90px; diff --git a/website/common/locales/en/backgrounds.json b/website/common/locales/en/backgrounds.json index f8d1b582ff..63f5229849 100644 --- a/website/common/locales/en/backgrounds.json +++ b/website/common/locales/en/backgrounds.json @@ -1035,6 +1035,10 @@ "backgroundSirensLairText": "Siren's Lair", "backgroundSirensLairNotes": "Dare to dive into a Siren’s Lair.", + "backgrounds082025": "SET 135: Released August 2025", + "backgroundsSunnyStreetWithShopsText": "Sunny Street with Shops", + "backgroundsSunnyStreetWithShopsNotes": "Enjoy the sights and sounds of a Sunny Street with Shops.", + "timeTravelBackgrounds": "Steampunk Backgrounds", "backgroundAirshipText": "Airship", "backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.", diff --git a/website/common/locales/en/gear.json b/website/common/locales/en/gear.json index 2ae38371a5..660c108bc2 100644 --- a/website/common/locales/en/gear.json +++ b/website/common/locales/en/gear.json @@ -600,6 +600,8 @@ "weaponMystery202404Notes": "This staff will bestow upon you an ancient wisdom as ageless as the rocks and trees. Confers no benefit. April 2024 Subscriber Item.", "weaponMystery202408Text": "Arcane Aegis", "weaponMystery202408Notes": "A magic bubble shield that protects you from enemy spells or helps you float in the air or water. Confers no benefit. August 2024 Subscriber Item.", + "weaponMystery202508Text": "Brilliant Crimson Blade", + "weaponMystery202508Notes": "This spinning blade will terrify any monster or red Daily that crosses your path! Confers no benefit. August 2025 Subscriber Item.", "weaponMystery301404Text": "Steampunk Cane", "weaponMystery301404Notes": "Excellent for taking a turn about town. March 3015 Subscriber Item. Confers no benefit.", @@ -1736,6 +1738,8 @@ "armorArmoireBeekeepersSuitNotes": "Keep yourself safe as you look after your busy bumbles. Increases Constitution by <%= con %>. Enchanted Armoire: Beekeeper Set (Item 2 of 4)", "armorArmoireFlyFishingWadersText": "Fly Fishing Waders", "armorArmoireFlyFishingWadersNotes": "Stay perfectly warm and dry when you wade into a stream, pond, lake, or river. Increases Strength and Constitution by <%= attrs %> each. Enchanted Armoire: Fly Fishing Set (Item 2 of 3)", + "armorArmoireRedWaistcoatText": "Red Waistcoat", + "armorArmoireRedWaistcoatNotes": "Look smart and stunning as you tackle your tasks. There’s something secret hidden in the vest pocket—what do you think it could be? Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Red Waistcoat Set (Item 2 of 2)", "headgear": "helm", "headgearCapitalized": "Headgear", @@ -2673,6 +2677,8 @@ "headArmoireBeekeepersHatNotes": "Keep your face safe as you tend to your buzzing buddies. Increases Perception by <%= per %>. Enchanted Armoire: Beekeeper Set (Item 1 of 4)", "headArmoireFlyFishingHatText": "Fly Fishing Hat", "headArmoireFlyFishingHatNotes": "With a wide brim and spot to hold a spare lure, don’t go fly fishing without this beauty. Increases Strength and Perception by <%= attrs %> each. Enchanted Armoire: Fly Fishing Set (Item 1 of 3)", + "headArmoireRedNewsieHatText": "Red Newsboy Cap", + "headArmoireRedNewsieHatNotes": "Extra! Extra! Read all about it: this cap is comfortable, fashionable, and practical. Increases Perception and Intelligence by <%= attrs %> each. Enchanted Armoire: Red Waistcoat Set (Item 1 of 2)", "offhand": "off-hand item", "offHandCapitalized": "Off-Hand Item", @@ -3006,6 +3012,8 @@ "shieldMystery202502Notes": "This Valentine’s Day and every day, may your heart be as light as these buoyant balloons. Confers no benefit. February 2025 Subscriber Item.", "shieldMystery202506Text": "Solar Shine Shield", "shieldMystery202506Notes": "Dispel darkness and bestow warm and cheerful rays wherever you are. Confers no benefit. June 2025 Subscriber Item.", + "shieldMystery202508Text": "Brilliant Cyan Blade", + "shieldMystery202508Notes": "If you thought one spinning blade was cool looking, try two! Confers no benefit. August 2025 Subscriber Item.", "shieldMystery301405Text": "Clock Shield", "shieldMystery301405Notes": "Time is on your side with this towering clock shield! Confers no benefit. June 3015 Subscriber Item.", diff --git a/website/common/locales/en/subscriber.json b/website/common/locales/en/subscriber.json index f4eba78f95..b6bb60ed5c 100644 --- a/website/common/locales/en/subscriber.json +++ b/website/common/locales/en/subscriber.json @@ -176,6 +176,7 @@ "mysterySet202505": "Soaring Swallowtail Set", "mysterySet202506": "Solar Shine Set", "mysterySet202507": "Spunky Skater Set", + "mysterySet202508": "Brilliant Blade Set", "mysterySet301404": "Steampunk Standard Set", "mysterySet301405": "Steampunk Accessories Set", "mysterySet301703": "Peacock Steampunk Set", diff --git a/website/common/script/content/appearance/backgrounds.js b/website/common/script/content/appearance/backgrounds.js index 3034db2013..f2c89ead8b 100644 --- a/website/common/script/content/appearance/backgrounds.js +++ b/website/common/script/content/appearance/backgrounds.js @@ -662,6 +662,9 @@ const backgrounds = { backgrounds072025: { sirens_lair: { }, }, + backgrounds082025: { + sunny_street_with_shops: { }, + }, eventBackgrounds: { birthday_bash: { price: 0, diff --git a/website/common/script/content/constants/releaseDates.js b/website/common/script/content/constants/releaseDates.js index 9dfc37d63f..ddd2ed0dbc 100644 --- a/website/common/script/content/constants/releaseDates.js +++ b/website/common/script/content/constants/releaseDates.js @@ -22,6 +22,7 @@ export const ARMOIRE_RELEASE_DATES = { gildedKnightSet: { year: 2025, month: 5 }, beekeepersSet: { year: 2025, month: 6 }, flyFishingSet: { year: 2025, month: 7 }, + redWaistcoatSet: { year: 2025, mont: 8 }, }; export const EGGS_RELEASE_DATES = { diff --git a/website/common/script/content/gear/sets/armoire.js b/website/common/script/content/gear/sets/armoire.js index e79764f14f..f4c2874c4f 100644 --- a/website/common/script/content/gear/sets/armoire.js +++ b/website/common/script/content/gear/sets/armoire.js @@ -537,6 +537,11 @@ const armor = { con: 7, set: 'flyFishingSet', }, + redWaistcoat: { + con: 8, + str: 8, + set: 'redWaistcoatSet', + }, }; const body = { @@ -1098,6 +1103,11 @@ const head = { per: 7, set: 'flyFishingSet', }, + redNewsieHat: { + per: 8, + int: 8, + set: 'redWaistcoatSet', + }, }; const shield = { diff --git a/website/common/script/content/gear/sets/mystery.js b/website/common/script/content/gear/sets/mystery.js index 7f2752ddf3..9e2092d17a 100644 --- a/website/common/script/content/gear/sets/mystery.js +++ b/website/common/script/content/gear/sets/mystery.js @@ -297,6 +297,7 @@ const shield = { 202501: { }, 202502: { }, 202506: { }, + 202508: { }, 301405: { }, 301704: { }, }; @@ -322,6 +323,7 @@ const weapon = { 202403: { }, 202404: { twoHanded: true }, 202408: { }, + 202508: { }, 301404: { }, }; From 338c633cdb05573b1d182632ccf866b5b84e20c2 Mon Sep 17 00:00:00 2001 From: CuriousMagpie Date: Fri, 20 Jun 2025 13:56:20 -0400 Subject: [PATCH 02/13] chore: fix typos --- website/common/locales/en/backgrounds.json | 4 ++-- website/common/script/content/constants/releaseDates.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/website/common/locales/en/backgrounds.json b/website/common/locales/en/backgrounds.json index 63f5229849..0113dfc1e9 100644 --- a/website/common/locales/en/backgrounds.json +++ b/website/common/locales/en/backgrounds.json @@ -1036,8 +1036,8 @@ "backgroundSirensLairNotes": "Dare to dive into a Siren’s Lair.", "backgrounds082025": "SET 135: Released August 2025", - "backgroundsSunnyStreetWithShopsText": "Sunny Street with Shops", - "backgroundsSunnyStreetWithShopsNotes": "Enjoy the sights and sounds of a Sunny Street with Shops.", + "backgroundSunnyStreetWithShopsText": "Sunny Street with Shops", + "backgroundSunnyStreetWithShopsNotes": "Enjoy the sights and sounds of a Sunny Street with Shops.", "timeTravelBackgrounds": "Steampunk Backgrounds", "backgroundAirshipText": "Airship", diff --git a/website/common/script/content/constants/releaseDates.js b/website/common/script/content/constants/releaseDates.js index ddd2ed0dbc..03c954d15c 100644 --- a/website/common/script/content/constants/releaseDates.js +++ b/website/common/script/content/constants/releaseDates.js @@ -22,7 +22,7 @@ export const ARMOIRE_RELEASE_DATES = { gildedKnightSet: { year: 2025, month: 5 }, beekeepersSet: { year: 2025, month: 6 }, flyFishingSet: { year: 2025, month: 7 }, - redWaistcoatSet: { year: 2025, mont: 8 }, + redWaistcoatSet: { year: 2025, month: 8 }, }; export const EGGS_RELEASE_DATES = { From c71528a478073776fc602ee58ffa5595840767f1 Mon Sep 17 00:00:00 2001 From: Hafiz Date: Mon, 4 Aug 2025 12:56:32 -0500 Subject: [PATCH 03/13] Respect user language preference in content endpoint Content API now returns data in user's preferred language when authenticated without language parameter. No breaking changes - existing clients unaffected. --- .../integration/content/GET-content.test.js | 25 +++++++++++++++++++ website/server/controllers/api-v3/content.js | 7 ++++++ 2 files changed, 32 insertions(+) diff --git a/test/api/v3/integration/content/GET-content.test.js b/test/api/v3/integration/content/GET-content.test.js index 0dbd2779e4..439667be9f 100644 --- a/test/api/v3/integration/content/GET-content.test.js +++ b/test/api/v3/integration/content/GET-content.test.js @@ -1,6 +1,7 @@ import { requester, translate as t, + generateUser, } from '../../../../helpers/api-integration/v3'; import i18n from '../../../../../website/common/script/i18n'; @@ -56,4 +57,28 @@ describe('GET /content', () => { const res = await requester().get('/content?filter=backgroundsFlat,invalid'); expect(res).to.not.have.property('backgroundsFlat'); }); + + describe('authenticated user', () => { + let user; + it('returns content in user\'s preferred language when no language parameter is provided', async () => { + user = await generateUser({ 'preferences.language': 'de' }); + const res = await user.get('/content'); + expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach'); + expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'de')); + }); + + it('respects language parameter over user\'s preferred language', async () => { + user = await generateUser({ 'preferences.language': 'de' }); + const res = await user.get('/content?language=fr'); + expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach'); + expect(res.backgrounds.backgrounds062014.beach.text).to.equal(i18n.t('backgroundBeachText', 'fr')); + }); + + it('falls back to English if user\'s preferred language is invalid', async () => { + user = await generateUser({ 'preferences.language': 'invalid_lang' }); + const res = await user.get('/content'); + expect(res).to.have.nested.property('backgrounds.backgrounds062014.beach'); + expect(res.backgrounds.backgrounds062014.beach.text).to.equal(t('backgroundBeachText')); + }); + }); }); diff --git a/website/server/controllers/api-v3/content.js b/website/server/controllers/api-v3/content.js index ed8a79d782..71fade7430 100644 --- a/website/server/controllers/api-v3/content.js +++ b/website/server/controllers/api-v3/content.js @@ -1,6 +1,7 @@ import nconf from 'nconf'; import { langCodes } from '../../libs/i18n'; import { serveContent } from '../../libs/content'; +import { authWithHeaders } from '../../middlewares/auth'; const IS_PROD = nconf.get('IS_PROD'); @@ -66,12 +67,18 @@ api.getContent = { method: 'GET', url: '/content', noLanguage: true, + middlewares: [authWithHeaders({ optional: true })], async handler (req, res) { let language = 'en'; const proposedLang = req.query.language; if (proposedLang && langCodes.includes(proposedLang)) { language = proposedLang; + } else if (res.locals.user && res.locals.user.preferences && res.locals.user.preferences.language) { + const userLang = res.locals.user.preferences.language; + if (langCodes.includes(userLang)) { + language = userLang; + } } let filter = req.query.filter || ''; From 53aa960afd4a046b6171ab2d26054142c72458fa Mon Sep 17 00:00:00 2001 From: Hafiz Date: Mon, 4 Aug 2025 13:08:16 -0500 Subject: [PATCH 04/13] fix lint --- website/server/controllers/api-v3/content.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/website/server/controllers/api-v3/content.js b/website/server/controllers/api-v3/content.js index 71fade7430..e48b79fb1c 100644 --- a/website/server/controllers/api-v3/content.js +++ b/website/server/controllers/api-v3/content.js @@ -74,7 +74,10 @@ api.getContent = { if (proposedLang && langCodes.includes(proposedLang)) { language = proposedLang; - } else if (res.locals.user && res.locals.user.preferences && res.locals.user.preferences.language) { + } else if (res.locals.user + && res.locals.user.preferences + && res.locals.user.preferences.language + ) { const userLang = res.locals.user.preferences.language; if (langCodes.includes(userLang)) { language = userLang; From c0508a2f22c90c66694d633e152c8bc447c7ed73 Mon Sep 17 00:00:00 2001 From: Hafiz Date: Tue, 5 Aug 2025 11:59:11 -0500 Subject: [PATCH 05/13] Fix profile modal tab navigation URLs for both own and other users profiles - Add routes for /user/profile, /user/stats, and /user/achievements - Update selectPage() to properly update URLs when switching tabs - Own profile uses /user/{tab} format - Other users' profiles use /profile/{userId}#{tab} format - Parse hash fragments when navigating to other users' profile tabs - Ensure direct navigation to tab URLs opens correct tab --- .../src/components/userMenu/profile.vue | 23 ++++++++++++++++--- website/client/src/router/index.js | 7 ++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/website/client/src/components/userMenu/profile.vue b/website/client/src/components/userMenu/profile.vue index c85e69f968..09fd792e07 100644 --- a/website/client/src/components/userMenu/profile.vue +++ b/website/client/src/components/userMenu/profile.vue @@ -1126,7 +1126,12 @@ export default { this.loadUser(); this.oldTitle = this.$store.state.title; this.handleExternalLinks(); - this.selectPage(this.startingPage); + // Check if there's a hash in the URL to determine the starting page + let pageToSelect = this.startingPage; + if (window.location.hash && (window.location.hash === '#stats' || window.location.hash === '#achievements')) { + pageToSelect = window.location.hash.substring(1); + } + this.selectPage(pageToSelect); this.$root.$on('habitica:report-profile-result', () => { this.loadUser(); }); @@ -1211,10 +1216,22 @@ export default { }, selectPage (page) { this.selectedPage = page || 'profile'; - window.history.replaceState(null, null, ''); + // Update URL based on whether viewing own profile or another user's profile + let newPath; + if (this.userId === this.userLoggedIn._id) { + // Own profile - use /user/profile, /user/stats, /user/achievements + newPath = `/user/${page}`; + } else { + // Other user's profile - use /profile/:userId#stats, /profile/:userId#achievements + newPath = `/profile/${this.userId}`; + if (page !== 'profile') { + newPath += `#${page}`; + } + } + window.history.replaceState(null, null, newPath); this.$store.dispatch('common:setTitle', { section: this.$t('user'), - subSection: this.$t(this.startingPage), + subSection: this.$t(page), }); }, getNextIncentive () { diff --git a/website/client/src/router/index.js b/website/client/src/router/index.js index 5c02b358d5..7adef8675b 100644 --- a/website/client/src/router/index.js +++ b/website/client/src/router/index.js @@ -98,6 +98,9 @@ const router = new VueRouter({ path: '/profile/:userId', props: true, }, + { name: 'profile', path: '/user/profile' }, + { name: 'stats', path: '/user/stats' }, + { name: 'achievements', path: '/user/achievements' }, { path: '/inventory', component: InventoryContainer, @@ -332,6 +335,10 @@ router.beforeEach(async (to, from, next) => { if (to.params.startingPage !== undefined) { startingPage = to.params.startingPage; } + // Check if there's a hash in the URL for stats or achievements + if (to.hash === '#stats' || to.hash === '#achievements') { + startingPage = to.hash.substring(1); + } if (from.name === null) { store.state.postLoadModal = `profile/${to.params.userId}`; return next({ name: 'tasks' }); From ffa73698a5070efa316165b6e85f006eb54a79ca Mon Sep 17 00:00:00 2001 From: Hafiz Date: Tue, 5 Aug 2025 12:39:33 -0500 Subject: [PATCH 06/13] Server now matches usernames case insensitively like client - Preserves original capitalization in mention text - Fixes profile links not working with wrong case mentions --- test/api/unit/libs/highlightMentions.test.js | 6 ++++++ test/api/v3/integration/chat/POST-chat.test.js | 12 ++++++++++++ website/server/libs/highlightMentions.js | 16 ++++++++++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/test/api/unit/libs/highlightMentions.test.js b/test/api/unit/libs/highlightMentions.test.js index 30fb34c72c..266bfbaaa6 100644 --- a/test/api/unit/libs/highlightMentions.test.js +++ b/test/api/unit/libs/highlightMentions.test.js @@ -47,6 +47,12 @@ describe('highlightMentions', () => { expect(result[0]).to.equal('[@user-dash](/profile/444): message [@user_underscore](/profile/555)'); }); + it('highlights users with case-insensitive matching', async () => { + const text = '@USER: message @User2 @USER3'; + const result = await highlightMentions(text); + expect(result[0]).to.equal('[@USER](/profile/111): message [@User2](/profile/222) [@USER3](/profile/333)'); + }); + it('doesn\'t highlight nonexisting users', async () => { const text = '@nouser message'; const result = await highlightMentions(text); diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js index ba4ac00ed0..f16cbea6f9 100644 --- a/test/api/v3/integration/chat/POST-chat.test.js +++ b/test/api/v3/integration/chat/POST-chat.test.js @@ -238,6 +238,18 @@ describe('POST /chat', () => { expect(groupMessages[0].id).to.exist; }); + it('creates a chat with case-insensitive mentions', async () => { + const originalUsername = member.auth.local.username; + const uppercaseUsername = originalUsername.toUpperCase(); + const messageWithMentions = `hi @${uppercaseUsername}`; + const newMessage = await user.post(`/groups/${groupWithChat._id}/chat`, { message: messageWithMentions }); + const groupMessages = await user.get(`/groups/${groupWithChat._id}/chat`); + + expect(newMessage.message.id).to.exist; + expect(newMessage.message.text).to.include(`[@${uppercaseUsername}](/profile/${member._id})`); + expect(groupMessages[0].id).to.exist; + }); + it('creates a chat with a max length of 3000 chars', async () => { const veryLongMessage = ` 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789. diff --git a/website/server/libs/highlightMentions.js b/website/server/libs/highlightMentions.js index ccb074590e..9798b490d0 100644 --- a/website/server/libs/highlightMentions.js +++ b/website/server/libs/highlightMentions.js @@ -164,18 +164,26 @@ export default async function highlightMentions (text) { if (mentions && mentions.length <= 5) { const usernames = mentions.map(mention => mention.substr(1)); + const usernameRegexes = usernames.map(username => new RegExp(`^${escapeRegExp(username)}$`, 'i')); members = await User - .find({ 'auth.local.username': { $in: usernames }, 'flags.verifiedUsername': true }) + .find({ + $or: usernameRegexes.map(regex => ({ 'auth.local.username': regex })), + 'flags.verifiedUsername': true + }) .select(['auth.local.username', '_id', 'preferences.pushNotifications', 'pushDevices', 'party', 'guilds']) .lean() .exec(); const baseUrl = determineBaseUrl(); members.forEach(member => { const { username } = member.auth.local; - const regex = new RegExp(`@${username}(?![\\-\\w])`, 'g'); - const replacement = `[@${username}](${baseUrl}/profile/${member._id})`; + const regex = new RegExp(`@${escapeRegExp(username)}(?![\\-\\w])`, 'gi'); - textBlocks.transformValidBlocks(blockText => blockText.replace(regex, replacement)); + textBlocks.transformValidBlocks(blockText => + blockText.replace(regex, match => { + const mentionedUsername = match.substr(1); + return `[@${mentionedUsername}](${baseUrl}/profile/${member._id})`; + }) + ); }); } From 0c0dc20dcc6af3e7b6de746e1283eec16fe2bd5e Mon Sep 17 00:00:00 2001 From: Hafiz Date: Tue, 5 Aug 2025 12:53:28 -0500 Subject: [PATCH 07/13] Fix undefined userId --- website/client/src/router/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/website/client/src/router/index.js b/website/client/src/router/index.js index 7adef8675b..4a262e625e 100644 --- a/website/client/src/router/index.js +++ b/website/client/src/router/index.js @@ -360,6 +360,7 @@ router.beforeEach(async (to, from, next) => { if ((to.name === 'stats' || to.name === 'achievements' || to.name === 'profile') && from.name !== null) { router.app.$emit('habitica:show-profile', { + userId: store.state.user.data._id, startingPage: to.name, fromPath: from.path, toPath: to.path, From c1ab9cb6cad687c06e7b9596707c2284fcc283cc Mon Sep 17 00:00:00 2001 From: Hafiz Date: Tue, 5 Aug 2025 13:10:07 -0500 Subject: [PATCH 08/13] lint fixes --- website/server/libs/highlightMentions.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/website/server/libs/highlightMentions.js b/website/server/libs/highlightMentions.js index 9798b490d0..ae2ca5d293 100644 --- a/website/server/libs/highlightMentions.js +++ b/website/server/libs/highlightMentions.js @@ -168,7 +168,7 @@ export default async function highlightMentions (text) { members = await User .find({ $or: usernameRegexes.map(regex => ({ 'auth.local.username': regex })), - 'flags.verifiedUsername': true + 'flags.verifiedUsername': true, }) .select(['auth.local.username', '_id', 'preferences.pushNotifications', 'pushDevices', 'party', 'guilds']) .lean() @@ -178,12 +178,10 @@ export default async function highlightMentions (text) { const { username } = member.auth.local; const regex = new RegExp(`@${escapeRegExp(username)}(?![\\-\\w])`, 'gi'); - textBlocks.transformValidBlocks(blockText => - blockText.replace(regex, match => { - const mentionedUsername = match.substr(1); - return `[@${mentionedUsername}](${baseUrl}/profile/${member._id})`; - }) - ); + textBlocks.transformValidBlocks(blockText => blockText.replace(regex, match => { + const mentionedUsername = match.substr(1); + return `[@${mentionedUsername}](${baseUrl}/profile/${member._id})`; + })); }); } From ec91b674f15577b67dacc120fb9b877c9cc9605e Mon Sep 17 00:00:00 2001 From: Hafiz Date: Wed, 6 Aug 2025 11:33:38 -0500 Subject: [PATCH 09/13] g1g1 width auto sizing w/padding --- .../client/src/components/header/notifications/g1g1.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/website/client/src/components/header/notifications/g1g1.vue b/website/client/src/components/header/notifications/g1g1.vue index 1edf280019..ec684d9d41 100644 --- a/website/client/src/components/header/notifications/g1g1.vue +++ b/website/client/src/components/header/notifications/g1g1.vue @@ -70,13 +70,19 @@ } .btn-secondary { - width: 5.75rem; + min-width: 5.75rem; + width: auto; + max-width: calc(100% - 2rem); min-height: 1.5rem; + padding: 0.25rem 0.75rem; border-radius: 2px; border-color: $white; box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12); font-size: 12px; font-weight: bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } From 08ff95d015c6c82efaf6ed5672437bdf6405f855 Mon Sep 17 00:00:00 2001 From: Hafiz Date: Wed, 6 Aug 2025 12:15:43 -0500 Subject: [PATCH 10/13] Challenge participants spacing & text sizing fix --- .../components/challenges/challengeDetail.vue | 77 +++++++++++++++---- 1 file changed, 64 insertions(+), 13 deletions(-) diff --git a/website/client/src/components/challenges/challengeDetail.vue b/website/client/src/components/challenges/challengeDetail.vue index f6d146249f..c9a4216532 100644 --- a/website/client/src/components/challenges/challengeDetail.vue +++ b/website/client/src/components/challenges/challengeDetail.vue @@ -75,16 +75,20 @@ class="box member-count" @click="showMemberModal()" > -
- {{ challenge.memberCount }} -
- {{ $t('participantsTitle') }} +
+
+
+ {{ challenge.memberCount }} +
+
+ {{ $t('participantsTitle') }} +
@@ -304,7 +308,7 @@ .box { display: inline-block; - padding: 1em; + padding: 0.5em; border-radius: 2px; background-color: $white; box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12); @@ -314,22 +318,69 @@ text-align: center; font-size: 20px; vertical-align: bottom; + overflow: hidden; + position: relative; &.member-count:hover { cursor: pointer; } + .box-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; + } + + .icon-number-row { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.1em; + + .number { + font-size: 20px; + font-weight: normal; + margin-left: 0.2em; + } + } + .svg-icon { width: 30px; display: inline-block; - margin-right: .2em; vertical-align: bottom; } .details { font-size: 12px; - margin-top: 0.4em; color: $gray-200; + width: 100%; + padding: 0 4px; + line-height: 1.15; + word-break: break-word; + max-height: 2.3em; + overflow: visible; + } + + &.member-count { + .icon-number-row { + .svg-icon { + width: 24px; + height: 24px; + } + + .number { + font-size: 18px; + } + } + + .details { + font-size: 11px; + line-height: 1.1; + max-height: 2.2em; + } } } From 4250fbc53f3dcb78a07b17e5239f07ff78c601cd Mon Sep 17 00:00:00 2001 From: Hafiz Date: Wed, 6 Aug 2025 13:24:31 -0500 Subject: [PATCH 11/13] Fix inconsistent profile URL format between own and other users' profiles - Update profile tab navigation to use consistent URL format for all users - Redirect old /user/* routes to new format for backward compatibility - Update all navigation points (dropdown menu, notifications) to use new URLs --- .../achievements/onboardingComplete.vue | 2 +- .../header/notifications/onboardingComplete.vue | 2 +- .../notifications/unallocatedStatsPoints.vue | 2 +- .../client/src/components/header/userDropdown.vue | 7 ++++++- .../client/src/components/userMenu/profile.vue | 15 ++++----------- website/client/src/router/index.js | 11 +++++++++-- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/website/client/src/components/achievements/onboardingComplete.vue b/website/client/src/components/achievements/onboardingComplete.vue index b7fb595191..6b8ccedaf4 100644 --- a/website/client/src/components/achievements/onboardingComplete.vue +++ b/website/client/src/components/achievements/onboardingComplete.vue @@ -117,7 +117,7 @@ export default { closeWithAction () { this.close(); setTimeout(() => { - this.$router.push({ name: 'achievements' }); + this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`); }, 200); }, }, diff --git a/website/client/src/components/header/notifications/onboardingComplete.vue b/website/client/src/components/header/notifications/onboardingComplete.vue index 9ad68ba316..b30dacf152 100644 --- a/website/client/src/components/header/notifications/onboardingComplete.vue +++ b/website/client/src/components/header/notifications/onboardingComplete.vue @@ -71,7 +71,7 @@ export default { props: ['notification', 'canRemove'], methods: { action () { - this.$router.push({ name: 'achievements' }); + this.$router.push(`/profile/${this.$store.state.user.data._id}#achievements`); }, }, }; diff --git a/website/client/src/components/header/notifications/unallocatedStatsPoints.vue b/website/client/src/components/header/notifications/unallocatedStatsPoints.vue index 1fd8964fff..b15dc35c1c 100644 --- a/website/client/src/components/header/notifications/unallocatedStatsPoints.vue +++ b/website/client/src/components/header/notifications/unallocatedStatsPoints.vue @@ -43,7 +43,7 @@ export default { }, methods: { action () { - this.$router.push({ name: 'stats' }); + this.$router.push(`/profile/${this.$store.state.user.data._id}#stats`); }, }, }; diff --git a/website/client/src/components/header/userDropdown.vue b/website/client/src/components/header/userDropdown.vue index efaec0a534..ea565996f3 100644 --- a/website/client/src/components/header/userDropdown.vue +++ b/website/client/src/components/header/userDropdown.vue @@ -176,7 +176,12 @@ export default { } }, showProfile (startingPage) { - this.$router.push({ name: startingPage }); + const userId = this.$store.state.user.data._id; + let path = `/profile/${userId}`; + if (startingPage !== 'profile') { + path += `#${startingPage}`; + } + this.$router.push(path); }, toLearnMore () { this.$router.push({ name: 'subscription' }); diff --git a/website/client/src/components/userMenu/profile.vue b/website/client/src/components/userMenu/profile.vue index 09fd792e07..d42713d4a0 100644 --- a/website/client/src/components/userMenu/profile.vue +++ b/website/client/src/components/userMenu/profile.vue @@ -1216,17 +1216,10 @@ export default { }, selectPage (page) { this.selectedPage = page || 'profile'; - // Update URL based on whether viewing own profile or another user's profile - let newPath; - if (this.userId === this.userLoggedIn._id) { - // Own profile - use /user/profile, /user/stats, /user/achievements - newPath = `/user/${page}`; - } else { - // Other user's profile - use /profile/:userId#stats, /profile/:userId#achievements - newPath = `/profile/${this.userId}`; - if (page !== 'profile') { - newPath += `#${page}`; - } + const profileUserId = this.userId || this.userLoggedIn._id; + let newPath = `/profile/${profileUserId}`; + if (page !== 'profile') { + newPath += `#${page}`; } window.history.replaceState(null, null, newPath); this.$store.dispatch('common:setTitle', { diff --git a/website/client/src/router/index.js b/website/client/src/router/index.js index 4a262e625e..87e52aeb9e 100644 --- a/website/client/src/router/index.js +++ b/website/client/src/router/index.js @@ -359,11 +359,18 @@ router.beforeEach(async (to, from, next) => { } if ((to.name === 'stats' || to.name === 'achievements' || to.name === 'profile') && from.name !== null) { + const userId = store.state.user.data._id; + let redirectPath = `/profile/${userId}`; + if (to.name === 'stats') { + redirectPath += '#stats'; + } else if (to.name === 'achievements') { + redirectPath += '#achievements'; + } router.app.$emit('habitica:show-profile', { - userId: store.state.user.data._id, + userId, startingPage: to.name, fromPath: from.path, - toPath: to.path, + toPath: redirectPath, }); return null; } From b9764ddfcf5fa2b2ec3dff85482a5287b0efa70f Mon Sep 17 00:00:00 2001 From: Hafiz Date: Wed, 6 Aug 2025 13:35:43 -0500 Subject: [PATCH 12/13] lint error fixes --- .../src/components/challenges/closeChallengeModal.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/client/src/components/challenges/closeChallengeModal.vue b/website/client/src/components/challenges/closeChallengeModal.vue index 42caa3b457..77cece95ab 100644 --- a/website/client/src/components/challenges/closeChallengeModal.vue +++ b/website/client/src/components/challenges/closeChallengeModal.vue @@ -391,13 +391,13 @@ export default { this.filteredMembers = []; return; } - + const searchLower = this.searchTerm.toLowerCase().replace('@', ''); this.filteredMembers = this.members.filter(member => { const username = member.auth?.local?.username || ''; const displayName = member.profile?.name || ''; - return username.toLowerCase().includes(searchLower) || - displayName.toLowerCase().includes(searchLower); + return username.toLowerCase().includes(searchLower) + || displayName.toLowerCase().includes(searchLower); }).slice(0, 10); }, getMemberDisplayName (member) { From 5d1d70b72e31ee80f933a2657a22ac590c2d66e2 Mon Sep 17 00:00:00 2001 From: Hafiz Date: Wed, 6 Aug 2025 14:09:10 -0500 Subject: [PATCH 13/13] end challenge modal fixes --- .../challenges/closeChallengeModal.vue | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/website/client/src/components/challenges/closeChallengeModal.vue b/website/client/src/components/challenges/closeChallengeModal.vue index 77cece95ab..b59a54637e 100644 --- a/website/client/src/components/challenges/closeChallengeModal.vue +++ b/website/client/src/components/challenges/closeChallengeModal.vue @@ -4,7 +4,7 @@ id="close-challenge-modal" :title="$t('endChallenge')" size="md" - :hide-header="true" + :hide-header="false" >
{{ $t('awardWinners') }} @@ -97,7 +98,7 @@ class="gem-icon" v-html="icons.gem" >
- {{ prize }} {{ prize === 1 ? $t('gem') || 'Gem' : $t('gems') }} + {{ prize }} {{ prize === 1 ? $t('gem') : $t('gems') }}
@@ -266,6 +267,7 @@ width: 16px; height: 16px; display: inline-flex; + color: $white; } } @@ -280,6 +282,15 @@ box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12); transition: all 0.2s ease; + &:disabled { + opacity: 0.5; + cursor: not-allowed; + + &:hover { + background-color: $white; + } + } + &.has-winner { background-color: $purple-200; color: $white; @@ -290,7 +301,7 @@ } } - &:hover:not(.has-winner) { + &:hover:not(.has-winner):not(:disabled) { background-color: $gray-700; }