Skip to content

Commit 5c77d57

Browse files
feat(Dealabs): add activity (#10324)
* feat: Add Dealabs presence Adds a new presence for Dealabs, the largest French community for sharing deals and coupons. **Features:** - 🛍️ **Browsing Deals:** Displays the deal title, price, and "temperature" (votes). - 💬 **Forum:** Shows current discussion topic or forum category. - 🏷️ **Merchants & Groups:** Updates when browsing specific merchants or categories. - 🌍 **Localization:** Fully translated in English and French. - ⚙️ **Settings:** - Privacy Mode (hides specific details globally). - Options to hide prices, images, or titles individually. **Technical details:** - Uses the new `getStrings` object mapping structure. - Validated with both privacy settings and various page types. * fix: Resolve linting errors and update logo dimensions Updates the logo to meet the 512x512px requirement and applies ESLint fixes (quotes, semicolons) to match project style. * Resolve linting errors and update logo dimensions * fix: Resolve final linting issues * fix(Dealabs): Address review requests (Localization, Metadata, Revert) - Revert accidental changes to `cli/src/commands/checkDns.ts` - Implement native localization system using `Dealabs.json` - Update thumbnail to a landscape promotional image in `metadata.json` - Use `ActivityType` enum in `presence.ts` instead of hardcoded value * Update: Conditional functionality added to buttons (settings) * fix translation * refactor: replace 'any' type with Translation interface Following code review feedback: - Removed usage of 'any' type for the strings object in presence.ts. - Created and exported a proper 'Translation' interface in util/interfaces.ts matching the JSON structure. - Cast fetched strings to the Translation interface to ensure type safety and prevent potential key errors. * Update index.ts
1 parent 72ddaa2 commit 5c77d57

File tree

10 files changed

+560
-0
lines changed

10 files changed

+560
-0
lines changed

websites/D/Dealabs/Dealabs.json

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{
2+
"dealabs.navigating": {
3+
"description": "Shown when browsing the main site",
4+
"message": "Browsing Dealabs"
5+
},
6+
"dealabs.hunting": {
7+
"description": "Shown when looking for deals generically",
8+
"message": "Hunting for deals"
9+
},
10+
"dealabs.viewDeal": {
11+
"description": "Shown when viewing a specific deal",
12+
"message": "Viewing a deal"
13+
},
14+
"dealabs.onDealabs": {
15+
"description": "State when on the website",
16+
"message": "On Dealabs"
17+
},
18+
"dealabs.onForum": {
19+
"description": "State when on the forum section",
20+
"message": "On the Forum"
21+
},
22+
"dealabs.readDiscuss": {
23+
"description": "Shown when reading a forum discussion",
24+
"message": "Reading a discussion"
25+
},
26+
"dealabs.searchCode": {
27+
"description": "Shown when looking for promo codes",
28+
"message": "Looking for a promo code"
29+
},
30+
"dealabs.viewCodes": {
31+
"description": "Button label to view codes",
32+
"message": "View codes"
33+
},
34+
"dealabs.browseDiscuss": {
35+
"description": "Shown when browsing the list of discussions",
36+
"message": "Browsing discussions"
37+
},
38+
"dealabs.detail": {
39+
"description": "Generic detail text",
40+
"message": "Detail"
41+
},
42+
"dealabs.free": {
43+
"description": "Indicates a free item",
44+
"message": "FREE"
45+
},
46+
"dealabs.hiddenDeal": {
47+
"description": "Text when deal titles are hidden by privacy mode",
48+
"message": "A deal"
49+
},
50+
"dealabs.hiddenTopic": {
51+
"description": "Text when topic titles are hidden by privacy mode",
52+
"message": "A discussion"
53+
},
54+
"dealabs.viewPage": {
55+
"description": "Button label to view the page",
56+
"message": "View page"
57+
},
58+
"dealabs.exploreCat": {
59+
"description": "Shown when browsing a category",
60+
"message": "Browsing a category"
61+
},
62+
"dealabs.homepage": {
63+
"description": "Shown when on the homepage",
64+
"message": "On the homepage"
65+
},
66+
"dealabs.forYou": {
67+
"description": "Shown when on the For You page",
68+
"message": "For You"
69+
},
70+
"dealabs.searchDeal": {
71+
"description": "Shown when searching",
72+
"message": "Searching for a deal"
73+
},
74+
"dealabs.newDeals": {
75+
"description": "Category: New Deals",
76+
"message": "New Deals 🕒"
77+
},
78+
"dealabs.hotDeals": {
79+
"description": "Category: Hot & Trending",
80+
"message": "Hot & Trending 🔥"
81+
},
82+
"dealabs.topDeals": {
83+
"description": "Category: Hottest Deals",
84+
"message": "Hottest Deals 🌶️"
85+
},
86+
"dealabs.searching": {
87+
"description": "Shown when searching",
88+
"message": "Searching"
89+
},
90+
"dealabs.searchingEllipsis": {
91+
"description": "Shown when searching (ellipsis)",
92+
"message": "Searching..."
93+
},
94+
"dealabs.unknown": {
95+
"description": "Generic unknown text",
96+
"message": "Unknown"
97+
},
98+
"dealabs.unknownCategory": {
99+
"description": "Text for unknown category",
100+
"message": "Unknown"
101+
}
102+
}

websites/D/Dealabs/metadata.json

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"$schema": "https://schemas.premid.app/metadata/1.16",
3+
"apiVersion": 1,
4+
"author": {
5+
"id": "188370726279970816",
6+
"name": "linkredible"
7+
},
8+
"service": "Dealabs",
9+
"description": {
10+
"en": "The largest community for sharing deals and coupons in France.",
11+
"fr": "La plus grande communauté de partage de bons plans et codes promo en France."
12+
},
13+
"url": "dealabs.com",
14+
"regExp": "^https?://(www\\.)?dealabs\\.com/.*",
15+
"version": "1.0.0",
16+
"logo": "https://i.imgur.com/Bg9ddtg.png",
17+
"thumbnail": "https://i.imgur.com/1nIb9QZ.png",
18+
"color": "#36bca1",
19+
"category": "socials",
20+
"tags": [
21+
"deals",
22+
"shopping",
23+
"community",
24+
"france",
25+
"multilanguage"
26+
],
27+
"settings": [
28+
{
29+
"id": "lang",
30+
"multiLanguage": true
31+
},
32+
{
33+
"id": "privacyMode",
34+
"title": "Mode confidentiel (Global)",
35+
"icon": "fas fa-user-secret",
36+
"placeholder": "Masquer tous les détails (titres, prix, images)",
37+
"value": false
38+
},
39+
{
40+
"id": "hideDealTitles",
41+
"title": "Masquer titres (Bons plans)",
42+
"icon": "fas fa-shopping-bag",
43+
"placeholder": "Remplace le titre du produit par un texte générique",
44+
"value": false,
45+
"if": {
46+
"privacyMode": false
47+
}
48+
},
49+
{
50+
"id": "hideImages",
51+
"title": "Masquer les images (Bons plans)",
52+
"icon": "fas fa-image",
53+
"placeholder": "Affiche toujours le logo Dealabs au lieu des produits",
54+
"value": false,
55+
"if": {
56+
"privacyMode": false
57+
}
58+
},
59+
{
60+
"id": "hidePrices",
61+
"title": "Masquer les prix",
62+
"icon": "fas fa-tag",
63+
"placeholder": "N'affiche pas le prix du deal dans le statut",
64+
"value": false,
65+
"if": {
66+
"privacyMode": false
67+
}
68+
},
69+
{
70+
"id": "hideDiscussionTitles",
71+
"title": "Masquer titres (Forum)",
72+
"icon": "fas fa-comments",
73+
"placeholder": "Remplace le sujet de la discussion par un texte générique",
74+
"value": false,
75+
"if": {
76+
"privacyMode": false
77+
}
78+
}
79+
]
80+
}

