@things-factory/gallery 0.0.0-stage → 10.1.223

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 (102) hide show
  1. package/client/bootstrap.ts +16 -0
  2. package/client/components/gallery-forms.ts +188 -0
  3. package/client/components/gallery-uploader.ts +557 -0
  4. package/client/components/gallery-viewer.ts +397 -0
  5. package/client/index.ts +1 -0
  6. package/client/pages/gallery-page.ts +567 -0
  7. package/client/route.ts +8 -0
  8. package/client/tsconfig.json +13 -0
  9. package/client/utils/gallery-api.ts +157 -0
  10. package/client/utils/gallery-badge.ts +44 -0
  11. package/client/utils/gallery-words.ts +22 -0
  12. package/client/utils/photo-prep.ts +93 -0
  13. package/dist-client/bootstrap.d.ts +1 -0
  14. package/dist-client/bootstrap.js +16 -0
  15. package/dist-client/bootstrap.js.map +1 -0
  16. package/dist-client/components/gallery-forms.d.ts +31 -0
  17. package/dist-client/components/gallery-forms.js +223 -0
  18. package/dist-client/components/gallery-forms.js.map +1 -0
  19. package/dist-client/components/gallery-uploader.d.ts +51 -0
  20. package/dist-client/components/gallery-uploader.js +588 -0
  21. package/dist-client/components/gallery-uploader.js.map +1 -0
  22. package/dist-client/components/gallery-viewer.d.ts +30 -0
  23. package/dist-client/components/gallery-viewer.js +416 -0
  24. package/dist-client/components/gallery-viewer.js.map +1 -0
  25. package/dist-client/index.d.ts +1 -0
  26. package/dist-client/index.js +2 -0
  27. package/dist-client/index.js.map +1 -0
  28. package/dist-client/pages/gallery-page.d.ts +55 -0
  29. package/dist-client/pages/gallery-page.js +586 -0
  30. package/dist-client/pages/gallery-page.js.map +1 -0
  31. package/dist-client/route.d.ts +1 -0
  32. package/dist-client/route.js +9 -0
  33. package/dist-client/route.js.map +1 -0
  34. package/dist-client/tsconfig.tsbuildinfo +1 -0
  35. package/dist-client/utils/gallery-api.d.ts +95 -0
  36. package/dist-client/utils/gallery-api.js +46 -0
  37. package/dist-client/utils/gallery-api.js.map +1 -0
  38. package/dist-client/utils/gallery-badge.d.ts +4 -0
  39. package/dist-client/utils/gallery-badge.js +45 -0
  40. package/dist-client/utils/gallery-badge.js.map +1 -0
  41. package/dist-client/utils/gallery-words.d.ts +7 -0
  42. package/dist-client/utils/gallery-words.js +20 -0
  43. package/dist-client/utils/gallery-words.js.map +1 -0
  44. package/dist-client/utils/photo-prep.d.ts +10 -0
  45. package/dist-client/utils/photo-prep.js +102 -0
  46. package/dist-client/utils/photo-prep.js.map +1 -0
  47. package/dist-server/attachment-reader.d.ts +1 -0
  48. package/dist-server/attachment-reader.js +13 -0
  49. package/dist-server/attachment-reader.js.map +1 -0
  50. package/dist-server/index.d.ts +1 -0
  51. package/dist-server/index.js +11 -0
  52. package/dist-server/index.js.map +1 -0
  53. package/dist-server/role-tasks.d.ts +2 -0
  54. package/dist-server/role-tasks.js +15 -0
  55. package/dist-server/role-tasks.js.map +1 -0
  56. package/dist-server/service/gallery/gallery-resolver.d.ts +29 -0
  57. package/dist-server/service/gallery/gallery-resolver.js +602 -0
  58. package/dist-server/service/gallery/gallery-resolver.js.map +1 -0
  59. package/dist-server/service/gallery/gallery-rules.d.ts +88 -0
  60. package/dist-server/service/gallery/gallery-rules.js +181 -0
  61. package/dist-server/service/gallery/gallery-rules.js.map +1 -0
  62. package/dist-server/service/gallery/gallery-service.d.ts +37 -0
  63. package/dist-server/service/gallery/gallery-service.js +234 -0
  64. package/dist-server/service/gallery/gallery-service.js.map +1 -0
  65. package/dist-server/service/gallery/gallery-types.d.ts +62 -0
  66. package/dist-server/service/gallery/gallery-types.js +227 -0
  67. package/dist-server/service/gallery/gallery-types.js.map +1 -0
  68. package/dist-server/service/gallery/gallery.d.ts +83 -0
  69. package/dist-server/service/gallery/gallery.js +330 -0
  70. package/dist-server/service/gallery/gallery.js.map +1 -0
  71. package/dist-server/service/gallery/index.d.ts +4 -0
  72. package/dist-server/service/gallery/index.js +8 -0
  73. package/dist-server/service/gallery/index.js.map +1 -0
  74. package/dist-server/service/index.d.ts +7 -0
  75. package/dist-server/service/index.js +23 -0
  76. package/dist-server/service/index.js.map +1 -0
  77. package/dist-server/tsconfig.tsbuildinfo +1 -0
  78. package/package.json +48 -4
  79. package/server/attachment-reader.ts +14 -0
  80. package/server/index.ts +9 -0
  81. package/server/role-tasks.ts +14 -0
  82. package/server/service/gallery/gallery-resolver.ts +541 -0
  83. package/server/service/gallery/gallery-rules.ts +187 -0
  84. package/server/service/gallery/gallery-service.ts +254 -0
  85. package/server/service/gallery/gallery-types.ts +151 -0
  86. package/server/service/gallery/gallery.ts +266 -0
  87. package/server/service/gallery/index.ts +5 -0
  88. package/server/service/index.ts +19 -0
  89. package/server/tsconfig.json +11 -0
  90. package/test/gallery-persistence.test.ts +372 -0
  91. package/test/gallery-rules.test.ts +140 -0
  92. package/test/open-doors.test.ts +62 -0
  93. package/test/photo-prep.test.ts +61 -0
  94. package/test/translations.test.ts +68 -0
  95. package/things-factory.config.js +8 -0
  96. package/translations/en.json +91 -0
  97. package/translations/es.json +91 -0
  98. package/translations/ja.json +91 -0
  99. package/translations/ko.json +91 -0
  100. package/translations/ms.json +91 -0
  101. package/translations/zh.json +91 -0
  102. package/README.md +0 -3
