@startinblox/components-ds4go 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/locales/en.xlf CHANGED
@@ -404,6 +404,64 @@
404
404
  <trans-unit id="sc01b3f089359860d">
405
405
  <source>Premium tier (40 calls/month)</source>
406
406
  </trans-unit>
407
+ <trans-unit id="s43d205c00fad9f7e">
408
+ <source>+<x id="0" equiv-text="${totalCategories - 3} ${msg(&quot;more&quot;)}"/></source>
409
+ </trans-unit>
410
+ <trans-unit id="s984a5e18d85b6d57">
411
+ <source><x id="0" equiv-text="${mediaCount} ${mediaCount &gt; 1 ? msg(&quot;medias&quot;) : msg(&quot;media&quot;)}"/></source>
412
+ </trans-unit>
413
+ <trans-unit id="sc792eb5e201cf4d0">
414
+ <source>medias</source>
415
+ </trans-unit>
416
+ <trans-unit id="sb333b8c7b846af83">
417
+ <source>media</source>
418
+ </trans-unit>
419
+ <trans-unit id="s09134b07b5aacab4">
420
+ <source>By</source>
421
+ </trans-unit>
422
+ <trans-unit id="sa5a5964988fb72cf">
423
+ <source>Yes, generate it!</source>
424
+ </trans-unit>
425
+ <trans-unit id="s06444a51bc5db27f">
426
+ <source>AI-Video generated!</source>
427
+ </trans-unit>
428
+ <trans-unit id="s11a98d9043541ac5">
429
+ <source>Your AI-Generated video has been generated and is available in your Tralalère space.</source>
430
+ </trans-unit>
431
+ <trans-unit id="s59faae16854d9291">
432
+ <source>Video already generated</source>
433
+ </trans-unit>
434
+ <trans-unit id="sb0bf78dc704ac07e">
435
+ <source>Generate an AI-Video with Tralalère (5€)</source>
436
+ </trans-unit>
437
+ <trans-unit id="sbe35fa6868f5d8c6">
438
+ <source>Published on</source>
439
+ </trans-unit>
440
+ <trans-unit id="s2a1231c2978c689e">
441
+ <source>Open link on a new tab</source>
442
+ </trans-unit>
443
+ <trans-unit id="s7d83729075105484">
444
+ <source>Media files</source>
445
+ </trans-unit>
446
+ <trans-unit id="s2cc7cd11d9b856cd">
447
+ <source>Categories</source>
448
+ </trans-unit>
449
+ <trans-unit id="s53cbe88b59dd0d59">
450
+ <source>Updated on</source>
451
+ </trans-unit>
452
+ <trans-unit id="s32375c74e64909b5">
453
+ <source>Generate with Tralalère</source>
454
+ </trans-unit>
455
+ <trans-unit id="s428d999c913eaca6">
456
+ <source>This will create an AI-Generated video of this fact.
457
+ You will be charged 5€ on your account.</source>
458
+ </trans-unit>
459
+ <trans-unit id="s8bf481a0b5d8a689">
460
+ <source>Generating...</source>
461
+ </trans-unit>
462
+ <trans-unit id="s638b60dba93b07ef">
463
+ <source>Please wait while Tralalère generates your AI-Video...</source>
464
+ </trans-unit>
407
465
  </body>
408
466
  </file>
409
467
  </xliff>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startinblox/components-ds4go",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Startin'blox DS4GO",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -317,11 +317,6 @@ export interface PolicyDefinition {
317
317
  policy?: OdrlPolicy;
318
318
  }
319
319
 
320
- export interface Asset {
321
- "@id": string;
322
- properties?: Record<string, any>;
323
- }
324
-
325
320
  // DSIF-specific types