websites/D/Dealabs/presence.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import type { DealabsSettings, Resolver, Translation } from './util/interfaces.js'
2+
import { ActivityType } from 'premid'
3+
import dealResolver from './sources/deal.js'
4+
import discussionResolver from './sources/discussion.js'
5+
import groupResolver from './sources/group.js'
6+
import listingResolver from './sources/listing.js'
7+
import merchantResolver from './sources/merchant.js'
8+
9+
const presence = new Presence({
10+
clientId: '1456698196767277128',
11+
})
12+
13+
const browsingTimestamp = Math.floor(Date.now() / 1000)
14+
15+
enum ActivityAssets {
16+
Logo = 'https://i.imgur.com/Bg9ddtg.png',
17+
}
18+
19+
presence.on('UpdateData', async () => {
20+
const settings: DealabsSettings = {
21+
privacyMode: await presence.getSetting('privacyMode'),
22+
hideDealTitles: await presence.getSetting('hideDealTitles'),
23+
hideDiscussionTitles: await presence.getSetting('hideDiscussionTitles'),
24+
hideImages: await presence.getSetting('hideImages'),
25+
hidePrices: await presence.getSetting('hidePrices'),
26+
}
27+
28+
const strings = await presence.getStrings({
29+
navigating: 'dealabs.navigating',
30+
hunting: 'dealabs.hunting',
31+
viewDeal: 'dealabs.viewDeal',
32+
onDealabs: 'dealabs.onDealabs',
33+
onForum: 'dealabs.onForum',
34+
readDiscuss: 'dealabs.readDiscuss',
35+
searchCode: 'dealabs.searchCode',
36+
viewCodes: 'dealabs.viewCodes',
37+
browseDiscuss: 'dealabs.browseDiscuss',
38+
detail: 'dealabs.detail',
39+
free: 'dealabs.free',
40+
hiddenDeal: 'dealabs.hiddenDeal',
41+
hiddenTopic: 'dealabs.hiddenTopic',
42+
viewPage: 'dealabs.viewPage',
43+
exploreCat: 'dealabs.exploreCat',
44+
homepage: 'dealabs.homepage',
45+
forYou: 'dealabs.forYou',
46+
searchDeal: 'dealabs.searchDeal',
47+
newDeals: 'dealabs.newDeals',
48+
hotDeals: 'dealabs.hotDeals',
49+
topDeals: 'dealabs.topDeals',
50+
searching: 'dealabs.searching',
51+
searchingEllipsis: 'dealabs.searchingEllipsis',
52+
unknown: 'dealabs.unknown',
53+
unknownCategory: 'dealabs.unknownCategory',
54+
})
55+
56+
const t = strings as unknown as Translation
57+
58+
const presenceData: PresenceData = {
59+
largeImageKey: ActivityAssets.Logo,
60+
startTimestamp: browsingTimestamp,
61+
type: ActivityType.Watching,
62+
details: strings.navigating,
63+
state: strings.hunting,
64+
}
65+
66+
const pathname = document.location.pathname
67+
68+
if (settings.privacyMode) {
69+
if (pathname.includes('/bons-plans/') || pathname.includes('/codes-promo/')) {
70+
presenceData.details = strings.viewDeal
71+
presenceData.state = strings.onDealabs
72+
}
73+
else if (pathname.includes('/discussions/') || pathname.includes('/groupe/')) {
74+
presenceData.details = strings.onForum
75+
presenceData.state = strings.readDiscuss
76+
}
77+
else {
78+
presenceData.details = strings.navigating
79+
presenceData.state = strings.hunting
80+
}
81+
presence.setActivity(presenceData)
82+
return
83+
}
84+
85+
const resolvers: Resolver[] = [
86+
merchantResolver,
87+
dealResolver,
88+
discussionResolver,
89+
groupResolver,
90+
listingResolver,
91+
]
92+
93+
const activeResolver = resolvers.filter(r => r.isActive(pathname))[0]
94+
95+
if (activeResolver) {
96+
const state = activeResolver.getState(t, settings)
97+
const details = activeResolver.getDetails(t, settings)
98+
99+
if (state)
100+
presenceData.state = state
101+
if (details)
102+
presenceData.details = details
103+
104+
if (!settings.hideImages && activeResolver.getLargeImage) {
105+
const img = activeResolver.getLargeImage()
106+
if (img)
107+
presenceData.largeImageKey = img
108+
}
109+
110+
if (activeResolver.getButtons) {
111+
presenceData.buttons = activeResolver.getButtons(t)
112+
}
113+
114+
presenceData.smallImageKey = ActivityAssets.Logo
115+
presenceData.smallImageText = 'Dealabs'
116+
}
117+
118+
if (!presenceData.state || (typeof presenceData.state === 'string' && presenceData.state.length < 2)) {
119+
presenceData.state = strings.hunting
120+
}
121+
if (!presenceData.details || (typeof presenceData.details === 'string' && presenceData.details.length < 2)) {
122+
presenceData.details = strings.onDealabs
123+
}
124+
125+
presence.setActivity(presenceData)
126+
})