@@ -0,0 +1,567 @@
1
+ import '@material/web/icon/icon.js'
2
+ import '@operato/input/ox-input-file.js'
3
+ import '../components/gallery-forms.js'
4
+ import '../components/gallery-uploader.js'
5
+ import '../components/gallery-viewer.js'
6
+
7
+ import { css, html, nothing } from 'lit'
8
+ import { customElement, query, state } from 'lit/decorators.js'
9
+
10
+ import { i18next, localize } from '@operato/i18n'
11
+ import { notify, openPopup } from '@operato/layout'
12
+ import { navigate, PageView, store } from '@operato/shell'
13
+ import { ScrollbarStyles } from '@operato/styles'
14
+ import { isMobileDevice } from '@operato/utils'
15
+ import { hasPrivilege } from '@things-factory/auth-base/dist-client'
16
+
17
+ import {
18
+ galleryApi,
19
+ type EventCard,
20
+ type EventFacts,
21
+ type Filter,
22
+ type Post,
23
+ type Space
24
+ } from '../utils/gallery-api.js'
25
+ import { refreshGalleryBadge } from '../utils/gallery-badge.js'
26
+ import { day, days, reasonOf, t } from '../utils/gallery-words.js'
27
+
28
+ type Tab = 'company' | 'events' | 'all' | 'mine'
29
+ const TABS: Tab[] = ['company', 'events', 'all', 'mine']
30
+ const PAGE = 30
31
+
32
+ /**
33
+ * 갤러리 — the company's photos, its events, everyone's, and one's own (ADR-0165 decisions 2, 6). One page: the tabs are
34
+ * its ways in, and an event, a person or a tag narrows it (`?event=` · `?author=` · `?tag=`; `?post=` opens one post).
35
+ * Posting takes only photos - 「사진 올리기」, and on a phone 「찍기」 opens the camera at once.
36
+ */
37
+ @customElement('gallery-page')
38
+ export class GalleryPage extends localize(i18next)(PageView) {
39
+ static styles = [
40
+ ScrollbarStyles,
41
+ css`
42
+ :host {
43
+ display: block;
44
+ overflow: auto;
45
+ padding: var(--spacing-large, 12px) var(--spacing-large, 16px);
46
+ box-sizing: border-box;
47
+ background: var(--md-sys-color-surface);
48
+ color: var(--md-sys-color-on-surface);
49
+ }
50
+ .tabs {
51
+ display: flex;
52
+ gap: var(--spacing-small, 4px);
53
+ overflow-x: auto;
54
+ margin-bottom: var(--spacing-medium, 8px);
55
+ }
56
+ .tabs button {
57
+ flex: none;
58
+ padding: 4px var(--spacing-large, 12px);
59
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
60
+ border-radius: 999px;
61
+ background: none;
62
+ color: var(--md-sys-color-on-surface);
63
+ font: inherit;
64
+ font-size: var(--ui-t-body, 14px);
65
+ cursor: pointer;
66
+ }
67
+ .tabs button[aria-selected='true'] {
68
+ border-color: var(--md-sys-color-primary);
69
+ background: var(--md-sys-color-primary-container);
70
+ color: var(--md-sys-color-on-primary-container);
71
+ }
72
+ .narrow {
73
+ display: flex;
74
+ flex-wrap: wrap;
75
+ align-items: baseline;
76
+ gap: var(--spacing-small, 4px) var(--spacing-medium, 8px);
77
+ margin-bottom: var(--spacing-medium, 8px);
78
+ }
79
+ .narrow h2 {
80
+ margin: 0;
81
+ font-size: var(--ui-t-title, 18px);
82
+ }
83
+ .narrow .muted {
84
+ font-size: var(--ui-t-small, 12px);
85
+ }
86
+ .chips {
87
+ display: flex;
88
+ flex-wrap: wrap;
89
+ gap: var(--spacing-small, 4px);
90
+ margin-bottom: var(--spacing-medium, 8px);
91
+ }
92
+ .chip,
93
+ .link {
94
+ display: inline-flex;
95
+ align-items: center;
96
+ gap: 2px;
97
+ padding: 2px var(--spacing-medium, 8px);
98
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
99
+ border-radius: 999px;
100
+ background: none;
101
+ color: var(--md-sys-color-on-surface);
102
+ font: inherit;
103
+ font-size: var(--ui-t-small, 12px);
104
+ line-height: 1.6;
105
+ cursor: pointer;
106
+ }
107
+ .chip[aria-pressed='true'] {
108
+ border-color: var(--md-sys-color-primary);
109
+ color: var(--md-sys-color-primary);
110
+ }
111
+ .chip md-icon,
112
+ .link md-icon {
113
+ --md-icon-size: 14px;
114
+ }
115
+ .link {
116
+ border: 0;
117
+ padding: 0;
118
+ color: var(--md-sys-color-primary);
119
+ }
120
+ .muted {
121
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
122
+ }
123
+ .grid {
124
+ display: grid;
125
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
126
+ gap: var(--spacing-medium, 8px);
127
+ }
128
+ .card {
129
+ display: flex;
130
+ flex-direction: column;
131
+ gap: 2px;
132
+ padding: 0;
133
+ border: 0;
134
+ background: none;
135
+ color: inherit;
136
+ font: inherit;
137
+ text-align: start;
138
+ cursor: pointer;
139
+ min-width: 0;
140
+ }
141
+ .picture {
142
+ position: relative;
143
+ width: 100%;
144
+ aspect-ratio: 1;
145
+ border-radius: var(--border-radius, 6px);
146
+ overflow: hidden;
147
+ background: var(--md-sys-color-surface-container-high);
148
+ }
149
+ .picture img {
150
+ width: 100%;
151
+ height: 100%;
152
+ object-fit: cover;
153
+ display: block;
154
+ transition: transform 160ms ease-out;
155
+ }
156
+ .card:hover .picture img {
157
+ transform: scale(1.03);
158
+ }
159
+ .badge {
160
+ position: absolute;
161
+ display: inline-flex;
162
+ align-items: center;
163
+ gap: 2px;
164
+ padding: 0 6px;
165
+ border-radius: 999px;
166
+ background: rgb(0 0 0 / 50%);
167
+ color: #fff;
168
+ font-size: var(--ui-t-small, 12px);
169
+ line-height: 1.6;
170
+ }
171
+ .badge md-icon {
172
+ --md-icon-size: 14px;
173
+ }
174
+ .badge.likes {
175
+ bottom: 4px;
176
+ inset-inline-start: 4px;
177
+ }
178
+ .badge.likes md-icon {
179
+ font-variation-settings: 'FILL' 0;
180
+ }
181
+ .badge.likes[liked] md-icon {
182
+ color: #ff6b6b;
183
+ font-variation-settings: 'FILL' 1;
184
+ }
185
+ .badge.count {
186
+ top: 4px;
187
+ inset-inline-end: 4px;
188
+ }
189
+ .badge.hidden {
190
+ top: 4px;
191
+ inset-inline-start: 4px;
192
+ background: var(--md-sys-color-error);
193
+ }
194
+ .under {
195
+ display: flex;
196
+ gap: var(--spacing-small, 4px);
197
+ font-size: var(--ui-t-small, 12px);
198
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
199
+ overflow: hidden;
200
+ white-space: nowrap;
201
+ }
202
+ .under .name {
203
+ overflow: hidden;
204
+ text-overflow: ellipsis;
205
+ }
206
+ .title {
207
+ font-size: var(--ui-t-body, 14px);
208
+ font-weight: 600;
209
+ overflow: hidden;
210
+ text-overflow: ellipsis;
211
+ white-space: nowrap;
212
+ }
213
+ .empty {
214
+ display: flex;
215
+ flex-direction: column;
216
+ align-items: center;
217
+ gap: var(--spacing-medium, 8px);
218
+ padding: 48px var(--spacing-large, 16px);
219
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
220
+ text-align: center;
221
+ }
222
+ .empty md-icon {
223
+ --md-icon-size: 40px;
224
+ }
225
+ .empty button,
226
+ .more {
227
+ padding: 6px var(--spacing-large, 16px);
228
+ border: 1px solid var(--md-sys-color-primary);
229
+ border-radius: 999px;
230
+ background: none;
231
+ color: var(--md-sys-color-primary);
232
+ font: inherit;
233
+ cursor: pointer;
234
+ }
235
+ .more {
236
+ display: block;
237
+ margin: var(--spacing-large, 16px) auto 0;
238
+ }
239
+ ox-input-file {
240
+ display: none;
241
+ }
242
+ @media (max-width: 460px) {
243
+ :host {
244
+ padding: var(--spacing-medium, 8px);
245
+ }
246
+ .grid {
247
+ grid-template-columns: repeat(2, minmax(0, 1fr));
248
+ gap: var(--spacing-small, 4px);
249
+ }
250
+ }
251
+ `
252
+ ]
253
+
254
+ @state() private tab: Tab = 'company'
255
+ @state() private eventId?: string
256
+ @state() private authorId?: string
257
+ @state() private tag?: string
258
+ @state() private posts: Post[] = []
259
+ @state() private total = 0
260
+ @state() private events: EventCard[] = []
261
+ @state() private tags: { tag: string; count: number }[] = []
262
+ @state() private event?: EventFacts
263
+ @state() private loading = false
264
+ @state() private moderator = false
265
+
266
+ @query('#pick') private picker!: any
267
+ @query('#shoot') private camera!: any
268
+
269
+ private onChanged = () => this.load()
270
+
271
+ private get me(): string | undefined {
272
+ return (store.getState() as any).auth?.user?.id
273
+ }
274
+
275
+ get context() {
276
+ const actions: any[] = []
277
+ if (this.tag && this.moderator)
278
+ actions.push({ title: t('gallery.button.rename-tag'), icon: 'sell', action: () => this.renameTag() })
279
+ if (this.tab === 'events' && !this.eventId && this.moderator)
280
+ actions.push({ title: t('gallery.button.create-event'), icon: 'event', action: () => this.makeEvent() })
281
+ if (this.tab !== 'events' || this.eventId) {
282
+ if (isMobileDevice())
283
+ actions.push({ title: t('gallery.button.shoot'), icon: 'photo_camera', action: () => this.camera?.pick() })
284
+ actions.push({
285
+ title: t('gallery.button.post'),
286
+ icon: 'add_photo_alternate',
287
+ emphasis: { raised: true },
288
+ action: () => this.picker?.pick()
289
+ })
290
+ }
291
+ return { title: t('gallery.title.page'), actions }
292
+ }
293
+
294
+ render() {
295
+ return html`
296
+ <ox-input-file
297
+ id="pick"
298
+ quiet
299
+ outside-trigger
300
+ hide-filelist
301
+ multiple
302
+ accept="image/*"
303
+ @change=${(e: CustomEvent) => this.picked(e)}
304
+ ></ox-input-file>
305
+ <ox-input-file
306
+ id="shoot"
307
+ quiet
308
+ outside-trigger
309
+ hide-filelist
310
+ accept="image/*"
311
+ capture="environment"
312
+ @change=${(e: CustomEvent) => this.picked(e)}
313
+ ></ox-input-file>
314
+ <div class="tabs" role="tablist">
315
+ ${TABS.map(tab => html`<button role="tab" aria-selected=${String(this.tab === tab && !this.narrowed)} @click=${() => navigate(`gallery-page?tab=${tab}`)}>${t(`gallery.tab.${tab}`)}</button>`)}
316
+ </div>
317
+ ${this.renderNarrowing()} ${this.tab === 'events' && !this.eventId ? this.renderEvents() : this.renderPosts()}
318
+ `
319
+ }
320
+
321
+ private get narrowed() {
322
+ return !!(this.eventId || this.tag || (this.authorId && this.authorId !== this.me))
323
+ }
324
+
325
+ private renderNarrowing() {
326
+ if (this.eventId) {
327
+ const event = this.event
328
+ return html`<div class="narrow">
329
+ <h2>${event?.name ?? ''}</h2>
330
+ ${event ? html`<span class="muted">${days(event.startAt, event.endAt)}${event.location ? ` · ${event.location}` : ''}</span>` : nothing}
331
+ <button class="link" @click=${() => navigate('gallery-page?tab=events')}>
332
+ <md-icon>arrow_back</md-icon>${t('gallery.tab.events')}
333
+ </button>
334
+ </div>`
335
+ }
336
+ if (this.authorId && this.authorId !== this.me) {
337
+ const name = this.posts[0]?.creator?.name ?? ''
338
+ return html`<div class="narrow"><h2>${t('gallery.text.persons-gallery', { name })}</h2></div>`
339
+ }
340
+ if (this.tag) {
341
+ return html`<div class="narrow">
342
+ <h2>#${this.tag}</h2>
343
+ <button class="link" @click=${() => navigate(`gallery-page?tab=${this.tab}`)}>
344
+ <md-icon>close</md-icon>${t('gallery.button.clear')}
345
+ </button>
346
+ </div>`
347
+ }
348
+ if (this.tab === 'events' || !this.tags.length) return nothing
349
+ return html`<div class="chips">
350
+ ${this.tags.map(one => html`<button class="chip" @click=${() => navigate(`gallery-page?tab=${this.tab}&tag=${encodeURIComponent(one.tag)}`)}>#${one.tag}</button>`)}
351
+ </div>`
352
+ }
353
+
354
+ private renderPosts() {
355
+ if (!this.posts.length) {
356
+ return this.loading
357
+ ? nothing
358
+ : html`<div class="empty">
359
+ <md-icon>photo_library</md-icon>
360
+ <div>
361
+ ${this.eventId ? t('gallery.text.empty-event') : this.tab === 'mine' ? t('gallery.text.empty-mine') : t('gallery.text.empty')}
362
+ </div>
363
+ <button @click=${() => this.picker?.pick()}>${t('gallery.button.post')}</button>
364
+ </div>`
365
+ }
366
+ return html`<div class="grid">
367
+ ${this.posts.map(
368
+ post =>
369
+ html`<button class="card" @click=${() => this.open(post)}>
370
+ <div class="picture">
371
+ ${post.photos[0] ? html`<img loading="lazy" src=${post.photos[0].thumbnail || post.photos[0].fullpath} alt=${post.caption ?? ''} />` : nothing}
372
+ ${post.hiddenAt ? html`<span class="badge hidden"><md-icon>visibility_off</md-icon>${t('gallery.text.hidden')}</span>` : nothing}
373
+ ${post.photos.length > 1 ? html`<span class="badge count"><md-icon>photo_library</md-icon>${post.photos.length}</span>` : nothing}
374
+ ${post.likeCount || post.liked ? html`<span class="badge likes" ?liked=${post.liked}><md-icon>favorite</md-icon>${post.likeCount}</span>` : nothing}
375
+ </div>
376
+ <div class="under">
377
+ <span class="name">${post.creator?.name ?? ''}</span><span>·</span><span>${day(post.takenAt)}</span>
378
+ </div>
379
+ </button>`
380
+ )}
381
+ </div>
382
+ ${this.posts.length < this.total ? html`<button class="more" ?disabled=${this.loading} @click=${() => this.load(true)}>${t('gallery.button.more')}</button>` : nothing}`
383
+ }
384
+
385
+ private renderEvents() {
386
+ if (!this.events.length) {
387
+ return this.loading
388
+ ? nothing
389
+ : html`<div class="empty">
390
+ <md-icon>event</md-icon>
391
+ <div>${t('gallery.text.empty-events')}</div>
392
+ ${this.moderator ? html`<button @click=${() => this.makeEvent()}>${t('gallery.button.create-event')}</button>` : nothing}
393
+ </div>`
394
+ }
395
+ return html`<div class="grid">
396
+ ${this.events.map(
397
+ event =>
398
+ html`<button class="card" @click=${() => navigate(`gallery-page?event=${event.id}`)}>
399
+ <div class="picture">
400
+ ${event.cover ? html`<img loading="lazy" src=${event.cover.thumbnail || event.cover.fullpath} alt="" />` : nothing}
401
+ <span class="badge count"><md-icon>photo_library</md-icon>${event.postCount}</span>
402
+ </div>
403
+ <div class="title">${event.name}</div>
404
+ <div class="under">
405
+ <span>${days(event.startAt, event.endAt)}</span
406
+ >${event.location ? html`<span>·</span><span class="name">${event.location}</span>` : nothing}
407
+ </div>
408
+ </button>`
409
+ )}
410
+ </div>`
411
+ }
412
+
413
+ connectedCallback() {
414
+ super.connectedCallback()
415
+ window.addEventListener('gallery-changed', this.onChanged)
416
+ }
417
+
418
+ disconnectedCallback() {
419
+ window.removeEventListener('gallery-changed', this.onChanged)
420
+ super.disconnectedCallback()
421
+ }
422
+
423
+ async pageInitialized() {
424
+ hasPrivilege({ category: 'gallery', privilege: 'mutation', domainOwnerGranted: true }).then(moderator => {
425
+ this.moderator = !!moderator
426
+ this.updateContext()
427
+ })
428
+ }
429
+
430
+ async pageUpdated(changes: any, lifecycle: any) {
431
+ if (!this.active || !(changes.active || changes.params)) return
432
+ const params = lifecycle?.params ?? {}
433
+ this.eventId = params.event || undefined
434
+ this.tag = params.tag || undefined
435
+ this.authorId = params.author === 'me' ? this.me : params.author || undefined
436
+ this.tab = this.eventId
437
+ ? 'events'
438
+ : this.authorId === this.me && this.authorId
439
+ ? 'mine'
440
+ : TABS.includes(params.tab)
441
+ ? params.tab
442
+ : 'company'
443
+ this.event = undefined
444
+ this.posts = []
445
+ this.updateContext()
446
+ await this.load()
447
+ if (params.post) this.openById(params.post)
448
+ galleryApi
449
+ .visit()
450
+ .then(() => refreshGalleryBadge())
451
+ .catch(() => {})
452
+ }
453
+
454
+ private filter(): Filter {
455
+ if (this.eventId) return { eventId: this.eventId }
456
+ const filter: Filter = {}
457
+ if (this.tag) filter.tag = this.tag
458
+ if (this.authorId) filter.authorId = this.authorId
459
+ else if (this.tab === 'company') filter.space = 'COMPANY'
460
+ else if (this.tab === 'mine' && this.me) filter.authorId = this.me
461
+ return filter
462
+ }
463
+
464
+ private async load(more = false) {
465
+ this.loading = true
466
+ try {
467
+ if (this.tab === 'events' && !this.eventId) {
468
+ this.events = await galleryApi.events()
469
+ return
470
+ }
471
+ const page = await galleryApi.posts(this.filter(), more ? this.posts.length : 0, PAGE)
472
+ this.posts = more ? [...this.posts, ...page.items] : page.items
473
+ this.total = page.total
474
+ if (this.eventId && !this.event)
475
+ this.event =
476
+ page.items.find(one => one.event)?.event ?? (await galleryApi.events()).find(one => one.id === this.eventId)
477
+ if (!more && !this.narrowed) this.tags = (await galleryApi.tags()).slice(0, 12)
478
+ } catch (error) {
479
+ notify({ level: 'error', message: reasonOf(error) })
480
+ } finally {
481
+ this.loading = false
482
+ }
483
+ }
484
+
485
+ /** Where a new post goes - the screen it is posted from. */
486
+ private preset(): { space: Space; eventId?: string } {
487
+ if (this.eventId) return { space: 'COMPANY', eventId: this.eventId }
488
+ return { space: this.tab === 'mine' ? 'PERSONAL' : 'COMPANY' }
489
+ }
490
+
491
+ private picked(e: CustomEvent) {
492
+ const files = [...((e.detail as File[]) ?? [])]
493
+ /* The picker's own reset says `change` again, with nothing - that one is let go. */
494
+ if (!files.length) return
495
+ ;(e.target as any).reset?.()
496
+ this.upload(files)
497
+ }
498
+
499
+ private upload(files: File[] = [], post?: Post) {
500
+ const { space, eventId } = this.preset()
501
+ const popup: any = openPopup(
502
+ html`<gallery-uploader
503
+ .files=${files}
504
+ .post=${post}
505
+ space=${space}
506
+ .eventId=${eventId ?? null}
507
+ @done=${() => popup.close()}
508
+ ></gallery-uploader>`,
509
+ { backdrop: true, size: 'large', title: post ? t('gallery.title.edit') : t('gallery.title.post') }
510
+ )
511
+ }
512
+
513
+ /* A popup closes by going back in the history: where to go next is taken once it is gone (`result`). */
514
+ private async open(post: Post) {
515
+ const popup: any = openPopup(
516
+ html`<gallery-viewer
517
+ .post=${post}
518
+ @done=${(e: CustomEvent) => popup.close(e.detail)}
519
+ @gallery-liked=${(e: CustomEvent) => this.replace(e.detail)}
520
+ ></gallery-viewer>`,
521
+ { backdrop: true, size: 'large', title: post.event?.name || post.creator?.name || t('gallery.title.page') }
522
+ )
523
+ const next = await popup.result
524
+ if (next?.edit) this.upload([], next.edit)
525
+ else if (next?.go) navigate(next.go)
526
+ }
527
+
528
+ private async openById(id: string) {
529
+ try {
530
+ const post = this.posts.find(one => one.id === id) ?? (await galleryApi.post(id))
531
+ if (post) this.open(post)
532
+ else notify({ level: 'warn', message: t('gallery.text.unknown-post') })
533
+ } catch (error) {
534
+ notify({ level: 'error', message: reasonOf(error) })
535
+ }
536
+ }
537
+
538
+ private replace(post: Post) {
539
+ this.posts = this.posts.map(one => (one.id === post.id ? post : one))
540
+ }
541
+
542
+ private async makeEvent() {
543
+ const popup: any = openPopup(
544
+ html`<gallery-event-form @done=${(e: CustomEvent) => popup.close(e.detail)}></gallery-event-form>`,
545
+ {
546
+ backdrop: true,
547
+ size: 'medium',
548
+ title: t('gallery.button.create-event')
549
+ }
550
+ )
551
+ const made = await popup.result
552
+ if (made?.id) navigate(`gallery-page?event=${made.id}`)
553
+ }
554
+
555
+ private async renameTag() {
556
+ const popup: any = openPopup(
557
+ html`<gallery-tag-form .tag=${this.tag} @done=${(e: CustomEvent) => popup.close(e.detail)}></gallery-tag-form>`,
558
+ {
559
+ backdrop: true,
560
+ size: 'small',
561
+ title: t('gallery.button.rename-tag')
562
+ }
563
+ )
564
+ const to = await popup.result
565
+ if (to) navigate(`gallery-page?tab=${this.tab}&tag=${encodeURIComponent(to)}`)
566
+ }
567
+ }
@@ -0,0 +1,8 @@
1
+ /* The page this module brings to an application that puts it in its menu (ADR-0165 decision 6). */
2
+ export default function route(page: string) {
3
+ switch (page) {
4
+ case 'gallery-page':
5
+ import('./pages/gallery-page.js')
6
+ return page
7
+ }
8
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig-base.json",
3
+ "compilerOptions": {
4
+ "experimentalDecorators": true,
5
+ "skipLibCheck": true,
6
+ "strict": true,
7
+ "declaration": true,
8
+ "module": "esnext",
9
+ "outDir": "../dist-client",
10
+ "baseUrl": "./"
11
+ },
12
+ "include": ["./**/*"]
13
+ }