@mideind/netskrafl-react 1.0.0-beta.6 → 1.0.0-beta.8

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.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/css/netskrafl.css +1508 -215
  3. package/dist/cjs/index.js +9018 -7764
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/esm/css/netskrafl.css +1508 -215
  6. package/dist/esm/index.js +9017 -7764
  7. package/dist/esm/index.js.map +1 -1
  8. package/dist/types.d.ts +4 -1
  9. package/package.json +13 -2
  10. package/.eslintignore +0 -8
  11. package/.eslintrc.json +0 -13
  12. package/rollup.config.js +0 -67
  13. package/src/components/index.ts +0 -2
  14. package/src/components/netskrafl/Netskrafl.stories.tsx +0 -66
  15. package/src/components/netskrafl/Netskrafl.tsx +0 -155
  16. package/src/components/netskrafl/Netskrafl.types.ts +0 -7
  17. package/src/components/netskrafl/index.ts +0 -2
  18. package/src/css/fonts.css +0 -4
  19. package/src/css/glyphs.css +0 -223
  20. package/src/css/main.css +0 -4
  21. package/src/css/skrafl-explo.css +0 -6636
  22. package/src/fonts/glyphicons-regular.eot +0 -0
  23. package/src/fonts/glyphicons-regular.ttf +0 -0
  24. package/src/fonts/glyphicons-regular.woff +0 -0
  25. package/src/index.ts +0 -4
  26. package/src/messages/messages.json +0 -1576
  27. package/src/mithril/actions.ts +0 -319
  28. package/src/mithril/bag.ts +0 -65
  29. package/src/mithril/bestdisplay.ts +0 -74
  30. package/src/mithril/blankdialog.ts +0 -94
  31. package/src/mithril/board.ts +0 -339
  32. package/src/mithril/buttons.ts +0 -303
  33. package/src/mithril/challengedialog.ts +0 -186
  34. package/src/mithril/channel.ts +0 -162
  35. package/src/mithril/chat.ts +0 -228
  36. package/src/mithril/components.ts +0 -496
  37. package/src/mithril/dragdrop.ts +0 -219
  38. package/src/mithril/elopage.ts +0 -202
  39. package/src/mithril/friend.ts +0 -227
  40. package/src/mithril/game.ts +0 -1378
  41. package/src/mithril/gameview.ts +0 -111
  42. package/src/mithril/globalstate.ts +0 -33
  43. package/src/mithril/i18n.ts +0 -187
  44. package/src/mithril/localstorage.ts +0 -133
  45. package/src/mithril/login.ts +0 -122
  46. package/src/mithril/logo.ts +0 -323
  47. package/src/mithril/main.ts +0 -755
  48. package/src/mithril/mithril.ts +0 -29
  49. package/src/mithril/model.ts +0 -855
  50. package/src/mithril/movelistitem.ts +0 -226
  51. package/src/mithril/page.ts +0 -856
  52. package/src/mithril/playername.ts +0 -91
  53. package/src/mithril/promodialog.ts +0 -82
  54. package/src/mithril/recentlist.ts +0 -148
  55. package/src/mithril/request.ts +0 -52
  56. package/src/mithril/review.ts +0 -634
  57. package/src/mithril/rightcolumn.ts +0 -398
  58. package/src/mithril/searchbutton.ts +0 -118
  59. package/src/mithril/statsdisplay.ts +0 -109
  60. package/src/mithril/tabs.ts +0 -169
  61. package/src/mithril/tile.ts +0 -145
  62. package/src/mithril/twoletter.ts +0 -76
  63. package/src/mithril/types.ts +0 -384
  64. package/src/mithril/userinfodialog.ts +0 -171
  65. package/src/mithril/util.ts +0 -304
  66. package/src/mithril/wait.ts +0 -246
  67. package/src/mithril/wordcheck.ts +0 -102
  68. package/tsconfig.json +0 -28
  69. package/vite.config.ts +0 -12