websites/D/Dealabs/sources/deal.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { ButtonArray, DealabsSettings, Resolver, Translation } from '../util/interfaces.js'
2+
import { getMainContent } from '../util/index.js'
3+
4+
const dealResolver: Resolver = {
5+
isActive: (pathname: string) => pathname.includes('/bons-plans/'),
6+
7+
getState: (t: Translation, settings: DealabsSettings) => {
8+
if (settings.hideDealTitles)
9+
return t.hiddenDeal
10+
11+
let title = document.querySelector('h1')?.textContent?.trim()
12+
if (!title) {
13+
const metaTitle = document.querySelector('meta[property="og:title"]')?.getAttribute('content')
14+
title = metaTitle ? ((metaTitle.split(' - Dealabs')[0] ?? metaTitle).trim()) : t.detail
15+
}
16+
return title
17+
},
18+
19+
getDetails: (t: Translation, settings: DealabsSettings) => {
20+
const mainContent = getMainContent()
21+
22+
let price: string | undefined
23+
if (!settings.hidePrices) {
24+
const priceEl = mainContent.querySelector('.thread-price')
25+
if (priceEl && priceEl.textContent) {
26+
price = priceEl.textContent.trim()
27+
}
28+
else {
29+
const freeLabel = mainContent.querySelector('.text--color-free')
30+
if (freeLabel || (mainContent.textContent && mainContent.textContent.includes(t.free) && !mainContent.textContent.includes('€'))) {
31+
price = t.free
32+
}
33+
}
34+
}
35+
36+
let temp: string | undefined
37+
const tempEl = mainContent.querySelector('.vote-temp')
38+
if (tempEl && tempEl.textContent) {
39+
temp = tempEl.textContent.trim()
40+
}
41+
42+
const parts: string[] = []
43+
if (temp) {
44+
let emoji = '❄️'
45+
const val = Number.parseInt(temp)
46+
if (!Number.isNaN(val)) {
47+
if (val > 100)
48+
emoji = '🔥'
49+
else if (val > 0)
50+
emoji = '🌡️'
51+
}
52+
parts.push(`${temp} ${emoji}`)
53+
}
54+
if (price)
55+
parts.push(price)
56+
57+
return parts.length > 0 ? parts.join(' • ') : t.viewDeal
58+
},
59+
60+
getLargeImage: () => {
61+
const image = document.querySelector('meta[property="og:image"]')?.getAttribute('content')
62+
if (image && (image.includes('logo-dark') || image.includes('logo-white') || image.includes('assets/img'))) {
63+
return undefined
64+
}
65+
return image || undefined
66+
},
67+
68+
getButtons: (t: Translation) => [{ label: t.viewPage, url: document.location.href }] as ButtonArray,
69+
}
70+
71+
export default dealResolver

0 commit comments

Comments
 (0)