326
321
  export interface DSIFRootAuthoritySector {
327
322
  sectorName: string;
@@ -0,0 +1,128 @@
1
+ import Ds4goCardFactStyle from "@styles/cards/ds4go-card-fact.scss?inline";
2
+ import { formatDate } from "@helpers";
3
+ import type { Category, Fact } from "@src/ds4go";
4
+ import { css, html, LitElement, nothing, unsafeCSS } from "lit";
5
+ import { customElement, property } from "lit/decorators.js";
6
+ import { msg, str } from "@lit/localize";
7
+
8
+ @customElement("ds4go-card-fact")
9
+ export class Ds4goCardFact extends LitElement {
10
+ static styles = css`
11
+ ${unsafeCSS(Ds4goCardFactStyle)};
12
+ `;
13
+
14
+ @property({ attribute: false })
15
+ object?: Fact;
16
+
17
+ private _getThumbnail(): string {
18
+ if (this.object?.enclosure) {
19
+ return String(this.object.enclosure);
20
+ }
21
+ // Default placeholder image
22
+ return "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='354' height='167' viewBox='0 0 354 167'%3E%3Crect width='354' height='167' fill='%23f0f0f0'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' font-family='sans-serif' font-size='14' fill='%23999'%3ENo Image%3C/text%3E%3C/svg%3E";
23
+ }
24
+
25
+ private _getFormattedDate(): string {
26
+ if (!this.object?.created_at) return "";
27
+ return formatDate(this.object.created_at, {
28
+ year: "numeric",
29
+ month: "long",
30
+ day: "numeric",
31
+ });
32
+ }
33
+
34
+ private _getTags() {
35
+ const tags: {
36
+ label: string;
37
+ type: string;
38
+ color?: string;
39
+ }[] = [];
40
+
41
+ // Add categories as tags (max 3, with overflow indicator)
42
+ (this.object?.categories || [])?.slice(0, 3).forEach((c: Category) => {
43
+ if (c.name) {
44
+ tags.push({
45
+ label: c.name,
46
+ type: "neutral",
47
+ });
48
+ }
49
+ });
50
+
51
+ const totalCategories = this.object?.categories?.length || 0;
52
+ if (totalCategories > 3) {
53
+ tags.push({
54
+ label: msg(str`+${totalCategories - 3} ${msg("more")}`),
55
+ type: "neutral",
56
+ });
57
+ }
58
+
59
+ // Add media count if available
60
+ const mediaCount = this.object?.medias?.length || 0;
61
+ if (mediaCount > 0) {
62
+ tags.push({
63
+ label: msg(
64
+ str`${mediaCount} ${mediaCount > 1 ? msg("medias") : msg("media")}`,
65
+ ),
66
+ type: "information",
67
+ });
68
+ }
69
+
70
+ return tags;
71
+ }
72
+
73
+ render() {
74
+ if (!this.object) return nothing;
75
+ const tags = this._getTags();
76
+ const formattedDate = this._getFormattedDate();
77
+
78
+ return html`<article class="vertical">
79
+ <header
80
+ class="image"
81
+ style='background-image: url("${this._getThumbnail()}")'
82
+ ></header>
83
+ <main>
84
+ ${formattedDate
85
+ ? html`<div class="meta-info">
86
+ <span class="date">${formattedDate}</span>
87
+ </div>`
88
+ : nothing}
89
+ <div class="content">
90
+ ${this.object?.name
91
+ ? html`<div class="card-header">
92
+ <tems-division
93
+ label=${this.object?.name}
94
+ type="h4"
95
+ ></tems-division>
96
+ </div>`
97
+ : nothing}
98
+ ${this.object?.description
99
+ ? html`<div class="card-content">
100
+ <tems-division
101
+ label=${this.object?.description}
102
+ type="body-m"
103
+ ></tems-division>
104
+ </div>`
105
+ : nothing}
106
+ ${tags.length > 0
107
+ ? html`<div class="tags">
108
+ ${tags.map(
109
+ (tag) =>
110
+ html`<tems-badge
111
+ size="sm"
112
+ label=${tag.label}
113
+ type=${tag.type}
114
+ color=${tag.color || nothing}
115
+ ></tems-badge>`,
116
+ )}
117
+ </div>`
118
+ : nothing}
119
+ ${this.object.author
120
+ ? html`<div class="meta-info">
121
+ <span class="author">${msg("By")} ${this.object.author}</span>
122
+ </div>`
123
+ : nothing}
124
+ </div>
125
+ </main>
126
+ </article>`;
127
+ }
128
+ }
@@ -0,0 +1,149 @@
1
+ import {
2
+ ComponentObjectsHandler,
3
+ filterObjectByDateAfter,
4
+ filterObjectById,
5
+ filterObjectByInterval,
6
+ filterObjectByNamedValue,
7
+ filterObjectByType,
8
+ filterObjectByValue,
9
+ } from "@helpers";
10
+ import type { Fact } from "@src/ds4go";
11
+ import type { Resource, SearchObject, UnknownResource } from "@src/component";
12
+ import { css, html, nothing, type PropertyValues } from "lit";
13
+ import { customElement, state } from "lit/decorators.js";
14
+
15
+ @customElement("ds4go-fact-holder")
16
+ export class Ds4goFactHolder extends ComponentObjectsHandler {
17
+ static styles = css`
18
+ .card-grid {
19
+ display: flex;
20
+ flex-direction: row;
21
+ flex-wrap: wrap;
22
+ gap: 20px;
23
+ }
24
+ .card-grid-vertical {
25
+ justify-content: stretch;
26
+ }
27
+ .card-grid-vertical ds4go-card-fact {
28
+ width: 354px;
29
+ height: auto;
30
+ }
31
+ `;
32
+
33
+ @state()
34
+ search: SearchObject[] = [];
35
+
36
+ @state()
37
+ protected _displayObjects: Resource[] = [];
38
+
39
+ protected filter(objects: Resource[], filters: SearchObject[] = []) {
40
+ if (!filters || filters.length === 0 || !objects || objects.length === 0) {
41
+ return objects;
42
+ }
43
+
44
+ const groupedFilters = new Map<string, SearchObject[]>();
45
+ for (const filter of filters) {
46
+ const groupKey = filter.name;
47
+ const group = groupedFilters.get(groupKey) || [];
48
+ group.push(filter);
49
+ groupedFilters.set(groupKey, group);
50
+ }
51
+
52
+ let currentFilteredObjects = [...objects];
53
+
54
+ for (const filterGroup of groupedFilters.values()) {
55
+ if (currentFilteredObjects.length === 0) {
56
+ break;
57
+ }
58
+
59
+ const tempResults: UnknownResource[] = [];
60
+ for (const filter of filterGroup) {
61
+ switch (filter.type) {
62
+ case "interval":
63
+ tempResults.push(
64
+ filterObjectByInterval(
65
+ currentFilteredObjects,
66
+ filter.name,
67
+ filter.value,
68
+ ),
69
+ );
70
+ break;
71
+ case "dateAfter":
72
+ tempResults.push(
73
+ filterObjectByDateAfter(
74
+ currentFilteredObjects,
75
+ filter.name,
76
+ filter.value,
77
+ ),
78
+ );
79
+ break;
80
+ case "matchId":
81
+ tempResults.push(
82
+ filterObjectById(
83
+ currentFilteredObjects,
84
+ filter.name,
85
+ filter.value,
86
+ ),
87
+ );
88
+ break;
89
+ case "matchType":
90
+ tempResults.push(
91
+ filterObjectByType(currentFilteredObjects, filter.value),
92
+ );
93
+ break;
94
+ case "exact":
95
+ tempResults.push(
96
+ filterObjectByNamedValue(
97
+ currentFilteredObjects,
98
+ filter.name,
99
+ filter.value,
100
+ ),
101
+ );
102
+ break;
103
+ default:
104
+ tempResults.push(
105
+ filterObjectByValue(currentFilteredObjects, filter.value),
106
+ );
107
+ }
108
+ }
109
+ currentFilteredObjects = [...tempResults.flat()] as Resource[];
110
+ }
111
+
112
+ return currentFilteredObjects;
113
+ }
114
+
115
+ protected _handleClickEvent(originalObj: Resource) {
116
+ this.dispatchEvent(new CustomEvent("clicked", { detail: originalObj }));
117
+ }
118
+
119
+ willUpdate(changedProperties: PropertyValues) {
120
+ if (changedProperties.has("objects") || changedProperties.has("search")) {
121
+ if (this.objects) {
122
+ this._displayObjects = this.filter(this.objects, this.search);
123
+ } else {
124
+ this._displayObjects = [];
125
+ }
126
+ this.dispatchEvent(
127
+ new CustomEvent("result-count", {
128
+ detail: this._displayObjects.length,
129
+ }),
130
+ );
131
+ }
132
+ }
133
+
134
+ render() {
135
+ if (!this._displayObjects || this._displayObjects.length === 0) {
136
+ return nothing;
137
+ }
138
+
139
+ return html`<div class="card-grid card-grid-vertical">
140
+ ${this._displayObjects.map((displayObj) => {
141
+ const fact = displayObj as Fact;
142
+ return html`<ds4go-card-fact
143
+ .object=${import.meta.env.DEV ? fact : nothing}
144
+ @click=${() => this._handleClickEvent(fact)}
145
+ ></ds4go-card-fact>`;
146
+ })}
147
+ </div>`;
148
+ }
149
+ }
@@ -0,0 +1,217 @@
1
+ import { formatDate, formatCase, OrbitComponent } from "@helpers";
2
+ import { localized, msg } from "@lit/localize";
3
+ import { Task } from "@lit/task";
4
+ import { PropertiesPicker } from "@src/component";
5
+ import type { Category, Fact } from "@src/ds4go";
6
+
7
+ import ModalStyle from "@styles/modal/ds4go-fact-modal.scss?inline";
8
+ import { css, html, nothing, unsafeCSS } from "lit";
9
+ import { customElement, state } from "lit/decorators.js";
10
+
11
+ @customElement("ds4go-fact-modal")
12
+ @localized()
13
+ export class Ds4goFactModal extends OrbitComponent {
14
+ constructor() {
15
+ super({ setupSubscriptions: false, ignoreRouter: true });
16
+ }
17
+
18
+ static styles = css`
19
+ ${unsafeCSS(ModalStyle)}
20
+ `;
21
+
22
+ cherryPickedProperties: PropertiesPicker[] = [
23
+ { key: "created_at", value: "created_at" },
24
+ { key: "updated_at", value: "updated_at", cast: formatDate },
25
+ { key: "name", value: "name" },
26
+ { key: "description", value: "description" },
27
+ { key: "author", value: "author" },
28
+ { key: "link", value: "link" },
29
+ { key: "enclosure", value: "enclosure" },
30
+ { key: "categories", value: "categories", expand: true },
31
+ { key: "medias", value: "medias", expand: true },
32
+ { key: "url", value: "url" },
33
+ // { key: "file_size", value: "file_size" },
34
+ // { key: "file_type", value: "file_type" },
35
+ // { key: "width", value: "width" },
36
+ // { key: "height", value: "height" },
37
+ // Can't get raw review json, as store does not support datas without ids
38
+ // { key: "review", value: "review" },
39
+ ];
40
+
41
+ @state()
42
+ disableButton = false;
43
+
44
+ _generateVideo() {
45
+ this.orbit?.Swal.fire({
46
+ title: msg("Generate with Tralalère"),
47
+ text: msg(
48
+ "This will create an AI-Generated video of this fact.\nYou will be charged 5€ on your account.",
49
+ ),
50
+ icon: "warning",
51
+ position: "center",
52
+ backdrop: true,
53
+ showCancelButton: true,
54
+ confirmButtonText: msg("Yes, generate it!"),
55
+ cancelButtonText: msg("Cancel"),
56
+ customClass: {
57
+ confirmButton:
58
+ "button text-xsmall text-bold text-center reversed color-secondary bordered icon icon-check icon-margin-right-xsmall no-background-image",
59
+ cancelButton:
60
+ "button text-xsmall text-bold text-center reversed color-primary bordered icon icon-exclamation icon-margin-right-xsmall no-background-image",
61
+ },
62
+ }).then((result: any) => {
63
+ if (result.isConfirmed) {
64
+ this.orbit?.Swal.fire({
65
+ title: msg("Generating..."),
66
+ text: msg("Please wait while Tralalère generates your AI-Video..."),
67
+ icon: "info",
68
+ showConfirmButton: false,
69
+ allowOutsideClick: false,
70
+ allowEscapeKey: false,
71
+ allowEnterKey: false,
72
+ });
73
+ setTimeout(() => {
74
+ this.orbit?.Swal.fire({
75
+ title: msg("AI-Video generated!"),
76
+ text: msg(
77
+ "Your AI-Generated video has been generated and is available in your Tralalère space.",
78
+ ),
79
+ icon: "success",
80
+ });
81
+ this.disableButton = true;
82
+ }, 2500);
83
+ }
84
+ });
85
+ }
86
+
87
+ _closeModal() {
88
+ this.dispatchEvent(new CustomEvent("close"));
89
+ }
90
+
91
+ _getResource = new Task(this, {
92
+ task: async () => {
93
+ if (!this.object) return;
94
+
95
+ this.object = await this._getProxyValue(this.object["@id"]);
96
+
97
+ return this.object;
98
+ },
99
+ args: () => [this.caching, this.currentRoute],
100
+ });
101
+
102
+ render() {
103
+ return this._getResource.render({
104
+ pending: () => html`<solid-loader></solid-loader>`,
105
+ complete: (obj?: Fact) => {
106
+ if (!obj) {
107
+ this._closeModal();
108
+ return nothing;
109
+ }
110
+
111
+ const formattedDate = formatDate(obj.created_at, {
112
+ year: "numeric",
113
+ month: "long",
114
+ day: "numeric",
115
+ hour: "2-digit",
116
+ minute: "2-digit",
117
+ hour12: false,
118
+ });
119
+
120
+ return html`<div class="modal">
121
+ <div class="topbar">
122
+ <tems-button
123
+ @click=${this._closeModal}
124
+ type="outline-gray"
125
+ .iconLeft=${html`<icon-material-symbols-close-rounded></icon-material-symbols-close-rounded>`}
126
+ ></tems-button>
127
+ <tems-button
128
+ @click=${this._generateVideo}
129
+ disabled=${this.disableButton || nothing}
130
+ >${this.disableButton
131
+ ? msg("Video already generated")
132
+ : msg("Generate an AI-Video with Tralalère (5€)")}</tems-button
133
+ >
134
+ </div>
135
+ <div class="modal-content-wrapper">
136
+ <div class="modal-box">
137
+ <div class="modal-content">
138
+ ${obj.enclosure
139
+ ? html`<div class="cover-image">
140
+ <img src=${String(obj.enclosure)} alt="Cover image" />
141
+ </div>`
142
+ : nothing}
143
+ ${obj.author
144
+ ? html`<tems-division type="body-sm" class="author-section">
145
+ <div>
146
+ ${msg("Published on")} ${formattedDate} | ${msg("By")}
147
+ ${String(obj.author)}
148
+ </div>
149
+ </tems-division>`
150
+ : nothing}
151
+ <tems-division type="h3"
152
+ ><div>${String(obj.name || "")}</div></tems-division
153
+ >
154
+ <tems-division type="body-m"
155
+ ><div>${String(obj.description || "")}</div></tems-division
156
+ >
157
+ ${obj.link
158
+ ? html`<tems-division type="body-sm">
159
+ <tems-button
160
+ type="outline-color"
161
+ .label=${msg("Open link on a new tab")}
162
+ @click=${() => window.open(String(obj.link), "_blank")}
163
+ ></tems-button>
164
+ </tems-division>`
165
+ : nothing}
166
+ ${(obj.medias || []).length > 0
167
+ ? html`<tems-division type="h4"
168
+ >${msg("Media files")}</tems-division
169
+ >
170
+ <div class="medias-gallery">
171
+ ${(obj.medias || []).map(
172
+ (media: any) =>
173
+ html`<div class="media-item">
174
+ ${media.url
175
+ ? html`<img
176
+ src=${String(media.url)}
177
+ alt=${media.description || "Media"}
178
+ @click=${() =>
179
+ window.open(String(media.url), "_blank")}
180
+ />`
181
+ : nothing}
182
+ </div>`,
183
+ )}
184
+ </div>`
185
+ : nothing}
186
+ ${(obj.categories || []).length > 0
187
+ ? html`<tems-division type="h4"
188
+ >${msg("Categories")}</tems-division
189
+ >
190
+ <div class="categories-list">
191
+ ${(obj.categories || []).map(
192
+ (c: Category) =>
193
+ html`<tems-badge type="neutral" size="sm"
194
+ >${formatCase(String(c.name))}</tems-badge
195
+ >`,
196
+ )}
197
+ </div>`
198
+ : nothing}
199
+ <tems-division type="body-sm"
200
+ ><div>
201
+ <div>${msg("Updated on")} ${String(obj.updated_at)}</div>
202
+ </div></tems-division
203
+ >
204
+ </div>
205
+ </div>
206
+ </div>
207
+ </div>`;
208
+ },
209
+ error: (e) => {
210
+ if (import.meta.env.DEV) {
211
+ console.error(e, this);
212
+ }
213
+ return nothing;
214
+ },
215
+ });
216
+ }
217
+ }