@@ -1,186 +0,0 @@
1
- /*
2
-
3
- ChallengeDialog.ts
4
-
5
- Challenge dialog component
6
-
7
- Copyright (C) 2024 Miðeind ehf.
8
- Author: Vilhjalmur Thorsteinsson
9
-
10
- The Creative Commons Attribution-NonCommercial 4.0
11
- International Public License (CC-BY-NC 4.0) applies to this software.
12
- For further information, see https://github.com/mideind/Netskrafl
13
-
14
- */
15
-
16
- import { IView, UserListItem } from "./types";
17
- import { ComponentFunc, m } from "./mithril";
18
- import { mt, t, ts } from "./i18n";
19
- import { glyph, nbsp } from "./util";
20
- import { DialogButton, MultiSelection, OnlinePresence } from "./components";
21
-
22
- interface IAttributes {
23
- view: IView;
24
- item: UserListItem;
25
- }
26
-
27
- export const ChallengeDialog: ComponentFunc<IAttributes> = () => {
28
- // Show a dialog box for a new challenge being issued
29
- let manualChallenge = false;
30
- return {
31
- view: (vnode) => {
32
- const { view, item } = vnode.attrs;
33
- const model = view.model;
34
- const state = model.state;
35
- if (!state) return undefined;
36
- const manual = state.plan !== ""; // If subscriber/friend, allow manual challenges
37
- const fairPlay = item.fairplay && state.fairPlay; // Both users are fair-play
38
- return m(".modal-dialog",
39
- { id: 'chall-dialog', style: { visibility: 'visible' } },
40
- m(".ui-widget.ui-widget-content.ui-corner-all", { id: 'chall-form' },
41
- [
42
- m(".chall-hdr",
43
- m("table",
44
- m("tbody",
45
- m("tr",
46
- [
47
- m("td", m("h1.chall-icon", glyph("hand-right"))),
48
- m("td.l-border",
49
- [
50
- m(OnlinePresence, { id: "chall-online", userId: item.userid }),
51
- m("h1", item.nick),
52
- m("h2", item.fullname)
53
- ]
54
- )
55
- ]
56
- )
57
- )
58
- )
59
- ),
60
- m("div", { style: { "text-align": "center" } },
61
- [
62
- m(".promo-fullscreen",
63
- [
64
- mt("p", [mt("strong", "Ný áskorun"), " - veldu lengd viðureignar:"]),
65
- m(MultiSelection,
66
- { initialSelection: 0, defaultClass: 'chall-time' },
67
- [
68
- m("div", { id: 'chall-none', tabindex: 1 },
69
- t("Viðureign án klukku")
70
- ),
71
- m("div", { id: 'chall-10', tabindex: 2 },
72
- [glyph("time"), t("2 x 10 mínútur")]
73
- ),
74
- m("div", { id: 'chall-15', tabindex: 3 },
75
- [glyph("time"), t("2 x 15 mínútur")]
76
- ),
77
- m("div", { id: 'chall-20', tabindex: 4 },
78
- [glyph("time"), t("2 x 20 mínútur")]
79
- ),
80
- m("div", { id: 'chall-25', tabindex: 5 },
81
- [glyph("time"), t("2 x 25 mínútur")]
82
- ),
83
- state?.runningLocal ? // !!! TODO Debugging aid
84
- m("div", { id: 'chall-3', tabindex: 6 },
85
- [glyph("time"), t("2 x 3 mínútur")]
86
- )
87
- :
88
- m("div", { id: 'chall-30', tabindex: 6 },
89
- [glyph("time"), t("2 x 30 mínútur")]
90
- )
91
- ]
92
- )
93
- ]
94
- ),
95
- m(".promo-mobile",
96
- [
97
- m("p", mt("strong", "Ný áskorun")),
98
- m(".chall-time.selected",
99
- { id: 'extra-none', tabindex: 1 },
100
- t("Viðureign án klukku")
101
- )
102
- ]
103
- )
104
- ]
105
- ),
106
- manual ? m("div", { id: "chall-manual" },
107
- [
108
- mt("span.caption.wide",
109
- [
110
- "Nota ", mt("strong", "handvirka véfengingu"),
111
- m("br"), "(\"keppnishamur\")"
112
- ]
113
- ),
114
- m(".toggler[id='manual-toggler'][tabindex='7']",
115
- {
116
- onclick: (ev: Event) => {
117
- manualChallenge = !manualChallenge;
118
- ev.preventDefault();
119
- }
120
- },
121
- [
122
- m(`.option${manualChallenge ? "" : ".selected"}`,
123
- m("span", nbsp())
124
- ),
125
- m(`.option${manualChallenge ? ".selected" : ""}`,
126
- glyph("lightbulb")
127
- )
128
- ]
129
- )
130
- ]
131
- ) : "",
132
- fairPlay ? m("div", { id: "chall-fairplay" },
133
- [
134
- t("Báðir leikmenn lýsa því yfir að þeir skrafla "),
135
- m("br"),
136
- mt("strong", "án stafrænna hjálpartækja"),
137
- t(" af nokkru tagi"), "."
138
- ]
139
- ) : "",
140
- m(DialogButton,
141
- {
142
- id: "chall-cancel",
143
- title: ts("Hætta við"),
144
- tabindex: 8,
145
- onclick: (ev: Event) => {
146
- view.popDialog();
147
- ev.preventDefault();
148
- }
149
- },
150
- glyph("remove")
151
- ),
152
- m(DialogButton,
153
- {
154
- id: "chall-ok",
155
- title: ts("Skora á"),
156
- tabindex: 9,
157
- onclick: (ev: Event) => {
158
- // Issue a new challenge
159
- let duration: string | number =
160
- document.querySelector("div.chall-time.selected")?.id.slice(6) ?? "0";
161
- if (duration === "none")
162
- duration = 0;
163
- else
164
- duration = parseInt(duration);
165
- item.chall = true;
166
- model.modifyChallenge(
167
- {
168
- destuser: item.userid,
169
- action: "issue",
170
- duration: duration,
171
- fairplay: fairPlay,
172
- manual: manualChallenge
173
- }
174
- );
175
- view.popDialog();
176
- ev.preventDefault();
177
- }
178
- },
179
- glyph("ok")
180
- )
181
- ]
182
- )
183
- );
184
- }
185
- };
186
- };
@@ -1,162 +0,0 @@
1
- /*
2
-
3
- Channel.ts
4
-
5
- Utility functions for working with Firebase
6
-
7
- Copyright (C) 2024 Miðeind ehf.
8
- Original author: Vilhjálmur Þorsteinsson
9
-
10
- The Creative Commons Attribution-NonCommercial 4.0
11
- International Public License (CC-BY-NC 4.0) applies to this software.
12
- For further information, see https://github.com/mideind/Netskrafl
13
-
14
- */
15
-
16
- import { FirebaseApp, FirebaseOptions, initializeApp } from 'firebase/app';
17
- import { getAnalytics, logEvent as firebaseLogEvent, Analytics } from 'firebase/analytics';
18
- import { getAuth, signInWithCustomToken, onAuthStateChanged, Auth } from 'firebase/auth';
19
- import { Database, getDatabase, off, onDisconnect, onValue, push, ref, remove, set } from 'firebase/database';
20
-
21
- import { GlobalState } from "./globalstate";
22
-
23
- let app: FirebaseApp;
24
- let auth: Auth;
25
- let database: Database;
26
- let analytics: Analytics;
27
-
28
- function initFirebase(state: GlobalState) {
29
- try {
30
- const projectId = state.projectId;
31
- const firebaseOptions: FirebaseOptions = {
32
- projectId,
33
- apiKey: state.firebaseAPIKey,
34
- authDomain: `${projectId}.firebaseapp.com`,
35
- databaseURL: state.databaseURL,
36
- storageBucket: `${projectId}.firebasestorage.app`,
37
- messagingSenderId: state.firebaseSenderId,
38
- appId: state.firebaseAppId,
39
- measurementId: state.measurementId,
40
- };
41
- app = initializeApp(firebaseOptions, "netskrafl");
42
- if (!app) {
43
- console.error("Failed to initialize Firebase");
44
- }
45
- } catch (e) {
46
- console.error("Failed to initialize Firebase", e);
47
- }
48
- }
49
-
50
- export async function loginFirebase(
51
- state: GlobalState,
52
- firebaseToken: string,
53
- onLoginFunc?: () => void
54
- ) {
55
- if (!app) initFirebase(state);
56
- if (!app) return;
57
- const userId = state.userId;
58
- const locale = state.locale;
59
- auth = getAuth(app);
60
- if (!auth) {
61
- console.error("Failed to initialize Firebase Auth");
62
- } else {
63
- // Register our login function to execute once the user login is done
64
- onAuthStateChanged(auth,
65
- (user) => {
66
- const signedIn = (user !== null);
67
- if (signedIn) {
68
- // User is signed in
69
- onLoginFunc && onLoginFunc();
70
- // For new users, log an additional signup event
71
- if (state.newUser) {
72
- logEvent("sign_up",
73
- {
74
- locale: state.locale,
75
- method: state.loginMethod,
76
- userid: state.userId
77
- }
78
- );
79
- }
80
- // And always log a login event
81
- logEvent("login",
82
- {
83
- locale: state.locale,
84
- method: state.loginMethod,
85
- userid: state.userId
86
- }
87
- );
88
- } else {
89
- // No user is signed in
90
- }
91
- }
92
- );
93
- }
94
- // Log in to Firebase using the provided custom token
95
- await signInWithCustomToken(auth, firebaseToken);
96
- database = getDatabase(app);
97
- if (!database) {
98
- console.error("Failed to initialize Firebase Database");
99
- }
100
- analytics = getAnalytics(app);
101
- if (!analytics) {
102
- console.error("Failed to initialize Firebase Analytics");
103
- }
104
- initPresence(state.projectId, userId, locale);
105
- }
106
-
107
- function initPresence(projectId: string, userId: string, locale: string) {
108
- // Ensure that this user connection is recorded in Firebase
109
- if (!database) return;
110
- const connectedRef = ref(database, '.info/connected');
111
- // Create a unique connection entry for this user
112
- const connectionPath =
113
- projectId === "netskrafl"
114
- ? `connection/${userId}`
115
- : `connection/${locale}/${userId}`;
116
- const userRef = push(ref(database, connectionPath));
117
- onValue(connectedRef, (snapshot) => {
118
- if (snapshot.val()) {
119
- // We're connected (or reconnected)
120
- // When this user disconnects, remove this entry
121
- onDisconnect(userRef).remove();
122
- // Set presence
123
- set(userRef, true);
124
- }
125
- else
126
- // Unset presence
127
- remove(userRef);
128
- });
129
- }
130
-
131
- export function attachFirebaseListener(
132
- path: string, func: (json: any, firstAttach: boolean) => void
133
- ) {
134
- // Attach a message listener to a Firebase path
135
- if (!database) return;
136
- let cnt = 0;
137
- const pathRef = ref(database, path);
138
- onValue(pathRef, function(snapshot) {
139
- // Note: we need function() here for proper closure
140
- // The cnt variable is used to tell the listener whether it's being
141
- // called upon the first attach or upon a later data change
142
- cnt++;
143
- const json = snapshot.val();
144
- // console.log("received on path", path, ":", json);
145
- if (json) {
146
- func(json, cnt == 1);
147
- }
148
- });
149
- }
150
-
151
- export function detachFirebaseListener(path: string) {
152
- // Detach a message listener from a Firebase path
153
- if (!database) return;
154
- const pathRef = ref(database, path);
155
- off(pathRef);
156
- }
157
-
158
- export function logEvent(ev: string, params: any) {
159
- // Log a Firebase analytics event
160
- if (!analytics) return;
161
- firebaseLogEvent(analytics, ev, params);
162
- }
@@ -1,228 +0,0 @@
1
- /*
2
-
3
- Chat.ts
4
-
5
- Chat component
6
-
7
- Copyright (C) 2025 Miðeind ehf.
8
- Author: Vilhjalmur Thorsteinsson
9
-
10
- The Creative Commons Attribution-NonCommercial 4.0
11
- International Public License (CC-BY-NC 4.0) applies to this software.
12
- For further information, see https://github.com/mideind/Netskrafl
13
-
14
- */
15
-
16
- import { IView } from "./types";
17
- import { ts } from "./i18n";
18
- import { ComponentFunc, m, VnodeChildren, VnodeDOM } from "./mithril";
19
- import { escapeHtml, getInput, glyph, setInput } from "./util";
20
- import { DialogButton } from "./components";
21
- import { serverUrl } from "./request";
22
-
23
- // Max number of chat messages per game
24
- const MAX_CHAT_MESSAGES = 250;
25
-
26
- interface IAttributes {
27
- view: IView;
28
- }
29
-
30
- const EMOTICONS = [
31
- { icon: ":-)", image: "/static/icontexto_emoticons_03.png" },
32
- { icon: ":-D", image: "/static/icontexto_emoticons_02.png" },
33
- { icon: ";-)", image: "/static/icontexto_emoticons_04.png" },
34
- { icon: ":-(", image: "/static/icontexto_emoticons_12.png" },
35
- { icon: ":-o", image: "/static/icontexto_emoticons_10.png" },
36
- { icon: ":-O", image: "/static/icontexto_emoticons_10.png" },
37
- { icon: ":-p", image: "/static/icontexto_emoticons_14.png" },
38
- { icon: ":-P", image: "/static/icontexto_emoticons_14.png" },
39
- { icon: "B-)", image: "/static/icontexto_emoticons_16.png" },
40
- { icon: ":)", image: "/static/icontexto_emoticons_03.png" },
41
- { icon: ":D", image: "/static/icontexto_emoticons_02.png" },
42
- { icon: ";)", image: "/static/icontexto_emoticons_04.png" },
43
- { icon: ":(", image: "/static/icontexto_emoticons_12.png" },
44
- { icon: ":o", image: "/static/icontexto_emoticons_10.png" },
45
- { icon: ":O", image: "/static/icontexto_emoticons_10.png" },
46
- { icon: ":p", image: "/static/icontexto_emoticons_14.png" },
47
- { icon: ":P", image: "/static/icontexto_emoticons_14.png" },
48
- { icon: "(y)", image: "/static/thumb-up.png" }
49
- ];
50
-
51
- export const Chat: ComponentFunc<IAttributes> = (initialVnode) => {
52
- // The chat tab
53
-
54
- const { view } = initialVnode.attrs;
55
- const model = view.model;
56
- const game = model.game!;
57
- const state = model.state!;
58
-
59
- function decodeTimestamp(ts: string) {
60
- // Parse and split an ISO timestamp string, formatted as YYYY-MM-DD HH:MM:SS
61
- return {
62
- year: parseInt(ts.slice(0, 4)),
63
- month: parseInt(ts.slice(5, 7)),
64
- day: parseInt(ts.slice(8, 10)),
65
- hour: parseInt(ts.slice(11, 13)),
66
- minute: parseInt(ts.slice(14, 16)),
67
- second: parseInt(ts.slice(17, 19))
68
- };
69
- }
70
-
71
- function dateFromTimestamp(ts: string) {
72
- // Create a JavaScript millisecond-based representation of an ISO timestamp
73
- var dcTs = decodeTimestamp(ts);
74
- return Date.UTC(dcTs.year, dcTs.month - 1, dcTs.day,
75
- dcTs.hour, dcTs.minute, dcTs.second);
76
- }
77
-
78
- function timeDiff(dtFrom: number, dtTo: number) {
79
- // Return the difference between two JavaScript time points, in seconds
80
- return Math.round((dtTo - dtFrom) / 1000.0);
81
- }
82
-
83
- let dtLastMsg: number | null = null;
84
-
85
- function makeTimestamp(ts: string, key: number): VnodeChildren {
86
- // Decode the ISO format timestamp we got from the server
87
- let dtTs = dateFromTimestamp(ts);
88
- let result: VnodeChildren = null;
89
- if (dtLastMsg === null || timeDiff(dtLastMsg, dtTs) >= 5 * 60) {
90
- // If 5 minutes or longer interval between messages,
91
- // insert a time
92
- const ONE_DAY = 24 * 60 * 60 * 1000; // 24 hours expressed in milliseconds
93
- const dtNow = new Date().getTime();
94
- let dtToday = dtNow - dtNow % ONE_DAY; // Start of today (00:00 UTC)
95
- let dtYesterday = dtToday - ONE_DAY; // Start of yesterday
96
- let strTs: string;
97
- if (dtTs < dtYesterday) {
98
- // Older than today or yesterday: Show full timestamp YYYY-MM-DD HH:MM
99
- strTs = ts.slice(0, -3);
100
- } else if (dtTs < dtToday) {
101
- // Yesterday
102
- strTs = "Í gær " + ts.slice(11, 16);
103
- } else {
104
- // Today
105
- strTs = ts.slice(11, 16);
106
- }
107
- result = m(".chat-ts", { key: key }, strTs);
108
- }
109
- dtLastMsg = dtTs;
110
- return result;
111
- }
112
-
113
- const player = game ? game.player : null;
114
-
115
- function replaceEmoticons(str: string): string {
116
- // Replace all emoticon shortcuts in the string str with a corresponding image URL
117
- for (const emoticon of EMOTICONS)
118
- if (str.indexOf(emoticon.icon) >= 0) {
119
- // The string contains the emoticon: prepare to replace all occurrences
120
- const imgUrl = serverUrl(emoticon.image);
121
- const img = `<img src='${imgUrl}' height='32' width='32'>`;
122
- // Re the following trick, see https://stackoverflow.com/questions/1144783/
123
- // replacing-all-occurrences-of-a-string-in-javascript
124
- str = str.split(emoticon.icon).join(img);
125
- }
126
- return str;
127
- }
128
-
129
- function chatMessages(): VnodeChildren {
130
- let r: VnodeChildren = [];
131
- if (game?.chatLoading || !game?.messages)
132
- return r;
133
- var key = 0;
134
- const userId = model.state?.userId ?? "";
135
- for (const msg of game.messages) {
136
- let p = player ?? 0;
137
- if (msg.from_userid != userId)
138
- p = 1 - p;
139
- const mTs = makeTimestamp(msg.ts, key);
140
- if (mTs !== null) {
141
- r.push(mTs);
142
- key++;
143
- }
144
- let escMsg = escapeHtml(msg.msg);
145
- escMsg = replaceEmoticons(escMsg);
146
- r.push(m(".chat-msg" +
147
- (p === 0 ? ".left" : ".right") +
148
- (p === player ? ".local" : ".remote"),
149
- { key: key++ },
150
- m.trust(escMsg))
151
- );
152
- }
153
- return r;
154
- }
155
-
156
- function scrollChatToBottom(): void {
157
- // Scroll the last chat message into view
158
- const chatlist = document.querySelectorAll("#chat-area .chat-msg");
159
- if (!chatlist.length)
160
- return;
161
- const target: HTMLElement = chatlist[chatlist.length - 1] as HTMLElement;
162
- (target.parentNode as HTMLElement).scrollTop = target.offsetTop;
163
- }
164
-
165
- function focus(vnode: VnodeDOM) {
166
- // Put the focus on the DOM object associated with the vnode
167
- if (!view.isDialogShown()) {
168
- // Don't hijack the focus from a dialog overlay
169
- (vnode.dom as HTMLElement).focus();
170
- }
171
- }
172
-
173
- function sendMessage(): void {
174
- let msg = getInput("msg").trim();
175
- if (game && msg.length > 0) {
176
- game.sendMessage(msg);
177
- setInput("msg", "");
178
- }
179
- }
180
-
181
- const numMessages = game?.messages ? game.messages.length : 0;
182
-
183
- return {
184
- view: () => m(".chat-container",
185
- {
186
- style: "z-index: 6" // Appear on top of board on mobile
187
- // key: uuid
188
- },
189
- [
190
- m(".chat-area" + (game?.showClock() ? ".with-clock" : ""),
191
- {
192
- id: 'chat-area',
193
- // Make sure that we see the bottom-most chat message
194
- oncreate: scrollChatToBottom,
195
- onupdate: scrollChatToBottom
196
- },
197
- chatMessages()
198
- ),
199
- m(".chat-input",
200
- [
201
- m("input.chat-txt",
202
- {
203
- type: "text",
204
- id: "msg",
205
- name: "msg",
206
- maxlength: 254,
207
- disabled: (numMessages >= MAX_CHAT_MESSAGES),
208
- oncreate: (vnode) => { focus(vnode); },
209
- onupdate: (vnode) => { focus(vnode); },
210
- onkeypress: (ev: KeyboardEvent) => {
211
- if (ev.key == "Enter") { sendMessage(); ev.preventDefault(); }
212
- }
213
- }
214
- ),
215
- m(DialogButton,
216
- {
217
- id: "chat-send",
218
- title: ts("Senda"),
219
- onclick: (ev: Event) => { sendMessage(); ev.preventDefault(); }
220
- },
221
- glyph("chat"),
222
- )
223
- ]
224
- )
225
- ]
226
- )
227
- }
228
- };