diff --git a/public/UITexts.example b/public/UITexts.example index 887390c..fe20439 100644 --- a/public/UITexts.example +++ b/public/UITexts.example @@ -20,26 +20,5 @@ "widget.room.chat.clear_history": "leeg geschiedenis", "widget.room.youtube.shared": "YouTube word gedeeld", "widget.room.youtube.open_video": "Open de video", - "wiredfurni.params.area_selection.selected": "Geselecteerd gebied: Lengte=%x%, Breedte=%y%, breedte=%w%, hoogte=%h%", - "wiredfurni.params.carry_mode": "Gli utenti sono trasportati quando:", - "wiredfurni.params.carry_mode.0": "rimani fermo direttamente sul Furni in movimento", - "wiredfurni.params.carry_mode.1": "rimani fermo sulla stessa casella del Furni in movimento", - "wiredfurni.params.sources.users.title.carry": "Consentire il trasporto per:", - "wiredfurni.params.sources.users.900": "Tutti gli utenti nella Stanza", - "wiredfurni.params.anim_time.title": "Durata animazione movimento", - "wiredfurni.params.anim_time.description": "Regola la velocita dello slide per i Wired che spostano utenti e furni.", - "wiredfurni.params.anim_time.value": "%ms% ms", - "wiredfurni.params.pickamount": "Seleziona %picks% effetti", - "wiredfurni.params.skipactions": "Evita gli effetti delle ultime %skips% esecuzioni.", - "wiredfurni.params.mov_no_animation.title": "Animazione movimento", - "wiredfurni.params.mov_no_animation.description": "Questo extra disattiva lo slide per i Wired che spostano utenti e furni.", - "wiredfurni.params.select_options": "Seleziona opzioni:", - "wiredfurni.params.movephysics.keep_altitude": "Mantieni altitudine del Furni", - "wiredfurni.params.movephysics.move_through_furni": "Muovi attraverso Furni", - "wiredfurni.params.movephysics.move_through_users": "Muovi attraverso utenti", - "wiredfurni.params.movephysics.block_by_furni": "รจ bloccato da Furni", - "wiredfurni.params.sources.furni.title.physics.0": "Muovi attraverso Furni:", - "wiredfurni.params.sources.furni.title.physics.1": "Bloccato da Furni:", - "wiredfurni.params.sources.users.title.physics.0": "Muovi attraverso gli utenti:", - "wiredfurni.params.sources.furni.900": "Tutti i Furni nella Stanza" + "wiredfurni.params.area_selection.selected": "Geselecteerd gebied: Lengte=%x%, Breedte=%y%, breedte=%w%, hoogte=%h%" } diff --git a/public/renderer-config.json b/public/renderer-config.json deleted file mode 100644 index b0beddf..0000000 --- a/public/renderer-config.json +++ /dev/null @@ -1,587 +0,0 @@ -{ - "socket.url": "ws://192.168.1.52:2096", - "asset.url": "https://client.paxxo.online/nitro/bundled", - "image.library.url": "https://client.paxxo.online/c_images/", - "hof.furni.url": "https://client.paxxo.online/c_images/dcr/hof_furni", - "images.url": "https://client.paxxo.online/nitro/images", - "gamedata.url": "https://client.paxxo.online/nitro/gamedata", - "sounds.url": "${asset.url}/sounds/%sample%.mp3", - "external.texts.url": [ - "${gamedata.url}/ExternalTexts.json", - "${gamedata.url}/UITexts.json" - ], - "external.samples.url": "${hof.furni.url}/mp3/sound_machine_sample_%sample%.mp3", - "furnidata.url": "${gamedata.url}/FurnitureData.json?v=1", - "productdata.url": "${gamedata.url}/ProductData.json?v=1", - "avatar.actions.url": "${gamedata.url}/HabboAvatarActions.json?v=1", - "avatar.figuredata.url": "${gamedata.url}/FigureData.json?v=1", - "avatar.figuremap.url": "${gamedata.url}/FigureMap.json?v=1", - "avatar.effectmap.url": "${gamedata.url}/EffectMap.json?v=1", - "avatar.asset.url": "${asset.url}/figure/%libname%.nitro", - "avatar.asset.effect.url": "${asset.url}/effect/%libname%.nitro", - "furni.asset.url": "${asset.url}/furniture/%libname%.nitro", - "furni.asset.icon.url": "${hof.furni.url}/icons/%libname%%param%_icon.png", - "pet.asset.url": "${asset.url}/pets/%libname%.nitro", - "generic.asset.url": "${asset.url}/generic/%libname%.nitro", - "badge.asset.url": "${image.library.url}album1584/%badgename%.gif", - "furni.rotation.bounce.steps": 20, - "furni.rotation.bounce.height": 0.0625, - "enable.avatar.arrow": false, - "system.log.debug": true, - "system.log.warn": true, - "system.log.error": true, - "system.log.events": false, - "system.log.packets": true, - "system.fps.animation": 24, - "system.fps.max": 60, - "system.pong.manually": true, - "system.pong.interval.ms": 20000, - "room.color.skip.transition": true, - "room.landscapes.enabled": true, - "room.zoom.enabled": true, - "avatar.mandatory.libraries": [ - "bd:1", - "li:0" - ], - "avatar.mandatory.effect.libraries": [ - "dance.1", - "dance.2", - "dance.3", - "dance.4" - ], - "avatar.default.figuredata": { - "palettes": [ - { - "id": 1, - "colors": [ - { - "id": 99999, - "index": 1001, - "club": 0, - "selectable": false, - "hexCode": "DDDDDD" - }, - { - "id": 99998, - "index": 1001, - "club": 0, - "selectable": false, - "hexCode": "FAFAFA" - } - ] - }, - { - "id": 3, - "colors": [ - { - "id": 10001, - "index": 1001, - "club": 0, - "selectable": false, - "hexCode": "EEEEEE" - }, - { - "id": 10002, - "index": 1002, - "club": 0, - "selectable": false, - "hexCode": "FA3831" - }, - { - "id": 10003, - "index": 1003, - "club": 0, - "selectable": false, - "hexCode": "FD92A0" - }, - { - "id": 10004, - "index": 1004, - "club": 0, - "selectable": false, - "hexCode": "2AC7D2" - }, - { - "id": 10005, - "index": 1005, - "club": 0, - "selectable": false, - "hexCode": "35332C" - }, - { - "id": 10006, - "index": 1006, - "club": 0, - "selectable": false, - "hexCode": "EFFF92" - }, - { - "id": 10007, - "index": 1007, - "club": 0, - "selectable": false, - "hexCode": "C6FF98" - }, - { - "id": 10008, - "index": 1008, - "club": 0, - "selectable": false, - "hexCode": "FF925A" - }, - { - "id": 10009, - "index": 1009, - "club": 0, - "selectable": false, - "hexCode": "9D597E" - }, - { - "id": 10010, - "index": 1010, - "club": 0, - "selectable": false, - "hexCode": "B6F3FF" - }, - { - "id": 10011, - "index": 1011, - "club": 0, - "selectable": false, - "hexCode": "6DFF33" - }, - { - "id": 10012, - "index": 1012, - "club": 0, - "selectable": false, - "hexCode": "3378C9" - }, - { - "id": 10013, - "index": 1013, - "club": 0, - "selectable": false, - "hexCode": "FFB631" - }, - { - "id": 10014, - "index": 1014, - "club": 0, - "selectable": false, - "hexCode": "DFA1E9" - }, - { - "id": 10015, - "index": 1015, - "club": 0, - "selectable": false, - "hexCode": "F9FB32" - }, - { - "id": 10016, - "index": 1016, - "club": 0, - "selectable": false, - "hexCode": "CAAF8F" - }, - { - "id": 10017, - "index": 1017, - "club": 0, - "selectable": false, - "hexCode": "C5C6C5" - }, - { - "id": 10018, - "index": 1018, - "club": 0, - "selectable": false, - "hexCode": "47623D" - }, - { - "id": 10019, - "index": 1019, - "club": 0, - "selectable": false, - "hexCode": "8A8361" - }, - { - "id": 10020, - "index": 1020, - "club": 0, - "selectable": false, - "hexCode": "FF8C33" - }, - { - "id": 10021, - "index": 1021, - "club": 0, - "selectable": false, - "hexCode": "54C627" - }, - { - "id": 10022, - "index": 1022, - "club": 0, - "selectable": false, - "hexCode": "1E6C99" - }, - { - "id": 10023, - "index": 1023, - "club": 0, - "selectable": false, - "hexCode": "984F88" - }, - { - "id": 10024, - "index": 1024, - "club": 0, - "selectable": false, - "hexCode": "77C8FF" - }, - { - "id": 10025, - "index": 1025, - "club": 0, - "selectable": false, - "hexCode": "FFC08E" - }, - { - "id": 10026, - "index": 1026, - "club": 0, - "selectable": false, - "hexCode": "3C4B87" - }, - { - "id": 10027, - "index": 1027, - "club": 0, - "selectable": false, - "hexCode": "7C2C47" - }, - { - "id": 10028, - "index": 1028, - "club": 0, - "selectable": false, - "hexCode": "D7FFE3" - }, - { - "id": 10029, - "index": 1029, - "club": 0, - "selectable": false, - "hexCode": "8F3F1C" - }, - { - "id": 10030, - "index": 1030, - "club": 0, - "selectable": false, - "hexCode": "FF6393" - }, - { - "id": 10031, - "index": 1031, - "club": 0, - "selectable": false, - "hexCode": "1F9B79" - }, - { - "id": 10032, - "index": 1032, - "club": 0, - "selectable": false, - "hexCode": "FDFF33" - } - ] - } - ], - "setTypes": [ - { - "type": "hd", - "paletteId": 1, - "mandatory_f_0": true, - "mandatory_f_1": true, - "mandatory_m_0": true, - "mandatory_m_1": true, - "sets": [ - { - "id": 99999, - "gender": "U", - "club": 0, - "colorable": true, - "selectable": false, - "preselectable": false, - "sellable": false, - "parts": [ - { - "id": 1, - "type": "bd", - "colorable": true, - "index": 0, - "colorindex": 1 - }, - { - "id": 1, - "type": "hd", - "colorable": true, - "index": 0, - "colorindex": 1 - }, - { - "id": 1, - "type": "lh", - "colorable": true, - "index": 0, - "colorindex": 1 - }, - { - "id": 1, - "type": "rh", - "colorable": true, - "index": 0, - "colorindex": 1 - } - ] - } - ] - }, - { - "type": "bds", - "paletteId": 1, - "mandatory_f_0": false, - "mandatory_f_1": false, - "mandatory_m_0": false, - "mandatory_m_1": false, - "sets": [ - { - "id": 10001, - "gender": "U", - "club": 0, - "colorable": true, - "selectable": false, - "preselectable": false, - "sellable": false, - "parts": [ - { - "id": 10001, - "type": "bds", - "colorable": true, - "index": 0, - "colorindex": 1 - }, - { - "id": 10001, - "type": "lhs", - "colorable": true, - "index": 0, - "colorindex": 1 - }, - { - "id": 10001, - "type": "rhs", - "colorable": true, - "index": 0, - "colorindex": 1 - } - ], - "hiddenLayers": [ - { - "partType": "bd" - }, - { - "partType": "rh" - }, - { - "partType": "lh" - } - ] - } - ] - }, - { - "type": "ss", - "paletteId": 3, - "mandatory_f_0": false, - "mandatory_f_1": false, - "mandatory_m_0": false, - "mandatory_m_1": false, - "sets": [ - { - "id": 10010, - "gender": "F", - "club": 0, - "colorable": true, - "selectable": false, - "preselectable": false, - "sellable": false, - "parts": [ - { - "id": 10001, - "type": "ss", - "colorable": true, - "index": 0, - "colorindex": 1 - } - ], - "hiddenLayers": [ - { - "partType": "ch" - }, - { - "partType": "lg" - }, - { - "partType": "ca" - }, - { - "partType": "wa" - }, - { - "partType": "sh" - }, - { - "partType": "ls" - }, - { - "partType": "rs" - }, - { - "partType": "lc" - }, - { - "partType": "rc" - }, - { - "partType": "cc" - }, - { - "partType": "cp" - } - ] - }, - { - "id": 10011, - "gender": "M", - "club": 0, - "colorable": true, - "selectable": false, - "preselectable": false, - "sellable": false, - "parts": [ - { - "id": 10002, - "type": "ss", - "colorable": true, - "index": 0, - "colorindex": 1 - } - ], - "hiddenLayers": [ - { - "partType": "ch" - }, - { - "partType": "lg" - }, - { - "partType": "ca" - }, - { - "partType": "wa" - }, - { - "partType": "sh" - }, - { - "partType": "ls" - }, - { - "partType": "rs" - }, - { - "partType": "lc" - }, - { - "partType": "rc" - }, - { - "partType": "cc" - }, - { - "partType": "cp" - } - ] - } - ] - } - ] - }, - "avatar.default.actions": { - "actions": [ - { - "id": "Default", - "state": "std", - "precedence": 1000, - "main": true, - "isDefault": true, - "geometryType": "vertical", - "activePartSet": "figure", - "assetPartDefinition": "std" - } - ] - }, - "pet.types": [ - "dog", - "cat", - "croco", - "terrier", - "bear", - "pig", - "lion", - "rhino", - "spider", - "turtle", - "chicken", - "frog", - "dragon", - "monster", - "monkey", - "horse", - "monsterplant", - "bunnyeaster", - "bunnyevil", - "bunnydepressed", - "bunnylove", - "pigeongood", - "pigeonevil", - "demonmonkey", - "bearbaby", - "terrierbaby", - "gnome", - "gnome", - "kittenbaby", - "puppybaby", - "pigletbaby", - "haloompa", - "fools", - "pterosaur", - "velociraptor", - "cow", - "LeetPen", - "bbwibb", - "elephants" - ], - "preload.assets.urls": [ - "${asset.url}/generic/avatar_additions.nitro", - "${asset.url}/generic/group_badge.nitro", - "${asset.url}/generic/floor_editor.nitro", - "${images.url}/loading_icon.png", - "${images.url}/clear_icon.png", - "${images.url}/big_arrow.png" - ] -} \ No newline at end of file diff --git a/public/ui-config.example b/public/ui-config.example index 08fce1f..19c74ac 100644 --- a/public/ui-config.example +++ b/public/ui-config.example @@ -26,6 +26,7 @@ "game.center.enabled": false, "guides.enabled": true, "toolbar.hide.quests": true, + "catalog.style.new": true, "navigator.room.models": [{ "clubLevel": 0, "tileSize": 104, diff --git a/public/ui-config.json b/public/ui-config.json deleted file mode 100644 index ac761b3..0000000 --- a/public/ui-config.json +++ /dev/null @@ -1,1326 +0,0 @@ -{ - "image.library.notifications.url": "${image.library.url}notifications/%image%.png", - "achievements.images.url": "${image.library.url}Quests/%image%.png", - "camera.url": "http://192.168.0.8/camera/photo", - "thumbnails.url": "http://192.168.0.8/camera/photo/thumb/%thumbnail%.png", - "url.prefix": "", - "habbopages.url": "/gamedata/habbopages/", - "group.homepage.url": "${url.prefix}/groups/%groupid%/id", - "guide.help.alpha.groupid": 0, - "chat.viewer.height.percentage": 0.4, - "widget.dimmer.colorwheel": false, - "avatar.wardrobe.max.slots": 10, - "user.badges.max.slots": 5, - "user.tags.enabled": false, - "camera.publish.disabled": false, - "hc.disabled": false, - "badge.descriptions.enabled": true, - "motto.max.length": 38, - "bot.name.max.length": 15, - "pet.package.name.max.length": 15, - "wired.action.bot.talk.to.avatar.max.length": 64, - "wired.action.bot.talk.max.length": 64, - "wired.action.chat.max.length": 100, - "wired.action.kick.from.room.max.length": 100, - "wired.action.mute.user.max.length": 100, - "game.center.enabled": false, - "guides.enabled": true, - "toolbar.hide.quests": true, - "navigator.room.models": [{ - "clubLevel": 0, - "tileSize": 104, - "name": "a" - }, { - "clubLevel": 0, - "tileSize": 94, - "name": "b" - }, { - "clubLevel": 0, - "tileSize": 36, - "name": "c" - }, { - "clubLevel": 0, - "tileSize": 84, - "name": "d" - }, { - "clubLevel": 0, - "tileSize": 80, - "name": "e" - }, { - "clubLevel": 0, - "tileSize": 80, - "name": "f" - }, { - "clubLevel": 0, - "tileSize": 416, - "name": "i" - }, { - "clubLevel": 0, - "tileSize": 320, - "name": "j" - }, { - "clubLevel": 0, - "tileSize": 448, - "name": "k" - }, { - "clubLevel": 0, - "tileSize": 352, - "name": "l" - }, { - "clubLevel": 0, - "tileSize": 384, - "name": "m" - }, { - "clubLevel": 0, - "tileSize": 372, - "name": "n" - }, { - "clubLevel": 1, - "tileSize": 80, - "name": "g" - }, { - "clubLevel": 1, - "tileSize": 74, - "name": "h" - }, { - "clubLevel": 1, - "tileSize": 416, - "name": "o" - }, { - "clubLevel": 1, - "tileSize": 352, - "name": "p" - }, { - "clubLevel": 1, - "tileSize": 304, - "name": "q" - }, { - "clubLevel": 1, - "tileSize": 336, - "name": "r" - }, { - "clubLevel": 1, - "tileSize": 748, - "name": "u" - }, { - "clubLevel": 1, - "tileSize": 438, - "name": "v" - }, { - "clubLevel": 2, - "tileSize": 540, - "name": "t" - }, { - "clubLevel": 2, - "tileSize": 512, - "name": "w" - }, { - "clubLevel": 2, - "tileSize": 396, - "name": "x" - }, { - "clubLevel": 2, - "tileSize": 440, - "name": "y" - }, { - "clubLevel": 2, - "tileSize": 456, - "name": "z" - }, { - "clubLevel": 2, - "tileSize": 208, - "name": "0" - }, { - "clubLevel": 2, - "tileSize": 1009, - "name": "1" - }, { - "clubLevel": 2, - "tileSize": 1044, - "name": "2" - }, { - "clubLevel": 2, - "tileSize": 183, - "name": "3" - }, { - "clubLevel": 2, - "tileSize": 254, - "name": "4" - }, { - "clubLevel": 2, - "tileSize": 1024, - "name": "5" - }, { - "clubLevel": 2, - "tileSize": 801, - "name": "6" - }, { - "clubLevel": 2, - "tileSize": 354, - "name": "7" - }, { - "clubLevel": 2, - "tileSize": 888, - "name": "8" - }, { - "clubLevel": 2, - "tileSize": 926, - "name": "9" - } - ], - "hotelview": { - "room.pool": "791", - "room.picnic": "2193", - "room.rooftop": "", - "room.rooftop.pool": "", - "room.peaceful": "", - "room.infobus": "5956", - "room.lobby": "1450", - "show.avatar": true, - "widgets": { - "slot.1.widget": "", - "slot.1.conf": {}, - "slot.2.widget": "", - "slot.2.conf": { - "image": "", - "texts": "", - "btnLink": "" - }, - "slot.3.widget": "", - "slot.3.conf": {}, - "slot.4.widget": "", - "slot.4.conf": {}, - "slot.5.widget": "", - "slot.5.conf": {}, - "slot.6.widget": "", - "slot.6.conf": { - "campaign": "" - }, - "slot.7.widget": "", - "slot.7.conf": {} - }, - "images": { - "background": "${asset.url}/images/reception/stretch_blue.png", - "background.colour": "#8ee0ff", - "sun": "${asset.url}/images/reception/sun.png", - "drape": "${asset.url}/images/reception/drape.png", - "left": "", - "right": "", - "right.repeat": "" - } - }, - "achievements.unseen.ignored": [ - "ACH_AllTimeHotelPresence" - ], - "avatareditor.show.clubitems.dimmed": true, - "avatareditor.show.clubitems.first": true, - "chat.history.max.items": 100, - "system.currency.types": [ - -1, - 0, - 5 - ], - "catalog.links": { - "hc.buy_hc": "habbo_club", - "hc.hc_gifts": "club_gifts", - "pets.buy_food": "pet_food", - "pets.buy_saddle": "saddles" - }, - "hc.center": { - "benefits.info": true, - "payday.info": true, - "gift.info": true, - "benefits.habbopage": "habboclub", - "payday.habbopage": "hcpayday" - }, - "respect.options": { - "enabled": false, - "sound": "sound_respect_received" - }, - "currency.display.number.short": false, - "currency.asset.icon.url": "${images.url}/wallet/%type%.png", - "catalog.asset.url": "${image.library.url}catalogue", - "catalog.asset.image.url": "${catalog.asset.url}/%name%.gif", - "catalog.asset.icon.url": "${catalog.asset.url}/icon_%name%.png", - "catalog.tab.icons": false, - "catalog.headers": false, - "chat.input.maxlength": 100, - "chat.styles.disabled": [], - "chat.styles": [{ - "styleId": 0, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 1, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 2, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 3, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 4, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 5, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 6, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 7, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 8, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 9, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 10, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 11, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 12, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 13, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 14, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 15, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 16, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 17, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 18, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 19, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 20, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 21, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 22, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 23, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 24, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 25, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 26, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 27, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 28, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 29, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 30, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 31, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 32, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 33, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 34, - "minRank": 5, - "isSystemStyle": true, - "isHcOnly": false, - "isAmbassadorOnly": false - }, { - "styleId": 35, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 36, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 37, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 38, - "minRank": 0, - "isSystemStyle": false, - "isHcOnly": true, - "isAmbassadorOnly": false - }, { - "styleId": 39, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 40, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 41, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 42, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 43, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 44, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 45, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 46, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 47, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 48, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 49, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 50, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 51, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 52, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - }, { - "styleId": 53, - "minRank": 5, - "isSystemStyle": false, - "isHcOnly": false, - "isAmbassadorOnly": true - } - ], - "camera.available.effects": [{ - "name": "dark_sepia", - "colorMatrix": [ - 0.4, - 0.4, - 0.1, - 0, - 110, - 0.3, - 0.4, - 0.1, - 0, - 30, - 0.3, - 0.2, - 0.1, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 0, - "enabled": true - }, { - "name": "increase_saturation", - "colorMatrix": [ - 2, - -0.5, - -0.5, - 0, - 0, - -0.5, - 2, - -0.5, - 0, - 0, - -0.5, - -0.5, - 2, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 0, - "enabled": true - }, { - "name": "increase_contrast", - "colorMatrix": [ - 1.5, - 0, - 0, - 0, - -50, - 0, - 1.5, - 0, - 0, - -50, - 0, - 0, - 1.5, - 0, - -50, - 0, - 0, - 0, - 1.5, - 0 - ], - "minLevel": 0, - "enabled": true - }, { - "name": "shadow_multiply_02", - "colorMatrix": [], - "minLevel": 0, - "blendMode": 2, - "enabled": true - }, { - "name": "color_1", - "colorMatrix": [ - 0.393, - 0.769, - 0.189, - 0, - 0, - 0.349, - 0.686, - 0.168, - 0, - 0, - 0.272, - 0.534, - 0.131, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 1, - "enabled": true - }, { - "name": "hue_bright_sat", - "colorMatrix": [ - 1, - 0.6, - 0.2, - 0, - -50, - 0.2, - 1, - 0.6, - 0, - -50, - 0.6, - 0.2, - 1, - 0, - -50, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 1, - "enabled": true - }, { - "name": "hearts_hardlight_02", - "colorMatrix": [], - "minLevel": 1, - "blendMode": 9, - "enabled": true - }, { - "name": "texture_overlay", - "colorMatrix": [], - "minLevel": 1, - "blendMode": 4, - "enabled": true - }, { - "name": "pinky_nrm", - "colorMatrix": [], - "minLevel": 1, - "blendMode": 0, - "enabled": true - }, { - "name": "color_2", - "colorMatrix": [ - 0.333, - 0.333, - 0.333, - 0, - 0, - 0.333, - 0.333, - 0.333, - 0, - 0, - 0.333, - 0.333, - 0.333, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 2, - "enabled": true - }, { - "name": "night_vision", - "colorMatrix": [ - 0, - 0, - 0, - 0, - 0, - 0, - 1.1, - 0, - 0, - -50, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 2, - "enabled": true - }, { - "name": "stars_hardlight_02", - "colorMatrix": [], - "minLevel": 2, - "blendMode": 9, - "enabled": true - }, { - "name": "coffee_mpl", - "colorMatrix": [], - "minLevel": 2, - "blendMode": 2, - "enabled": true - }, { - "name": "security_hardlight", - "colorMatrix": [], - "minLevel": 3, - "blendMode": 9, - "enabled": true - }, { - "name": "bluemood_mpl", - "colorMatrix": [], - "minLevel": 3, - "blendMode": 2, - "enabled": true - }, { - "name": "rusty_mpl", - "colorMatrix": [], - "minLevel": 3, - "blendMode": 2, - "enabled": true - }, { - "name": "decr_conrast", - "colorMatrix": [ - 0.5, - 0, - 0, - 0, - 50, - 0, - 0.5, - 0, - 0, - 50, - 0, - 0, - 0.5, - 0, - 50, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 4, - "enabled": true - }, { - "name": "green_2", - "colorMatrix": [ - 0.5, - 0.5, - 0.5, - 0, - 0, - 0.5, - 0.5, - 0.5, - 0, - 90, - 0.5, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 4, - "enabled": true - }, { - "name": "alien_hrd", - "colorMatrix": [], - "minLevel": 4, - "blendMode": 9, - "enabled": true - }, { - "name": "color_3", - "colorMatrix": [ - 0.609, - 0.609, - 0.082, - 0, - 0, - 0.309, - 0.609, - 0.082, - 0, - 0, - 0.309, - 0.609, - 0.082, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 5, - "enabled": true - }, { - "name": "color_4", - "colorMatrix": [ - 0.8, - -0.8, - 1, - 0, - 70, - 0.8, - -0.8, - 1, - 0, - 70, - 0.8, - -0.8, - 1, - 0, - 70, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 5, - "enabled": true - }, { - "name": "toxic_hrd", - "colorMatrix": [], - "minLevel": 5, - "blendMode": 9, - "enabled": true - }, { - "name": "hypersaturated", - "colorMatrix": [ - 2, - -1, - 0, - 0, - 0, - -1, - 2, - 0, - 0, - 0, - 0, - -1, - 2, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 6, - "enabled": true - }, { - "name": "Yellow", - "colorMatrix": [ - 1, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 6, - "enabled": true - }, { - "name": "misty_hrd", - "colorMatrix": [], - "minLevel": 6, - "blendMode": 9, - "enabled": true - }, { - "name": "x_ray", - "colorMatrix": [ - 0, - 1.2, - 0, - 0, - -100, - 0, - 2, - 0, - 0, - -120, - 0, - 2, - 0, - 0, - -120, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 7, - "enabled": true - }, { - "name": "decrease_saturation", - "colorMatrix": [ - 0.7, - 0.2, - 0.2, - 0, - 0, - 0.2, - 0.7, - 0.2, - 0, - 0, - 0.2, - 0.2, - 0.7, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 7, - "enabled": true - }, { - "name": "drops_mpl", - "colorMatrix": [], - "minLevel": 8, - "blendMode": 2, - "enabled": true - }, { - "name": "shiny_hrd", - "colorMatrix": [], - "minLevel": 9, - "blendMode": 9, - "enabled": true - }, { - "name": "glitter_hrd", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 9, - "enabled": true - }, { - "name": "frame_gold", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "frame_gray_4", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "frame_black_2", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "frame_wood_2", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "finger_nrm", - "colorMatrix": [], - "minLevel": 10, - "blendMode": 0, - "enabled": true - }, { - "name": "color_5", - "colorMatrix": [ - 3.309, - 0.609, - 1.082, - 0.2, - 0, - 0.309, - 0.609, - 0.082, - 0, - 0, - 1.309, - 0.609, - 0.082, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - }, { - "name": "black_white_negative", - "colorMatrix": [ - -0.5, - -0.5, - -0.5, - 0, - 0, - -0.5, - -0.5, - -0.5, - 0, - 0, - -0.5, - -0.5, - -0.5, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - }, { - "name": "blue", - "colorMatrix": [ - 0.5, - 0.5, - 0.5, - 0, - -255, - 0.5, - 0.5, - 0.5, - 0, - -170, - 0.5, - 0.5, - 0.5, - 0, - 0, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - }, { - "name": "red", - "colorMatrix": [ - 0.5, - 0.5, - 0.5, - 0, - 0, - 0.5, - 0.5, - 0.5, - 0, - -170, - 0.5, - 0.5, - 0.5, - 0, - -170, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - }, { - "name": "green", - "colorMatrix": [ - 0.5, - 0.5, - 0.5, - 0, - -170, - 0.5, - 0.5, - 0.5, - 0, - 0, - 0.5, - 0.5, - 0.5, - 0, - -170, - 0, - 0, - 0, - 1, - 0 - ], - "minLevel": 10, - "enabled": true - } - ], - "notification": { - "notification.admin.transient": { - "display": "POP_UP", - "image": "${image.library.url}/album1358/frank_wave_001.gif" - }, - "notification.builders_club.membership_expired": { - "display": "POP_UP" - }, - "notification.builders_club.membership_expires": { - "display": "POP_UP", - "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" - }, - "notification.builders_club.membership_extended": { - "delivery": "PERSISTENT", - "display": "POP_UP" - }, - "notification.builders_club.membership_made": { - "delivery": "PERSISTENT", - "display": "POP_UP", - "image": "${image.library.url}/notifications/builders_club_membership_extended.png" - }, - "notification.builders_club.membership_renewed": { - "delivery": "PERSISTENT", - "display": "POP_UP", - "image": "${image.library.url}/notifications/builders_club_membership_extended.png" - }, - "notification.builders_club.room_locked": { - "display": "BUBBLE", - "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" - }, - "notification.builders_club.room_unlocked": { - "display": "BUBBLE" - }, - "notification.builders_club.visit_denied_for_owner": { - "display": "BUBBLE", - "image": "${image.library.url}/notifications/builders_club_room_locked_small.png" - }, - "notification.builders_club.visit_denied_for_visitor": { - "display": "POP_UP", - "image": "${image.library.url}/notifications/builders_club_room_locked.png" - }, - "notification.campaign.credit.donation": { - "display": "BUBBLE" - }, - "notification.campaign.product.donation": { - "display": "BUBBLE" - }, - "notification.casino.too_many_dice.placement": { - "display": "POP_UP" - }, - "notification.casino.too_many_dice": { - "display": "POP_UP" - }, - "notification.cfh.created": { - "display": "POP_UP", - "title": "" - }, - "notification.feed.enabled": false, - "notification.floorplan_editor.error": { - "display": "POP_UP" - }, - "notification.forums.delivered": { - "delivery": "PERSISTENT", - "display": "POP_UP" - }, - "notification.forums.forum_settings_updated": { - "display": "BUBBLE" - }, - "notification.forums.message.hidden": { - "display": "BUBBLE" - }, - "notification.forums.message.restored": { - "display": "BUBBLE" - }, - "notification.forums.thread.hidden": { - "display": "BUBBLE" - }, - "notification.forums.thread.locked": { - "display": "BUBBLE" - }, - "notification.forums.thread.pinned": { - "display": "BUBBLE" - }, - "notification.forums.thread.restored": { - "display": "BUBBLE" - }, - "notification.forums.thread.unlocked": { - "display": "BUBBLE" - }, - "notification.forums.thread.unpinned": { - "display": "BUBBLE" - }, - "notification.furni_placement_error": { - "display": "BUBBLE" - }, - "notification.gifting.valentine": { - "delivery": "PERSISTENT", - "display": "BUBBLE", - "image": "${image.library.url}/notifications/polaroid_photo.png" - }, - "notification.items.enabled": true, - "notification.mute.forbidden.time": { - "display": "BUBBLE" - }, - "notification.npc.gift.received": { - "display": "BUBBLE", - "image": "${image.library.url}/album1584/X1517.gif" - } - } -} \ No newline at end of file diff --git a/src/api/ui-settings/IUiSettings.ts b/src/api/ui-settings/IUiSettings.ts new file mode 100644 index 0000000..5c6e96d --- /dev/null +++ b/src/api/ui-settings/IUiSettings.ts @@ -0,0 +1,43 @@ +export interface IUiSettings +{ + colorMode: 'color' | 'image' | 'default'; + headerColor: string; + headerImageUrl: string; + headerAlpha: number; +} + +export const DEFAULT_UI_SETTINGS: IUiSettings = { + colorMode: 'default', + headerColor: '#1E7295', + headerImageUrl: '', + headerAlpha: 100 +}; + +export const PRESET_COLORS: string[] = [ + '#000000', '#444444', '#888888', '#CCCCCC', '#660000', '#CC3333', '#FF6666', '#CC6600', + '#FF3333', '#FF6633', '#FF9933', '#FFCC00', '#FFFF00', '#66FF00', '#00CC00', '#009900', + '#00FFCC', '#33CCFF', '#3366FF', '#0000CC', '#6633CC', '#9933FF', '#CC33FF', '#FF66CC', + '#FF99CC', '#1E7295', '#185D79', '#2DABC2', '#2B91A7', '#283F5D' +]; + +export interface IThemePreset +{ + name: string; + color: string; + alpha: number; +} + +export const THEME_PRESETS: IThemePreset[] = [ + { name: 'default', color: '#1E7295', alpha: 100 }, + { name: 'ocean', color: '#0077B6', alpha: 100 }, + { name: 'forest', color: '#2D6A4F', alpha: 100 }, + { name: 'sunset', color: '#E76F51', alpha: 100 }, + { name: 'royal', color: '#7B2CBF', alpha: 100 }, + { name: 'midnight', color: '#1B1B2F', alpha: 100 }, + { name: 'cherry', color: '#C1121F', alpha: 100 }, + { name: 'gold', color: '#B8860B', alpha: 100 }, + { name: 'slate', color: '#475569', alpha: 100 }, + { name: 'candy', color: '#FF69B4', alpha: 100 }, + { name: 'emerald', color: '#059669', alpha: 100 }, + { name: 'volcano', color: '#DC2626', alpha: 90 }, +]; diff --git a/src/api/ui-settings/UiSettingsContext.tsx b/src/api/ui-settings/UiSettingsContext.tsx new file mode 100644 index 0000000..99685ce --- /dev/null +++ b/src/api/ui-settings/UiSettingsContext.tsx @@ -0,0 +1,224 @@ +import { GetCommunication, UiSettingsDataEvent, UiSettingsLoadComposer, UiSettingsSaveComposer } from '@nitrots/nitro-renderer'; +import { createContext, FC, PropsWithChildren, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { DEFAULT_UI_SETTINGS, IUiSettings } from './IUiSettings'; + +const STORAGE_KEY = 'nitro.ui.settings'; + +interface IUiSettingsContext +{ + settings: IUiSettings; + isCustomActive: boolean; + updateSettings: (partial: Partial) => void; + resetSettings: () => void; + getHeaderStyle: () => React.CSSProperties; + getTabsStyle: () => React.CSSProperties; + getAccentColor: () => string; +} + +const UiSettingsContext = createContext({ + settings: DEFAULT_UI_SETTINGS, + isCustomActive: false, + updateSettings: () => {}, + resetSettings: () => {}, + getHeaderStyle: () => ({}), + getTabsStyle: () => ({}), + getAccentColor: () => DEFAULT_UI_SETTINGS.headerColor +}); + +const darkenColor = (hex: string, amount: number): string => +{ + const num = parseInt(hex.replace('#', ''), 16); + const r = Math.max(0, ((num >> 16) & 0xFF) - amount); + const g = Math.max(0, ((num >> 8) & 0xFF) - amount); + const b = Math.max(0, (num & 0xFF) - amount); + + return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1); +}; + +const loadSettings = (): IUiSettings => +{ + try + { + const stored = localStorage.getItem(STORAGE_KEY); + if(stored) return { ...DEFAULT_UI_SETTINGS, ...JSON.parse(stored) }; + } + catch(e) {} + + return { ...DEFAULT_UI_SETTINGS }; +}; + +const saveSettings = (settings: IUiSettings): void => +{ + try + { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + } + catch(e) {} +}; + +const sendComposer = (composer: any): void => +{ + try + { + GetCommunication()?.connection?.send(composer); + } + catch(e) {} +}; + +export const UiSettingsProvider: FC = ({ children }) => +{ + const [ settings, setSettings ] = useState(loadSettings); + const serverSaveTimerRef = useRef>(null); + + // Carica dal server al mount e ascolta risposta + useEffect(() => + { + sendComposer(new UiSettingsLoadComposer()); + + const connection = GetCommunication()?.connection; + + if(!connection) return; + + const handler = (event: any) => + { + try + { + const parser = event.getParser(); + const json = parser?.settingsJson; + + if(json && json !== '{}') + { + const serverSettings = { ...DEFAULT_UI_SETTINGS, ...JSON.parse(json) }; + setSettings(serverSettings); + saveSettings(serverSettings); + } + } + catch(e) {} + }; + + connection.addMessageEvent(new UiSettingsDataEvent(handler)); + }, []); + + const syncToServer = useCallback((settingsToSave: IUiSettings) => + { + if(serverSaveTimerRef.current) clearTimeout(serverSaveTimerRef.current); + + serverSaveTimerRef.current = setTimeout(() => + { + sendComposer(new UiSettingsSaveComposer(JSON.stringify(settingsToSave))); + }, 1000); + }, []); + + const updateSettings = useCallback((partial: Partial) => + { + setSettings(prev => + { + const updated = { ...prev, ...partial }; + saveSettings(updated); + syncToServer(updated); + + return updated; + }); + }, [ syncToServer ]); + + const resetSettings = useCallback(() => + { + setSettings({ ...DEFAULT_UI_SETTINGS }); + saveSettings(DEFAULT_UI_SETTINGS); + syncToServer(DEFAULT_UI_SETTINGS); + }, [ syncToServer ]); + + const getHeaderStyle = useCallback((): React.CSSProperties => + { + if(settings.colorMode === 'color') + { + const alpha = (settings.headerAlpha ?? 100) / 100; + const num = parseInt(settings.headerColor.replace('#', ''), 16); + const r = (num >> 16) & 0xFF; + const g = (num >> 8) & 0xFF; + const b = num & 0xFF; + + return { backgroundColor: `rgba(${ r }, ${ g }, ${ b }, ${ alpha })` }; + } + + if(settings.colorMode === 'image' && settings.headerImageUrl) + { + return { + backgroundImage: `url(${ settings.headerImageUrl })`, + backgroundSize: 'cover', + backgroundPosition: 'center', + backgroundRepeat: 'repeat' + }; + } + + return {}; + }, [ settings ]); + + const getTabsStyle = useCallback((): React.CSSProperties => + { + if(settings.colorMode === 'color') + { + return { backgroundColor: darkenColor(settings.headerColor, 30) }; + } + + if(settings.colorMode === 'image' && settings.headerImageUrl) + { + return { + backgroundImage: `url(${ settings.headerImageUrl })`, + backgroundSize: 'cover', + backgroundPosition: 'center bottom', + backgroundRepeat: 'repeat' + }; + } + + return {}; + }, [ settings ]); + + const getAccentColor = useCallback((): string => + { + if(settings.colorMode === 'color') return settings.headerColor; + + return DEFAULT_UI_SETTINGS.headerColor; + }, [ settings ]); + + const isCustomActive = settings.colorMode !== 'default'; + + const ALL_CSS_VARS = [ + '--ui-accent-color', '--ui-accent-dark', + '--ui-ctx-bg', '--ui-ctx-header-bg', '--ui-ctx-item-bg1', '--ui-ctx-item-bg2', + '--ui-btn-primary-bg', '--ui-btn-primary-border', + '--ui-dark-bg', '--ui-dark-border' + ]; + + useEffect(() => + { + const root = document.documentElement; + + if(settings.colorMode === 'color') + { + const c = settings.headerColor; + root.style.setProperty('--ui-accent-color', c); + root.style.setProperty('--ui-accent-dark', darkenColor(c, 30)); + root.style.setProperty('--ui-ctx-bg', darkenColor(c, 50)); + root.style.setProperty('--ui-ctx-header-bg', darkenColor(c, 20)); + root.style.setProperty('--ui-ctx-item-bg1', darkenColor(c, 60)); + root.style.setProperty('--ui-ctx-item-bg2', darkenColor(c, 70)); + root.style.setProperty('--ui-btn-primary-bg', c); + root.style.setProperty('--ui-btn-primary-border', darkenColor(c, 20)); + root.style.setProperty('--ui-dark-bg', darkenColor(c, 55)); + root.style.setProperty('--ui-dark-border', darkenColor(c, 60)); + } + else + { + ALL_CSS_VARS.forEach(v => root.style.removeProperty(v)); + } + }, [ settings ]); + + return ( + + { children } + + ); +}; + +export const useUiSettings = () => useContext(UiSettingsContext); diff --git a/src/api/ui-settings/index.ts b/src/api/ui-settings/index.ts new file mode 100644 index 0000000..255a5da --- /dev/null +++ b/src/api/ui-settings/index.ts @@ -0,0 +1,2 @@ +export * from './IUiSettings'; +export * from './UiSettingsContext'; diff --git a/src/api/utils/SanitizeHtml.ts b/src/api/utils/SanitizeHtml.ts new file mode 100644 index 0000000..39af6e9 --- /dev/null +++ b/src/api/utils/SanitizeHtml.ts @@ -0,0 +1,10 @@ +import DOMPurify from 'dompurify'; + +export const SanitizeHtml = (html: string): string => +{ + return DOMPurify.sanitize(html, { + ALLOWED_TAGS: [ 'b', 'i', 'u', 'br', 'span', 'div', 'p', 'a', 'strong', 'em', 'img' ], + ALLOWED_ATTR: [ 'href', 'target', 'class', 'style', 'src', 'alt' ], + ALLOW_DATA_ATTR: false + }); +}; diff --git a/src/api/utils/index.ts b/src/api/utils/index.ts index 4a4b221..1f22e7f 100644 --- a/src/api/utils/index.ts +++ b/src/api/utils/index.ts @@ -15,6 +15,7 @@ export * from './PrefixUtils'; export * from './ProductImageUtility'; export * from './Randomizer'; export * from './RoomChatFormatter'; +export * from './SanitizeHtml'; export * from './SetLocalStorage'; export * from './SoundNames'; export * from './WindowSaveOptions'; diff --git a/src/components/catalog/CatalogAdminContext.tsx b/src/components/catalog/CatalogAdminContext.tsx new file mode 100644 index 0000000..5a5fe18 --- /dev/null +++ b/src/components/catalog/CatalogAdminContext.tsx @@ -0,0 +1,296 @@ +import { CatalogAdminCreateOfferComposer, CatalogAdminCreatePageComposer, CatalogAdminDeleteOfferComposer, CatalogAdminDeletePageComposer, CatalogAdminMoveOfferComposer, CatalogAdminMovePageComposer, CatalogAdminPublishComposer, CatalogAdminResultEvent, CatalogAdminSaveOfferComposer, CatalogAdminSavePageComposer } from '@nitrots/nitro-renderer'; +import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { ICatalogNode, IPurchasableOffer, NotificationAlertType, SendMessageComposer } from '../../api'; +import { useMessageEvent, useNotification } from '../../hooks'; + +export interface IPageEditData +{ + pageId?: number; + caption: string; + parentId: number; + pageLayout: string; + enabled: string; + visible: string; + minRank: number; + clubOnly?: string; + orderNum: number; + pageHeadline?: string; + pageTeaser?: string; + pageSpecial?: string; + pageText1?: string; + pageText2?: string; + pageTextDetails?: string; + pageTextTeaser?: string; +} + +export interface IOfferEditData +{ + offerId?: number; + pageId: number; + itemIds: string; + catalogName: string; + costCredits: number; + costPoints: number; + pointsType: number; + amount: number; + clubOnly: string; + extradata: string; + haveOffer: string; + offerId_group: number; + limitedStack: number; + orderNumber: number; +} + +interface ICatalogAdminContext +{ + adminMode: boolean; + setAdminMode: (value: boolean) => void; + editingOffer: IPurchasableOffer | null; + setEditingOffer: (offer: IPurchasableOffer | null) => void; + editingPageData: boolean; + setEditingPageData: (value: boolean) => void; + editingRootPage: boolean; + setEditingRootPage: (value: boolean) => void; + editingPageNode: ICatalogNode | null; + setEditingPageNode: (node: ICatalogNode | null) => void; + loading: boolean; + lastError: string | null; + savePage: (data: IPageEditData) => void; + createPage: (data: IPageEditData) => void; + deletePage: (pageId: number) => void; + saveOffer: (data: IOfferEditData) => void; + createOffer: (data: IOfferEditData) => void; + deleteOffer: (offerId: number) => void; + reorderOffers: (orders: { id: number; orderNumber: number }[]) => void; + reorderPage: (pageId: number, newParentId: number, newIndex: number) => void; + togglePageEnabled: (pageId: number) => void; + togglePageVisible: (pageId: number) => void; + publishCatalog: () => void; + hasPendingChanges: boolean; +} + +const CatalogAdminContext = createContext(null); + +export const useCatalogAdmin = () => useContext(CatalogAdminContext); + +export const CatalogAdminProvider: FC<{ children: ReactNode }> = ({ children }) => +{ + const [ adminMode, setAdminMode ] = useState(false); + const [ editingOffer, setEditingOffer ] = useState(null); + const [ editingPageData, setEditingPageData ] = useState(false); + const [ editingRootPage, setEditingRootPage ] = useState(false); + const [ editingPageNode, setEditingPageNode ] = useState(null); + const [ loading, setLoading ] = useState(false); + const [ lastError, setLastError ] = useState(null); + const [ hasPendingChanges, setHasPendingChanges ] = useState(false); + const pendingActionRef = useRef(null); + const { simpleAlert = null } = useNotification(); + + // Keyboard shortcuts: Esc to close edit panels + useEffect(() => + { + if(!adminMode) return; + + const handleKeyDown = (e: KeyboardEvent) => + { + if(e.key === 'Escape') + { + if(editingOffer) { setEditingOffer(null); e.preventDefault(); return; } + if(editingPageData || editingRootPage || editingPageNode) + { + setEditingPageData(false); + setEditingRootPage(false); + setEditingPageNode(null); + e.preventDefault(); + } + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => window.removeEventListener('keydown', handleKeyDown); + }, [ adminMode, editingOffer, editingPageData, editingRootPage, editingPageNode ]); + + useMessageEvent(CatalogAdminResultEvent, (event: CatalogAdminResultEvent) => + { + const parser = event.getParser(); + const action = pendingActionRef.current; + + pendingActionRef.current = null; + setLoading(false); + + if(!parser.success) + { + setLastError(parser.message || 'Operation failed'); + + if(simpleAlert) + { + simpleAlert(parser.message || 'Operation failed', NotificationAlertType.ALERT, null, null, 'Admin Error'); + } + } + else + { + setLastError(null); + setEditingOffer(null); + setEditingPageData(false); + setEditingRootPage(false); + setEditingPageNode(null); + + if(action === 'publish') + { + setHasPendingChanges(false); + } + else + { + setHasPendingChanges(true); + } + + if(simpleAlert && action) + { + const messages: Record = { + 'savePage': 'Page saved (publish to apply)', + 'createPage': 'Page created (publish to apply)', + 'deletePage': 'Page deleted (publish to apply)', + 'saveOffer': 'Offer saved (publish to apply)', + 'createOffer': 'Offer created (publish to apply)', + 'deleteOffer': 'Offer deleted (publish to apply)', + 'reorder': 'Order updated (publish to apply)', + 'toggleEnabled': 'Page toggled (publish to apply)', + 'toggleVisible': 'Visibility toggled (publish to apply)', + 'movePage': 'Page moved (publish to apply)', + 'publish': 'Catalog published! All users updated.', + }; + + simpleAlert(messages[action] || 'Operation completed', NotificationAlertType.DEFAULT, null, null, 'Catalog Admin'); + } + } + }); + + const savePage = useCallback((data: IPageEditData) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'savePage'; + SendMessageComposer(new CatalogAdminSavePageComposer( + data.pageId || 0, data.caption, data.caption, data.pageLayout, 0, + data.minRank, data.visible === '1', data.enabled === '1', + data.orderNum, data.parentId, + data.pageHeadline || '', data.pageTeaser || '', data.pageTextDetails || '' + )); + }, []); + + const createPage = useCallback((data: IPageEditData) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'createPage'; + SendMessageComposer(new CatalogAdminCreatePageComposer( + data.caption, data.caption, data.pageLayout, 0, + data.minRank, data.visible === '1', data.enabled === '1', + data.orderNum, data.parentId + )); + }, []); + + const deletePage = useCallback((pageId: number) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'deletePage'; + SendMessageComposer(new CatalogAdminDeletePageComposer(pageId)); + }, []); + + const saveOffer = useCallback((data: IOfferEditData) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'saveOffer'; + SendMessageComposer(new CatalogAdminSaveOfferComposer( + data.offerId || 0, data.pageId, parseInt(data.itemIds) || 0, + data.catalogName, data.costCredits, data.costPoints, data.pointsType, + data.amount, data.clubOnly === '1' ? 1 : 0, data.extradata, + data.haveOffer === '1', data.offerId_group, data.limitedStack, data.orderNumber + )); + }, []); + + const createOffer = useCallback((data: IOfferEditData) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'createOffer'; + SendMessageComposer(new CatalogAdminCreateOfferComposer( + data.pageId, parseInt(data.itemIds) || 0, + data.catalogName, data.costCredits, data.costPoints, data.pointsType, + data.amount, data.clubOnly === '1' ? 1 : 0, data.extradata, + data.haveOffer === '1', data.offerId_group, data.limitedStack, data.orderNumber + )); + }, []); + + const deleteOffer = useCallback((offerId: number) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'deleteOffer'; + SendMessageComposer(new CatalogAdminDeleteOfferComposer(offerId)); + }, []); + + const reorderOffers = useCallback((orders: { id: number; orderNumber: number }[]) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'reorder'; + + for(const order of orders) + { + SendMessageComposer(new CatalogAdminMoveOfferComposer(order.id, order.orderNumber)); + } + }, []); + + const reorderPage = useCallback((pageId: number, newParentId: number, newIndex: number) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'movePage'; + SendMessageComposer(new CatalogAdminMovePageComposer(pageId, newParentId, newIndex)); + }, []); + + const togglePageEnabled = useCallback((pageId: number) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'toggleEnabled'; + SendMessageComposer(new CatalogAdminMovePageComposer(pageId, -1, -1)); + }, []); + + const togglePageVisible = useCallback((pageId: number) => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'toggleVisible'; + SendMessageComposer(new CatalogAdminMovePageComposer(pageId, -2, -1)); + }, []); + + const publishCatalog = useCallback(() => + { + setLoading(true); + setLastError(null); + pendingActionRef.current = 'publish'; + SendMessageComposer(new CatalogAdminPublishComposer()); + }, []); + + return ( + + { children } + + ); +}; diff --git a/src/components/catalog/CatalogClassicView.tsx b/src/components/catalog/CatalogClassicView.tsx new file mode 100644 index 0000000..a5fabd4 --- /dev/null +++ b/src/components/catalog/CatalogClassicView.tsx @@ -0,0 +1,183 @@ +import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect } from 'react'; +import { FaCog, FaEdit, FaEye, FaEyeSlash, FaPlus, FaTrash } from 'react-icons/fa'; +import { GetConfigurationValue, LocalizeText } from '../../api'; +import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; +import { useCatalog } from '../../hooks'; +import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; +import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView'; +import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView'; +import { CatalogIconView } from './views/catalog-icon/CatalogIconView'; +import { CatalogGiftView } from './views/gift/CatalogGiftView'; +import { CatalogNavigationView } from './views/navigation/CatalogNavigationView'; +import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout'; +import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView'; + +const CatalogClassicViewInner: FC<{}> = () => +{ + const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null } = useCatalog(); + const catalogAdmin = useCatalogAdmin(); + const adminMode = catalogAdmin?.adminMode ?? false; + const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {}); + const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false; + const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {}); + const loading = catalogAdmin?.loading ?? false; + + const isMod = GetSessionDataManager().isModerator; + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + setIsVisible(true); + return; + case 'hide': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prevValue => !prevValue); + return; + case 'open': + if(parts.length > 2) + { + if(parts.length === 4) + { + switch(parts[2]) + { + case 'offerId': + openPageByOfferId(parseInt(parts[3])); + return; + } + } + else + { + openPageByName(parts[2]); + } + } + else + { + setIsVisible(true); + } + + return; + } + }, + eventUrlPrefix: 'catalog/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ setIsVisible, openPageByOfferId, openPageByName ]); + + return ( + <> + { isVisible && + + setIsVisible(false) } /> + { /* Admin banner */ } + { adminMode && +
+ โš™ Admin Mode + +
} + + { rootNode && (rootNode.children.length > 0) && rootNode.children.map((child, index) => + { + if(!adminMode && !child.isVisible) return null; + + const isHidden = !child.isVisible; + + return ( + + { + if(searchResult) setSearchResult(null); + + activateNode(child); + } } > +
+ { GetConfigurationValue('catalog.tab.icons') && } + { child.localization } + { adminMode && isHidden && } + { adminMode && +
e.stopPropagation() }> + { catalogAdmin.setEditingPageNode(child); catalogAdmin.setEditingRootPage(false); catalogAdmin.setEditingPageData(true); } } /> + catalogAdmin.togglePageVisible(child.pageId) }> + { isHidden ? : } + + { if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) catalogAdmin.deletePage(child.pageId); } } /> +
} +
+
+ ); + }) } + { /* Admin toggle button in tabs bar */ } + { isMod && + setAdminMode(!adminMode) }> + + } +
+ + { /* Admin: add new root category */ } + { adminMode && rootNode && +
+ + +
} + + { !navigationHidden && + + { activeNodes && (activeNodes.length > 0) && + } + } + + { adminMode && } + { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) } + + +
+
} + + + + + ); +}; + +export const CatalogClassicView: FC<{}> = () => +{ + return ( + + + + ); +}; diff --git a/src/components/catalog/CatalogModernView.tsx b/src/components/catalog/CatalogModernView.tsx new file mode 100644 index 0000000..ec19b70 --- /dev/null +++ b/src/components/catalog/CatalogModernView.tsx @@ -0,0 +1,291 @@ +import { AddLinkEventTracker, GetSessionDataManager, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; +import { FC, useEffect, useState } from 'react'; +import { FaCog, FaEdit, FaEye, FaEyeSlash, FaHeart, FaPlus, FaStar, FaTrash } from 'react-icons/fa'; +import { LocalizeText } from '../../api'; +import { NitroCardContentView, NitroCardHeaderView, NitroCardView } from '../../common'; +import { useCatalog, useCatalogFavorites } from '../../hooks'; +import { CatalogAdminProvider, useCatalogAdmin } from './CatalogAdminContext'; +import { CatalogAdminOfferEditView } from './views/admin/CatalogAdminOfferEditView'; +import { CatalogAdminPageEditView } from './views/admin/CatalogAdminPageEditView'; +import { CatalogIconView } from './views/catalog-icon/CatalogIconView'; +import { CatalogFavoritesView } from './views/favorites/CatalogFavoritesView'; +import { CatalogGiftView } from './views/gift/CatalogGiftView'; +import { CatalogNavigationView } from './views/navigation/CatalogNavigationView'; +import { CatalogSearchView } from './views/page/common/CatalogSearchView'; +import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout'; +import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView'; + +const CatalogModernViewInner: FC<{}> = () => +{ + const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null } = useCatalog(); + const catalogAdmin = useCatalogAdmin(); + const adminMode = catalogAdmin?.adminMode ?? false; + const setAdminMode = catalogAdmin?.setAdminMode ?? (() => {}); + const hasPendingChanges = catalogAdmin?.hasPendingChanges ?? false; + const publishCatalog = catalogAdmin?.publishCatalog ?? (() => {}); + const loading = catalogAdmin?.loading ?? false; + const { favoriteOfferIds, favoritePageIds } = useCatalogFavorites(); + const [ showFavorites, setShowFavorites ] = useState(false); + + const isMod = GetSessionDataManager().isModerator; + const totalFavs = favoriteOfferIds.length + favoritePageIds.length; + + useEffect(() => + { + const linkTracker: ILinkEventTracker = { + linkReceived: (url: string) => + { + const parts = url.split('/'); + + if(parts.length < 2) return; + + switch(parts[1]) + { + case 'show': + setIsVisible(true); + return; + case 'hide': + setIsVisible(false); + return; + case 'toggle': + setIsVisible(prevValue => !prevValue); + return; + case 'open': + if(parts.length > 2) + { + if(parts.length === 4) + { + switch(parts[2]) + { + case 'offerId': + openPageByOfferId(parseInt(parts[3])); + return; + } + } + else + { + openPageByName(parts[2]); + } + } + else + { + setIsVisible(true); + } + + return; + } + }, + eventUrlPrefix: 'catalog/' + }; + + AddLinkEventTracker(linkTracker); + + return () => RemoveLinkEventTracker(linkTracker); + }, [ setIsVisible, openPageByOfferId, openPageByName ]); + + return ( + <> + { isVisible && + + setIsVisible(false) } /> + + { /* Admin banner */ } + { adminMode && +
+ โš™ Admin Mode + +
} + +
+ { /* === LEFT SIDEBAR === */ } +
+ + { /* Favorites toggle */ } +
setShowFavorites(!showFavorites) } + > +
+ 0 ? 'text-danger' : 'text-muted' }` } /> + { totalFavs > 0 && + + { totalFavs } + } +
+ { LocalizeText('catalog.favorites') } +
+ +
+ + { /* Admin: root page actions */ } + { adminMode && rootNode && +
+ + +
} + + { /* Category icons */ } + { rootNode && rootNode.children.length > 0 && rootNode.children.map((child, index) => + { + if(!adminMode && !child.isVisible) return null; + + const isHidden = !child.isVisible; + + return ( +
+ { + if(searchResult) setSearchResult(null); + if(showFavorites) setShowFavorites(false); + activateNode(child); + } } + > +
+ + { isHidden && } +
+ + { child.localization } + + { /* Admin actions on each root category */ } + { adminMode && +
+
+ { + e.stopPropagation(); + catalogAdmin.setEditingPageNode(child); + catalogAdmin.setEditingRootPage(false); + catalogAdmin.setEditingPageData(true); + } } + > + +
+
+ { + e.stopPropagation(); + catalogAdmin.togglePageVisible(child.pageId); + } } + > + { isHidden + ? + : } +
+
+ { + e.stopPropagation(); + if(confirm(LocalizeText('catalog.admin.delete.category.confirm', [ 'name' ], [ child.localization ]))) + { + catalogAdmin.deletePage(child.pageId); + } + } } + > + +
+
} +
+ ); + }) } +
+ + { /* === MAIN AREA === */ } +
+ { /* Toolbar: search + admin */ } +
+ { /* Breadcrumb */ } +
+ + { activeNodes && activeNodes.length > 0 + ? activeNodes.map((node, i) => ( + + { i > 0 && โ€บ } + activateNode(node) : undefined }> + { node.localization } + + + )) + : { LocalizeText('catalog.title') } } +
+ +
+ +
+ + { isMod && + } +
+ + { /* Content area */ } +
+ { showFavorites + ?
+ setShowFavorites(false) } /> +
+ : <> + { !navigationHidden && activeNodes && activeNodes.length > 0 && +
+ +
} +
+ { adminMode && } + { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) } +
+ } +
+
+
+ + } + + + + + ); +}; + +export const CatalogModernView: FC<{}> = () => +{ + return ( + + + + ); +}; diff --git a/src/components/catalog/CatalogView.tsx b/src/components/catalog/CatalogView.tsx index fc5f633..9e4650f 100644 --- a/src/components/catalog/CatalogView.tsx +++ b/src/components/catalog/CatalogView.tsx @@ -1,111 +1,13 @@ -import { AddLinkEventTracker, ILinkEventTracker, RemoveLinkEventTracker } from '@nitrots/nitro-renderer'; -import { FC, useEffect } from 'react'; -import { GetConfigurationValue, LocalizeText } from '../../api'; -import { Column, Grid, NitroCardContentView, NitroCardHeaderView, NitroCardTabsItemView, NitroCardTabsView, NitroCardView } from '../../common'; -import { useCatalog } from '../../hooks'; -import { CatalogIconView } from './views/catalog-icon/CatalogIconView'; -import { CatalogGiftView } from './views/gift/CatalogGiftView'; -import { CatalogNavigationView } from './views/navigation/CatalogNavigationView'; -import { GetCatalogLayout } from './views/page/layout/GetCatalogLayout'; -import { MarketplacePostOfferView } from './views/page/layout/marketplace/MarketplacePostOfferView'; +import { FC } from 'react'; +import { GetConfigurationValue } from '../../api'; +import { CatalogClassicView } from './CatalogClassicView'; +import { CatalogModernView } from './CatalogModernView'; -export const CatalogView: FC<{}> = props => +export const CatalogView: FC<{}> = () => { - const { isVisible = false, setIsVisible = null, rootNode = null, currentPage = null, navigationHidden = false, setNavigationHidden = null, activeNodes = [], searchResult = null, setSearchResult = null, openPageByName = null, openPageByOfferId = null, activateNode = null, getNodeById } = useCatalog(); + const useNewStyle = GetConfigurationValue('catalog.style.new', false); - useEffect(() => - { - const linkTracker: ILinkEventTracker = { - linkReceived: (url: string) => - { - const parts = url.split('/'); + if(useNewStyle) return ; - if(parts.length < 2) return; - - switch(parts[1]) - { - case 'show': - setIsVisible(true); - return; - case 'hide': - setIsVisible(false); - return; - case 'toggle': - setIsVisible(prevValue => !prevValue); - return; - case 'open': - if(parts.length > 2) - { - if(parts.length === 4) - { - switch(parts[2]) - { - case 'offerId': - openPageByOfferId(parseInt(parts[3])); - return; - } - } - else - { - openPageByName(parts[2]); - } - } - else - { - setIsVisible(true); - } - - return; - } - }, - eventUrlPrefix: 'catalog/' - }; - - AddLinkEventTracker(linkTracker); - - return () => RemoveLinkEventTracker(linkTracker); - }, [ setIsVisible, openPageByOfferId, openPageByName ]); - - return ( - <> - { isVisible && - - setIsVisible(false) } /> - - { rootNode && (rootNode.children.length > 0) && rootNode.children.map((child, index) => - { - if(!child.isVisible) return null; - - return ( - - { - if(searchResult) setSearchResult(null); - - activateNode(child); - } } > -
- { GetConfigurationValue('catalog.tab.icons') && } - { child.localization } -
-
- ); - }) } -
- - - { !navigationHidden && - - { activeNodes && (activeNodes.length > 0) && - } - } - - { GetCatalogLayout(currentPage, () => setNavigationHidden(true)) } - - - -
} - - - - ); + return ; }; diff --git a/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx b/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx new file mode 100644 index 0000000..f6da55a --- /dev/null +++ b/src/components/catalog/views/admin/CatalogAdminOfferEditView.tsx @@ -0,0 +1,225 @@ +import { FC, useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa'; +import { LocalizeText } from '../../../../api'; +import { useCatalog } from '../../../../hooks'; +import { IOfferEditData, useCatalogAdmin } from '../../CatalogAdminContext'; + +export const CatalogAdminOfferEditView: FC<{}> = () => +{ + const { currentPage = null } = useCatalog(); + const catalogAdmin = useCatalogAdmin(); + const editingOffer = catalogAdmin?.editingOffer ?? null; + const setEditingOffer = catalogAdmin?.setEditingOffer; + const saveOffer = catalogAdmin?.saveOffer; + const deleteOffer = catalogAdmin?.deleteOffer; + const createOffer = catalogAdmin?.createOffer; + const loading = catalogAdmin?.loading ?? false; + + const [ itemIds, setItemIds ] = useState('0'); + const [ catalogName, setCatalogName ] = useState(''); + const [ costCredits, setCostCredits ] = useState(0); + const [ costPoints, setCostPoints ] = useState(0); + const [ pointsType, setPointsType ] = useState(0); + const [ amount, setAmount ] = useState(1); + const [ clubOnly, setClubOnly ] = useState('0'); + const [ extradata, setExtradata ] = useState(''); + const [ haveOffer, setHaveOffer ] = useState('1'); + const [ offerId, setOfferIdGroup ] = useState(-1); + const [ limitedStack, setLimitedStack ] = useState(0); + const [ orderNumber, setOrderNumber ] = useState(0); + const [ isNew, setIsNew ] = useState(false); + + useEffect(() => + { + if(!editingOffer) return; + + if(editingOffer.offerId === -1) + { + setIsNew(true); + setItemIds('0'); + setCatalogName(''); + setCostCredits(0); + setCostPoints(0); + setPointsType(0); + setAmount(1); + setClubOnly('0'); + setExtradata(''); + setHaveOffer('1'); + setOfferIdGroup(-1); + setLimitedStack(0); + setOrderNumber(0); + } + else + { + setIsNew(false); + setItemIds(String(editingOffer.product?.productClassId || 0)); + setCatalogName(editingOffer.localizationName || ''); + setCostCredits(editingOffer.priceInCredits); + setCostPoints(editingOffer.priceInActivityPoints); + setPointsType(editingOffer.activityPointType); + setAmount(editingOffer.product?.productCount || 1); + setClubOnly(editingOffer.clubLevel > 0 ? '1' : '0'); + setExtradata(editingOffer.product?.extraParam || ''); + setHaveOffer('1'); + setOfferIdGroup(editingOffer.offerId || -1); + setLimitedStack(0); + setOrderNumber(0); + } + }, [ editingOffer ]); + + if(!editingOffer) return null; + + const handleSave = async () => + { + if(!saveOffer || !createOffer) return; + + const data: IOfferEditData = { + offerId: isNew ? undefined : editingOffer.offerId, + pageId: currentPage?.pageId || 0, + itemIds, + catalogName, + costCredits, + costPoints, + pointsType, + amount, + clubOnly, + extradata, + haveOffer, + offerId_group: offerId, + limitedStack, + orderNumber + }; + + const success = isNew ? await createOffer(data) : await saveOffer(data); + + if(success && setEditingOffer) setEditingOffer(null); + }; + + const handleDelete = () => + { + if(isNew || !deleteOffer || !confirm(LocalizeText('catalog.admin.delete.offer.confirm'))) return; + + deleteOffer(editingOffer.offerId); + if(setEditingOffer) setEditingOffer(null); + }; + + const inputClass = 'text-[11px] border-2 border-card-grid-item-border rounded px-2 py-1 bg-white focus:outline-none focus:border-primary transition-colors'; + + return createPortal( +
setEditingOffer(null) }> +
+ +
e.stopPropagation() }> + { /* Header */ } +
+ + { isNew ? LocalizeText('catalog.admin.offer.new') : `${ LocalizeText('catalog.admin.offer.edit') } #${ editingOffer.offerId }` } + +
setEditingOffer(null) }> + +
+
+ +
+ { /* Current name */ } + { !isNew && +
+ { editingOffer.localizationName } +
} + + { /* Catalog Name */ } +
+ + setCatalogName(e.target.value) } /> +
+ + { /* Generale */ } +
+
{ LocalizeText('catalog.admin.offer.general') }
+
+
+ + setItemIds(e.target.value) } /> +
+
+ + setAmount(parseInt(e.target.value) || 1) } /> +
+
+ + setOrderNumber(parseInt(e.target.value) || 0) } /> +
+
+
+ + { /* Prezzi */ } +
+
{ LocalizeText('catalog.admin.offer.prices') }
+
+
+ + setCostCredits(parseInt(e.target.value) || 0) } /> +
+
+ + setCostPoints(parseInt(e.target.value) || 0) } /> +
+
+ + +
+
+
+ + { /* Opzioni */ } +
+
{ LocalizeText('catalog.admin.offer.options') }
+
+
+ + +
+
+ + setLimitedStack(parseInt(e.target.value) || 0) } /> +
+
+ + setOfferIdGroup(parseInt(e.target.value) || -1) } /> +
+
+
+ + setExtradata(e.target.value) } /> +
+
+ setHaveOffer(e.target.checked ? '1' : '0') } /> + +
+
+ + { /* Actions */ } +
+ { !isNew + ? + :
} + +
+
+
+
, + document.body + ); +}; diff --git a/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx b/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx new file mode 100644 index 0000000..6fd4a07 --- /dev/null +++ b/src/components/catalog/views/admin/CatalogAdminPageEditView.tsx @@ -0,0 +1,153 @@ +import { FC, useEffect, useState } from 'react'; +import { FaSave, FaSpinner, FaTimes, FaTrash } from 'react-icons/fa'; +import { LocalizeText } from '../../../../api'; +import { useCatalog } from '../../../../hooks'; +import { IPageEditData, useCatalogAdmin } from '../../CatalogAdminContext'; + +const LAYOUT_OPTIONS = [ + 'default_3x3', 'frontpage4', 'pets', 'pets2', 'pets3', + 'spaces_new', 'soundmachine', 'trophies', 'roomads', + 'guild_frontpage', 'guild_forum', 'guild_custom_furni', + 'vip_buy', 'marketplace', 'marketplace_own_items', + 'recycler', 'recycler_info', 'recycler_prizes', + 'info_loyalty', 'badge_display', 'bots', 'single_bundle', + 'color_grouping', 'recent_purchases', 'custom_prefix' +]; + +export const CatalogAdminPageEditView: FC<{}> = () => +{ + const { currentPage = null, activeNodes = [], rootNode = null } = useCatalog(); + const catalogAdmin = useCatalogAdmin(); + const editingPageData = catalogAdmin?.editingPageData ?? false; + const editingRootPage = catalogAdmin?.editingRootPage ?? false; + const editingPageNode = catalogAdmin?.editingPageNode ?? null; + const loading = catalogAdmin?.loading ?? false; + + const [ caption, setCaption ] = useState(''); + const [ pageLayout, setPageLayout ] = useState('default_3x3'); + const [ minRank, setMinRank ] = useState(1); + const [ visible, setVisible ] = useState('1'); + const [ enabled, setEnabled ] = useState('1'); + const [ orderNum, setOrderNum ] = useState(0); + + // Resolve what we're editing: + // 1. editingPageNode (explicit node from sidebar click) + // 2. editingRootPage (root button) + // 3. current active page (from "Modifica Pagina" in layout) + const targetNode = editingPageNode + ? editingPageNode + : editingRootPage + ? rootNode + : (activeNodes.length > 0 ? activeNodes[activeNodes.length - 1] : null); + + const targetPageId = targetNode?.pageId ?? currentPage?.pageId; + const isRoot = editingRootPage; + + const closeForm = () => + { + catalogAdmin?.setEditingPageData(false); + catalogAdmin?.setEditingRootPage(false); + catalogAdmin?.setEditingPageNode(null); + }; + + useEffect(() => + { + if(!editingPageData || !targetNode) return; + + setCaption(targetNode.localization || ''); + setPageLayout(currentPage?.layoutCode || 'default_3x3'); + setVisible(targetNode.isVisible ? '1' : '0'); + setEnabled('1'); + setMinRank(1); + setOrderNum(0); + }, [ editingPageData, targetNode, currentPage ]); + + if(!editingPageData || !targetNode) return null; + + const inputClass = 'text-[11px] border-2 border-card-grid-item-border rounded px-2 py-1 bg-white focus:outline-none focus:border-primary transition-colors'; + + const handleSave = async () => + { + if(!catalogAdmin?.savePage) return; + + const parentNode = targetNode.parent; + + const data: IPageEditData = { + pageId: targetPageId, + caption, + pageLayout, + minRank, + visible, + enabled, + orderNum, + parentId: parentNode ? parentNode.pageId : -1, + }; + + const success = await catalogAdmin.savePage(data); + + if(success) closeForm(); + }; + + const handleDelete = async () => + { + if(!catalogAdmin?.deletePage || isRoot) return; + if(!confirm(LocalizeText('catalog.admin.delete.page.confirm', [ 'name' ], [ targetNode.localization ]))) return; + + const success = await catalogAdmin.deletePage(targetPageId); + + if(success) closeForm(); + }; + + return ( +
+
+ + { isRoot ? LocalizeText('catalog.admin.edit.root') : `${ LocalizeText('catalog.admin.edit') } ${ targetNode.localization } (#${ targetPageId })` } + + +
+ +
+
+ + setCaption(e.target.value) } /> +
+
+ + setMinRank(parseInt(e.target.value) || 1) } /> +
+
+ + +
+
+ + setOrderNum(parseInt(e.target.value) || 0) } /> +
+
+ + +
+
+ +
+ { !isRoot + ? + :
} + +
+
+ ); +}; diff --git a/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx b/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx new file mode 100644 index 0000000..3b1cd21 --- /dev/null +++ b/src/components/catalog/views/catalog-rail/CatalogRailItemView.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import { ICatalogNode } from '../../../../api'; +import { CatalogIconView } from '../catalog-icon/CatalogIconView'; + +interface CatalogRailItemViewProps +{ + node: ICatalogNode; + isActive: boolean; + onClick: () => void; +} + +export const CatalogRailItemView: FC = props => +{ + const { node, isActive, onClick } = props; + + return ( +
+
+ +
+ + { node.localization } + +
+ ); +}; diff --git a/src/components/catalog/views/favorites/CatalogFavoritesView.tsx b/src/components/catalog/views/favorites/CatalogFavoritesView.tsx new file mode 100644 index 0000000..676f771 --- /dev/null +++ b/src/components/catalog/views/favorites/CatalogFavoritesView.tsx @@ -0,0 +1,154 @@ +import { FC, useMemo } from 'react'; +import { FaHeart, FaStar, FaTimes } from 'react-icons/fa'; +import { ICatalogNode, LocalizeText } from '../../../../api'; +import { useCatalog, useCatalogFavorites } from '../../../../hooks'; +import { CatalogIconView } from '../catalog-icon/CatalogIconView'; + +interface CatalogFavoritesViewProps +{ + onClose: () => void; +} + +export const CatalogFavoritesView: FC = props => +{ + const { onClose } = props; + const { favoriteOffers, favoritePageIds, toggleFavoritePage, toggleFavoriteOffer } = useCatalogFavorites(); + const { offersToNodes, activateNode, openPageByOfferId, rootNode } = useCatalog(); + + const favoritePages = useMemo(() => + { + if(!rootNode || favoritePageIds.length === 0) return []; + + const pages: Array<{ pageId: number; name: string; iconId: number; node: ICatalogNode }> = []; + + const findNode = (node: ICatalogNode) => + { + if(favoritePageIds.includes(node.pageId)) + { + pages.push({ pageId: node.pageId, name: node.localization, iconId: node.iconId, node }); + } + + if(node.children) + { + for(const child of node.children) findNode(child); + } + }; + + findNode(rootNode); + + return pages; + }, [ favoritePageIds, rootNode ]); + + // Enrich offers with node data if available + const enrichedOffers = useMemo(() => + { + return favoriteOffers.map(fav => + { + let nodeName: string | null = null; + let nodeIconId: number | null = null; + + if(offersToNodes) + { + const nodes = offersToNodes.get(fav.offerId); + + if(nodes && nodes.length > 0) + { + nodeName = nodes[0].localization; + nodeIconId = nodes[0].iconId; + } + } + + return { + ...fav, + displayName: fav.name || nodeName || `Offer #${ fav.offerId }`, + nodeIconId + }; + }); + }, [ favoriteOffers, offersToNodes ]); + + return ( +
+ { /* Header */ } +
+
+ + { LocalizeText('catalog.favorites') } + ({ enrichedOffers.length + favoritePages.length }) +
+ +
+ +
+ { /* Favorite Pages */ } + { favoritePages.length > 0 && +
+
+ + { LocalizeText('catalog.favorites.pages') } +
+
+ { favoritePages.map(page => ( +
{ activateNode(page.node); onClose(); } } + > + + { page.name } + { e.stopPropagation(); toggleFavoritePage(page.pageId); } } + /> +
+ )) } +
+
} + + { /* Favorite Offers */ } + { enrichedOffers.length > 0 && +
+
+ + { LocalizeText('catalog.favorites.furni') } +
+
+ { enrichedOffers.map(fav => ( +
{ openPageByOfferId(fav.offerId); onClose(); } } + > + { /* Furni icon */ } +
+ { fav.iconUrl + ? + : fav.nodeIconId !== null + ? + : + } +
+ { fav.displayName } + { e.stopPropagation(); toggleFavoriteOffer(fav.offerId); } } + /> +
+ )) } +
+
} + + { /* Empty state */ } + { favoritePages.length === 0 && enrichedOffers.length === 0 && +
+
+ +

{ LocalizeText('catalog.favorites.empty') }

+

{ LocalizeText('catalog.favorites.empty.hint') }

+
+
} +
+
+ ); +}; diff --git a/src/components/catalog/views/gift/CatalogGiftView.tsx b/src/components/catalog/views/gift/CatalogGiftView.tsx index 8104afb..e23e568 100644 --- a/src/components/catalog/views/gift/CatalogGiftView.tsx +++ b/src/components/catalog/views/gift/CatalogGiftView.tsx @@ -128,6 +128,7 @@ export const CatalogGiftView: FC<{}> = props => if(isBuyingGift) return; isBuyingGift = true; + setTimeout(() => { isBuyingGift = false; }, 10000); SendMessageComposer(new PurchaseFromCatalogAsGiftComposer(pageId, offerId, extraData, receiverName, message, colourId, selectedBoxIndex, selectedRibbonIndex, showMyFace)); return; diff --git a/src/components/catalog/views/navigation/CatalogBreadcrumbView.tsx b/src/components/catalog/views/navigation/CatalogBreadcrumbView.tsx new file mode 100644 index 0000000..60ed73a --- /dev/null +++ b/src/components/catalog/views/navigation/CatalogBreadcrumbView.tsx @@ -0,0 +1,39 @@ +import { FC } from 'react'; +import { FaChevronRight, FaHome } from 'react-icons/fa'; +import { LocalizeText } from '../../../../api'; +import { useCatalog } from '../../../../hooks'; + +export const CatalogBreadcrumbView: FC<{}> = () => +{ + const { activeNodes = [], activateNode } = useCatalog(); + + if(!activeNodes || activeNodes.length === 0) + { + return ( +
+ + { LocalizeText('catalog.title') } +
+ ); + } + + return ( +
+ activateNode(activeNodes[0]) } + /> + { activeNodes.map((node, i) => ( + + + activateNode(node) : undefined } + > + { node.localization } + + + )) } +
+ ); +}; diff --git a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx index 48ea04e..d31d801 100644 --- a/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx +++ b/src/components/catalog/views/navigation/CatalogNavigationItemView.tsx @@ -1,8 +1,8 @@ -import { FC } from 'react'; -import { FaCaretDown, FaCaretUp } from 'react-icons/fa'; -import { ICatalogNode } from '../../../../api'; -import { LayoutGridItem, Text } from '../../../../common'; -import { useCatalog } from '../../../../hooks'; +import { FC, useCallback, useRef, useState } from 'react'; +import { FaArrowsAlt, FaCaretDown, FaCaretUp, FaPlus, FaStar, FaTrash } from 'react-icons/fa'; +import { ICatalogNode, LocalizeText } from '../../../../api'; +import { useCatalog, useCatalogFavorites } from '../../../../hooks'; +import { useCatalogAdmin } from '../../CatalogAdminContext'; import { CatalogIconView } from '../catalog-icon/CatalogIconView'; import { CatalogNavigationSetView } from './CatalogNavigationSetView'; @@ -16,18 +16,122 @@ export const CatalogNavigationItemView: FC = pro { const { node = null, child = false } = props; const { activateNode = null } = useCatalog(); + const catalogAdmin = useCatalogAdmin(); + const adminMode = catalogAdmin?.adminMode ?? false; + const { isFavoritePage, toggleFavoritePage } = useCatalogFavorites(); + const isFav = node ? isFavoritePage(node.pageId) : false; + const [ isDragOver, setIsDragOver ] = useState(false); + const dragRef = useRef(null); + + const handleDragStart = useCallback((e: React.DragEvent) => + { + if(!adminMode) return; + + e.dataTransfer.setData('text/plain', JSON.stringify({ pageId: node.pageId, parentId: node.parent?.pageId ?? -1 })); + e.dataTransfer.effectAllowed = 'move'; + }, [ adminMode, node ]); + + const handleDragOver = useCallback((e: React.DragEvent) => + { + if(!adminMode) return; + + e.preventDefault(); + e.dataTransfer.dropEffect = 'move'; + setIsDragOver(true); + }, [ adminMode ]); + + const handleDragLeave = useCallback(() => + { + setIsDragOver(false); + }, []); + + const handleDrop = useCallback((e: React.DragEvent) => + { + if(!adminMode) return; + + e.preventDefault(); + setIsDragOver(false); + + try + { + const data = JSON.parse(e.dataTransfer.getData('text/plain')); + + if(data.pageId && data.pageId !== node.pageId) + { + // Drop onto a branch = reparent under this node + // Drop onto a leaf = reorder as sibling + const targetParentId = node.isBranch ? node.pageId : (node.parent?.pageId ?? -1); + const targetIndex = node.isBranch ? 0 : (node.parent?.children?.indexOf(node) ?? 0); + + catalogAdmin?.reorderPage(data.pageId, targetParentId, targetIndex); + } + } + catch(err) + { + // Invalid drag data + } + }, [ adminMode, node, catalogAdmin ]); return ( -
- activateNode(node) }> - - { node.localization } +
+
activateNode(node) } + onDragLeave={ adminMode ? handleDragLeave : undefined } + onDragOver={ adminMode ? handleDragOver : undefined } + onDragStart={ adminMode ? handleDragStart : undefined } + onDrop={ adminMode ? handleDrop : undefined } + > + { adminMode && + } +
+ +
+ { node.localization } + { adminMode && +
+ + { + e.stopPropagation(); + catalogAdmin.createPage({ + caption: 'New Page', + pageLayout: 'default_3x3', + minRank: 1, + visible: '1', + enabled: '1', + orderNum: 0, + parentId: node.pageId, + }); + } } + /> + + { + e.stopPropagation(); + if(confirm(LocalizeText('catalog.admin.delete.page.confirm', [ 'name' ], [ node.localization ]))) + { + catalogAdmin.deletePage(node.pageId); + } + } } + /> +
} + { !adminMode && node.pageId > 0 && + { e.stopPropagation(); toggleFavoritePage(node.pageId); } } + /> } { node.isBranch && - <> - { node.isOpen && } - { !node.isOpen && } - } - + + { node.isOpen ? : } + } +
{ node.isOpen && node.isBranch && }
diff --git a/src/components/catalog/views/navigation/CatalogNavigationView.tsx b/src/components/catalog/views/navigation/CatalogNavigationView.tsx index 10e2a2f..777c5fd 100644 --- a/src/components/catalog/views/navigation/CatalogNavigationView.tsx +++ b/src/components/catalog/views/navigation/CatalogNavigationView.tsx @@ -1,8 +1,6 @@ import { FC } from 'react'; import { ICatalogNode } from '../../../../api'; -import { AutoGrid, Column } from '../../../../common'; import { useCatalog } from '../../../../hooks'; -import { CatalogSearchView } from '../page/common/CatalogSearchView'; import { CatalogNavigationItemView } from './CatalogNavigationItemView'; import { CatalogNavigationSetView } from './CatalogNavigationSetView'; @@ -17,18 +15,13 @@ export const CatalogNavigationView: FC = props => const { searchResult = null } = useCatalog(); return ( - <> - - - - { searchResult && (searchResult.filteredNodes.length > 0) && searchResult.filteredNodes.map((n, index) => - { - return ; - }) } - { !searchResult && - } - - - +
+ { searchResult && (searchResult.filteredNodes.length > 0) && searchResult.filteredNodes.map((n, index) => + { + return ; + }) } + { !searchResult && + } +
); }; diff --git a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx index f99bbad..a3fcedd 100644 --- a/src/components/catalog/views/page/common/CatalogGridOfferView.tsx +++ b/src/components/catalog/views/page/common/CatalogGridOfferView.tsx @@ -1,8 +1,9 @@ import { MouseEventType } from '@nitrots/nitro-renderer'; import { FC, MouseEvent, useMemo, useState } from 'react'; +import { FaHeart } from 'react-icons/fa'; import { IPurchasableOffer, Offer, ProductTypeEnum } from '../../../../../api'; import { LayoutAvatarImageView, LayoutGridItem, LayoutGridItemProps } from '../../../../../common'; -import { useCatalog, useInventoryFurni } from '../../../../../hooks'; +import { useCatalog, useCatalogFavorites, useInventoryFurni } from '../../../../../hooks'; interface CatalogGridOfferViewProps extends LayoutGridItemProps { @@ -16,6 +17,8 @@ export const CatalogGridOfferView: FC = props => const [ isMouseDown, setMouseDown ] = useState(false); const { requestOfferToMover = null } = useCatalog(); const { isVisible = false } = useInventoryFurni(); + const { isFavoriteOffer, toggleFavoriteOffer } = useCatalogFavorites(); + const isFav = isFavoriteOffer(offer.offerId); const iconUrl = useMemo(() => { @@ -51,9 +54,28 @@ export const CatalogGridOfferView: FC = props => if(!product) return null; return ( - + { (offer.product.productType === ProductTypeEnum.ROBOT) && } +
{ e.stopPropagation(); e.preventDefault(); toggleFavoriteOffer(offer.offerId, offer.localizationName, iconUrl); } } + onMouseDown={ e => e.stopPropagation() } + > + +
); }; diff --git a/src/components/catalog/views/page/common/CatalogSearchView.tsx b/src/components/catalog/views/page/common/CatalogSearchView.tsx index dc3f34b..7cc30c7 100644 --- a/src/components/catalog/views/page/common/CatalogSearchView.tsx +++ b/src/components/catalog/views/page/common/CatalogSearchView.tsx @@ -2,11 +2,9 @@ import { GetSessionDataManager, IFurnitureData } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; import { FaSearch, FaTimes } from 'react-icons/fa'; import { CatalogPage, CatalogType, FilterCatalogNode, FurnitureOffer, GetOfferNodes, ICatalogNode, ICatalogPage, IPurchasableOffer, LocalizeText, PageLocalization, SearchResult } from '../../../../../api'; -import { Button, Flex } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; -import { NitroInput } from '../../../../../layout'; -export const CatalogSearchView: FC<{}> = props => +export const CatalogSearchView: FC<{}> = () => { const [ searchValue, setSearchValue ] = useState(''); const { currentType = null, rootNode = null, offersToNodes = null, searchResult = null, setSearchResult = null, setCurrentPage = null } = useCatalog(); @@ -78,29 +76,22 @@ export const CatalogSearchView: FC<{}> = props => }, [ offersToNodes, currentType, rootNode, searchValue, setCurrentPage, setSearchResult ]); return ( -
- - - - - - - - setSearchValue(event.target.value) } /> - - - - { (!searchValue || !searchValue.length) && - } - { searchValue && !!searchValue.length && - } +
+ + setSearchValue(e.target.value) } + /> + { searchValue && searchValue.length > 0 && + }
); }; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx index f2bd6f3..ee82e6e 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutBadgeDisplayView.tsx @@ -1,5 +1,5 @@ import { FC } from 'react'; -import { LocalizeText } from '../../../../../api'; +import { LocalizeText, SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogBadgeSelectorWidgetView } from '../widgets/CatalogBadgeSelectorWidgetView'; @@ -31,7 +31,7 @@ export const CatalogLayoutBadgeDisplayView: FC = props => { !currentOffer && <> { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx index 47e0dc6..4c1944c 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutColorGroupingView.tsx @@ -1,7 +1,7 @@ import { ColorConverter } from '@nitrots/nitro-renderer'; import { FC, useMemo, useState } from 'react'; import { FaFillDrip } from 'react-icons/fa'; -import { IPurchasableOffer } from '../../../../../api'; +import { IPurchasableOffer, SanitizeHtml } from '../../../../../api'; import { AutoGrid, Button, Column, Grid, LayoutGridItem, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogGridOfferView } from '../common/CatalogGridOfferView'; @@ -146,7 +146,7 @@ export const CatalogLayoutColorGroupingView: FC { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx index 0b7f904..e723833 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx @@ -1,7 +1,6 @@ import { PurchasePrefixComposer } from '@nitrots/nitro-renderer'; -import { createPortal } from 'react-dom'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api'; +import { LocalizeText, SanitizeHtml, SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; @@ -32,32 +31,6 @@ export const CatalogLayoutCustomPrefixView: FC = props => const [ showIconPicker, setShowIconPicker ] = useState(false); const [ selectedEffect, setSelectedEffect ] = useState(''); const [ purchased, setPurchased ] = useState(false); - const pickerContainerRef = useRef(null); - - // Inject style into emoji-mart Shadow DOM to remove backdrop-filter blur - useEffect(() => - { - if(!showIconPicker) return; - - const timer = setTimeout(() => - { - const container = pickerContainerRef.current; - if(!container) return; - - const emPicker = container.querySelector('em-emoji-picker'); - if(!emPicker?.shadowRoot) return; - - const existing = emPicker.shadowRoot.querySelector('#no-blur-fix'); - if(existing) return; - - const style = document.createElement('style'); - style.id = 'no-blur-fix'; - style.textContent = `.sticky { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background-color: rgb(var(--em-rgb-background)) !important; } .menu { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; background-color: rgb(var(--em-rgb-background)) !important; }`; - emPicker.shadowRoot.appendChild(style); - }, 50); - - return () => clearTimeout(timer); - }, [ showIconPicker ]); const colorString = useMemo(() => { @@ -104,7 +77,7 @@ export const CatalogLayoutCustomPrefixView: FC = props => setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: color })); setCustomColorInput(color); - // Auto-advance to next letter + // Auto-avanza alla lettera successiva if(selectedLetterIndex < prefixText.length - 1) { const nextIdx = selectedLetterIndex + 1; @@ -164,7 +137,7 @@ export const CatalogLayoutCustomPrefixView: FC = props => { page.localization.getImage(0) && } { page.localization.getText(0) && -
} +
} { /* Live Preview */ }
= props => { /* Text + Icon Row */ }
- +
= props =>
- +
@@ -241,14 +214,14 @@ export const CatalogLayoutCustomPrefixView: FC = props =>
- { /* Emoji Picker (emoji-mart) - portaled to body, no backdrop */ } - { showIconPicker && createPortal( + { /* Emoji Picker (emoji-mart) - fixed overlay */ } + { showIconPicker && ( <> -
setShowIconPicker(false) } /> -
+
setShowIconPicker(false) } /> +
{ setSelectedIcon(emoji.native); setShowIconPicker(false); } } theme="dark" previewPosition="none" @@ -261,13 +234,12 @@ export const CatalogLayoutCustomPrefixView: FC = props => set="native" />
- , - document.body + ) } { /* Effect Selector */ }
- +
{ PRESET_PREFIX_EFFECTS.map(fx => (
@@ -316,7 +288,7 @@ export const CatalogLayoutCustomPrefixView: FC = props =>
- Select a letter, then choose a color. Auto-advances. + { LocalizeText('catalog.prefix.color.hint') }
= props =>
{ colorMode === 'perLetter' && selectedLetterIndex !== null && - Selected letter: "{ prefixText[selectedLetterIndex] || '' }" + { LocalizeText('catalog.prefix.color.selected') } "{ prefixText[selectedLetterIndex] || '' }" } -
+
{ PRESET_COLORS.map((color, idx) => { const isActive = currentActiveColor === color; return (
handleColorSelect(color) } /> @@ -443,8 +410,8 @@ export const CatalogLayoutCustomPrefixView: FC = props =>
- Price: - 5 Credits + { LocalizeText('catalog.prefix.price') } + { LocalizeText('catalog.prefix.price.amount') }
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx index 50b2955..e797289 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutDefaultView.tsx @@ -1,7 +1,9 @@ import { FC } from 'react'; -import { GetConfigurationValue, ProductTypeEnum } from '../../../../../api'; -import { Column, Flex, Grid, LayoutImage, Text } from '../../../../../common'; +import { FaEdit, FaPlus } from 'react-icons/fa'; +import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; +import { Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; +import { useCatalogAdmin } from '../../../CatalogAdminContext'; import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView'; import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView'; @@ -16,46 +18,87 @@ export const CatalogLayoutDefaultView: FC = props => { const { page = null } = props; const { currentOffer = null, currentPage = null } = useCatalog(); + const catalogAdmin = useCatalogAdmin(); + const adminMode = catalogAdmin?.adminMode ?? false; return ( - <> - - - { GetConfigurationValue('catalog.headers') && - } - - - - { !currentOffer && - <> - { !!page.localization.getImage(1) && - } - - } - { currentOffer && - <> - - { (currentOffer.product.productType !== ProductTypeEnum.BADGE) && - <> - - - } - { (currentOffer.product.productType === ProductTypeEnum.BADGE) && } - - - - { currentOffer.localizationName } -
-
- -
- -
- -
- } -
-
- +
+ { /* Admin: quick actions */ } + { adminMode && !catalogAdmin.editingPageData && +
+ + +
} + + { /* Product detail card */ } + { currentOffer && +
+ { /* Preview area */ } +
+ { (currentOffer.product.productType !== ProductTypeEnum.BADGE) && + <> + + + } + { (currentOffer.product.productType === ProductTypeEnum.BADGE) && + } +
+ { /* Product info + purchase */ } +
+ { /* Title row */ } +
+
+ { currentOffer.localizationName } + { adminMode && + catalogAdmin.setEditingOffer(currentOffer) } + /> } +
+ { adminMode && +
+ ID: { currentOffer.product.productClassId } + Offer: { currentOffer.offerId } + { currentOffer.product.productType.toUpperCase() } +
} + +
+ { /* Price */ } + + { /* Spinner */ } + + { /* Actions */ } +
+ +
+
+
} + + { /* Welcome/description card */ } + { !currentOffer && +
+ { !!page.localization.getImage(1) && + } + +
} + + { /* Item grid */ } +
+ { GetConfigurationValue('catalog.headers') && + } + +
+
); }; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx index f94bd0e..cb8b6fa 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildCustomFurniView.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogGuildBadgeWidgetView } from '../widgets/CatalogGuildBadgeWidgetView'; @@ -23,7 +24,7 @@ export const CatalogLayouGuildCustomFurniView: FC = props => { !currentOffer && <> { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx index c21b204..21aabf4 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildForumView.tsx @@ -1,6 +1,6 @@ import { CatalogGroupsComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { SendMessageComposer } from '../../../../../api'; +import { SanitizeHtml, SendMessageComposer } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogFirstProductSelectorWidgetView } from '../widgets/CatalogFirstProductSelectorWidgetView'; @@ -26,7 +26,7 @@ export const CatalogLayouGuildForumView: FC = props => -
+
{ !!currentOffer && diff --git a/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx index 44f66b8..7a91686 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutGuildFrontpageView.tsx @@ -1,6 +1,6 @@ import { CreateLinkEvent } from '@nitrots/nitro-renderer'; import { FC } from 'react'; -import { LocalizeText } from '../../../../../api'; +import { LocalizeText, SanitizeHtml } from '../../../../../api'; import { Button } from '../../../../../common/Button'; import { Column } from '../../../../../common/Column'; import { Grid } from '../../../../../common/Grid'; @@ -14,9 +14,9 @@ export const CatalogLayouGuildFrontpageView: FC = props => return ( -
-
-
+
+
+
diff --git a/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx index a2a6a62..f5b1e4d 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutInfoLoyaltyView.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; export const CatalogLayoutInfoLoyaltyView: FC = props => @@ -8,7 +9,7 @@ export const CatalogLayoutInfoLoyaltyView: FC = props => return (
-
+
); diff --git a/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx b/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx index 8c2e085..d6dfe79 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutPets3View.tsx @@ -1,5 +1,6 @@ import { FC } from 'react'; -import { Column } from '../../../../../common'; +import { FaPaw } from 'react-icons/fa'; +import { SanitizeHtml } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; export const CatalogLayoutPets3View: FC = props => @@ -9,17 +10,28 @@ export const CatalogLayoutPets3View: FC = props => const imageUrl = page.localization.getImage(1); return ( - -
- { imageUrl && } -
+
+ { /* Header card */ } +
+ { imageUrl && } +
+
+ + +
+
- -
- -
-
+ + { /* Content */ } +
+
- + + { /* Footer */ } + { !!page.localization.getText(3) && +
+ +
} +
); }; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx index 54df095..44358ff 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutRoomBundleView.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; import { CatalogBundleGridWidgetView } from '../widgets/CatalogBundleGridWidgetView'; @@ -17,7 +18,7 @@ export const CatalogLayoutRoomBundleView: FC = props => { !!page.localization.getText(2) && - } + } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx index 99357a8..7b8f1ec 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutSingleBundleView.tsx @@ -1,4 +1,5 @@ import { FC } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; import { CatalogBundleGridWidgetView } from '../widgets/CatalogBundleGridWidgetView'; @@ -17,7 +18,7 @@ export const CatalogLayoutSingleBundleView: FC = props => { !!page.localization.getText(2) && - } + } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx index 0df267d..0aa131e 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutSoundMachineView.tsx @@ -1,6 +1,6 @@ import { GetOfficialSongIdMessageComposer, GetSoundManager, MusicPriorities, OfficialSongIdMessageEvent } from '@nitrots/nitro-renderer'; import { FC, useEffect, useState } from 'react'; -import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SendMessageComposer } from '../../../../../api'; +import { GetConfigurationValue, LocalizeText, ProductTypeEnum, SanitizeHtml, SendMessageComposer } from '../../../../../api'; import { Button, Column, Grid, LayoutImage, Text } from '../../../../../common'; import { useCatalog, useMessageEvent } from '../../../../../hooks'; import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView'; @@ -80,7 +80,7 @@ export const CatalogLayoutSoundMachineView: FC = props => <> { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx index 32865ce..9cd18b6 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutSpacesView.tsx @@ -1,4 +1,5 @@ import { FC, useEffect } from 'react'; +import { SanitizeHtml } from '../../../../../api'; import { Column, Grid, Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; @@ -26,7 +27,7 @@ export const CatalogLayoutSpacesView: FC = props => { !currentOffer && <> { !!page.localization.getImage(1) && } - + } { currentOffer && <> diff --git a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx index bcea3d6..e576695 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutTrophiesView.tsx @@ -1,6 +1,10 @@ import { FC, useEffect, useState } from 'react'; -import { Column, Grid, Text } from '../../../../../common'; +import { FaEdit, FaPen, FaPlus, FaTrophy } from 'react-icons/fa'; +import { LocalizeText, ProductTypeEnum, SanitizeHtml } from '../../../../../api'; +import { Text } from '../../../../../common'; import { useCatalog } from '../../../../../hooks'; +import { useCatalogAdmin } from '../../../CatalogAdminContext'; +import { CatalogAddOnBadgeWidgetView } from '../widgets/CatalogAddOnBadgeWidgetView'; import { CatalogItemGridWidgetView } from '../widgets/CatalogItemGridWidgetView'; import { CatalogPurchaseWidgetView } from '../widgets/CatalogPurchaseWidgetView'; import { CatalogTotalPriceWidget } from '../widgets/CatalogTotalPriceWidget'; @@ -12,6 +16,8 @@ export const CatalogLayoutTrophiesView: FC = props => const { page = null } = props; const [ trophyText, setTrophyText ] = useState(''); const { currentOffer = null, setPurchaseOptions = null } = useCatalog(); + const catalogAdmin = useCatalogAdmin(); + const adminMode = catalogAdmin?.adminMode ?? false; useEffect(() => { @@ -27,30 +33,104 @@ export const CatalogLayoutTrophiesView: FC = props => }); }, [ currentOffer, trophyText, setPurchaseOptions ]); + const canPurchase = currentOffer && trophyText.trim().length > 0; + return ( - - - -