@things-factory/gallery 0.0.0-stage → 10.1.226

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,157 @@
1
+ /*
2
+ * The gallery doors as the screens call them (ADR-0165). Every call throws the server's words when it is refused.
3
+ */
4
+ import gql from 'graphql-tag'
5
+
6
+ import { client, uploadContext } from '@operato/graphql'
7
+
8
+ export type Space = 'COMPANY' | 'PERSONAL'
9
+ export type Photo = { id: string; name?: string; thumbnail?: string | null; fullpath: string; mimetype?: string }
10
+ export type EventFacts = { id: string; name: string; startAt: string; endAt: string; location?: string | null }
11
+ export type EventCard = EventFacts & { postCount: number; cover?: Photo | null }
12
+ export type EventChoice = EventFacts & { near: boolean }
13
+
14
+ export interface Post {
15
+ id: string
16
+ space: Space
17
+ caption?: string | null
18
+ takenAt: string
19
+ tags?: string[] | null
20
+ likeCount: number
21
+ liked: boolean
22
+ editable: boolean
23
+ moderatable: boolean
24
+ hiddenAt?: string | null
25
+ hiddenReason?: string | null
26
+ createdAt: string
27
+ creator?: { id: string; name?: string } | null
28
+ event?: EventFacts | null
29
+ photos: Photo[]
30
+ }
31
+
32
+ export interface PostInput {
33
+ space?: Space
34
+ caption?: string
35
+ takenAt?: string
36
+ tags?: string[]
37
+ eventId?: string | null
38
+ }
39
+
40
+ export type Filter = { space?: Space; eventId?: string; tag?: string; authorId?: string }
41
+
42
+ const POST_FIELDS = `id space caption takenAt tags likeCount liked editable moderatable hiddenAt hiddenReason createdAt
43
+ creator { id name } event { id name startAt endAt location } photos { id name thumbnail fullpath mimetype }`
44
+ const EVENT_FIELDS = 'id name startAt endAt location'
45
+
46
+ function answer<T>(response: any, field: string): T {
47
+ if (response.errors?.length) throw new Error(response.errors.map((one: any) => one.message).join(' · '))
48
+ return response.data?.[field] as T
49
+ }
50
+
51
+ async function ask<T>(query: string, variables: Record<string, unknown>, field: string): Promise<T> {
52
+ return answer<T>(await client.query({ query: gql(query), variables, fetchPolicy: 'network-only' }), field)
53
+ }
54
+
55
+ async function change<T>(
56
+ mutation: string,
57
+ variables: Record<string, unknown>,
58
+ field: string,
59
+ uploading?: (fraction: number) => void
60
+ ): Promise<T> {
61
+ return answer<T>(
62
+ await client.mutate({
63
+ mutation: gql(mutation),
64
+ variables,
65
+ ...(uploading ? { context: uploadContext(uploading) } : {})
66
+ }),
67
+ field
68
+ )
69
+ }
70
+
71
+ /** Tells the open gallery and the menu's count that a post came, changed or went. */
72
+ export const galleryChanged = () => window.dispatchEvent(new CustomEvent('gallery-changed'))
73
+
74
+ export const galleryApi = {
75
+ posts: (filter: Filter, skip = 0, take = 30) =>
76
+ ask<{ items: Post[]; total: number }>(
77
+ `query ($filter: GalleryFilter, $skip: Int, $take: Int) { galleryPosts(filter: $filter, skip: $skip, take: $take) { items { ${POST_FIELDS} } total } }`,
78
+ { filter, skip, take },
79
+ 'galleryPosts'
80
+ ),
81
+ post: (id: string) =>
82
+ ask<Post | null>(`query ($id: String!) { galleryPost(id: $id) { ${POST_FIELDS} } }`, { id }, 'galleryPost'),
83
+ likers: (id: string) =>
84
+ ask<{ id: string; name?: string }[]>(
85
+ `query ($id: String!) { galleryLikers(id: $id) { id name } }`,
86
+ { id },
87
+ 'galleryLikers'
88
+ ),
89
+ tags: (text = '') =>
90
+ ask<{ tag: string; count: number }[]>(
91
+ `query ($text: String) { galleryTags(text: $text) { tag count } }`,
92
+ { text },
93
+ 'galleryTags'
94
+ ),
95
+ events: () =>
96
+ ask<EventCard[]>(
97
+ `query { galleryEvents { ${EVENT_FIELDS} postCount cover { id thumbnail fullpath } } }`,
98
+ {},
99
+ 'galleryEvents'
100
+ ),
101
+ eventChoices: (takenAt?: string) =>
102
+ ask<EventChoice[]>(
103
+ `query ($takenAt: DateTimeISO) { galleryEventChoices(takenAt: $takenAt) { ${EVENT_FIELDS} near } }`,
104
+ { takenAt },
105
+ 'galleryEventChoices'
106
+ ),
107
+ summary: () => ask<{ fresh: number }>(`query { gallerySummary { fresh } }`, {}, 'gallerySummary'),
108
+ create: (input: PostInput, photos: File[], uploading?: (fraction: number) => void) =>
109
+ change<Post>(
110
+ `mutation ($input: GalleryPostInput!, $photos: [Upload!]!) { createGalleryPost(input: $input, photos: $photos) { ${POST_FIELDS} } }`,
111
+ { input, photos },
112
+ 'createGalleryPost',
113
+ uploading ?? (() => {})
114
+ ),
115
+ update: (id: string, input: PostInput, attach: File[], remove: string[], uploading?: (fraction: number) => void) =>
116
+ change<Post>(
117
+ `mutation ($id: String!, $input: GalleryPostInput!, $attach: [Upload!], $remove: [String!]) {
118
+ updateGalleryPost(id: $id, input: $input, attach: $attach, remove: $remove) { ${POST_FIELDS} }
119
+ }`,
120
+ { id, input, attach, remove },
121
+ 'updateGalleryPost',
122
+ attach.length ? (uploading ?? (() => {})) : undefined
123
+ ),
124
+ remove: (id: string) =>
125
+ change<boolean>(`mutation ($id: String!) { deleteGalleryPost(id: $id) }`, { id }, 'deleteGalleryPost'),
126
+ like: (id: string, like: boolean) =>
127
+ change<{ id: string; likeCount: number; liked: boolean }>(
128
+ `mutation ($id: String!, $like: Boolean!) { likeGalleryPost(id: $id, like: $like) { id likeCount liked } }`,
129
+ { id, like },
130
+ 'likeGalleryPost'
131
+ ),
132
+ visit: () => change<boolean>(`mutation { visitGallery }`, {}, 'visitGallery'),
133
+ hide: (id: string, reason: string) =>
134
+ change<Post>(
135
+ `mutation ($id: String!, $reason: String!) { hideGalleryPost(id: $id, reason: $reason) { ${POST_FIELDS} } }`,
136
+ { id, reason },
137
+ 'hideGalleryPost'
138
+ ),
139
+ unhide: (id: string) =>
140
+ change<Post>(
141
+ `mutation ($id: String!) { unhideGalleryPost(id: $id) { ${POST_FIELDS} } }`,
142
+ { id },
143
+ 'unhideGalleryPost'
144
+ ),
145
+ renameTag: (from: string, to: string) =>
146
+ change<number>(
147
+ `mutation ($from: String!, $to: String!) { renameGalleryTag(from: $from, to: $to) }`,
148
+ { from, to },
149
+ 'renameGalleryTag'
150
+ ),
151
+ createEvent: (input: { name: string; startAt: string; endAt: string; location?: string }) =>
152
+ change<EventCard>(
153
+ `mutation ($input: GalleryEventInput!) { createGalleryEvent(input: $input) { ${EVENT_FIELDS} postCount } }`,
154
+ { input },
155
+ 'createGalleryEvent'
156
+ )
157
+ }
@@ -0,0 +1,44 @@
1
+ /*
2
+ * The count the menu shows on 갤러리 - posts others made since one last opened it (ADR-0165 decision 6). An application
3
+ * puts `generateGalleryBadge` on its menu entry; the count is asked again when a post comes or the gallery is opened.
4
+ */
5
+ import { galleryApi } from './gallery-api.js'
6
+
7
+ let count: number | undefined
8
+ let version = 0
9
+ const waiting = new Set<() => void>()
10
+
11
+ /** Asks the server again and tells every badge. A count that could not be asked is not zero: it draws nothing. */
12
+ export async function refreshGalleryBadge(): Promise<void> {
13
+ try {
14
+ count = (await galleryApi.summary()).fresh || undefined
15
+ } catch {
16
+ count = undefined
17
+ }
18
+ version++
19
+ for (const wake of [...waiting]) wake()
20
+ }
21
+
22
+ let generation = 0
23
+
24
+ /** One badge's counts. The menu asks for a new one when it draws again; the one before it then ends. */
25
+ export async function* generateGalleryBadge() {
26
+ const mine = ++generation
27
+ for (const wake of [...waiting]) wake()
28
+ let seen = version
29
+ yield count
30
+ while (true) {
31
+ if (seen === version) {
32
+ await new Promise<void>(resolve => {
33
+ const wake = () => {
34
+ waiting.delete(wake)
35
+ resolve()
36
+ }
37
+ waiting.add(wake)
38
+ })
39
+ }
40
+ if (mine !== generation) return
41
+ seen = version
42
+ yield count
43
+ }
44
+ }
@@ -0,0 +1,22 @@
1
+ /*
2
+ * The words of the gallery - one place for the page, the viewer and the uploader.
3
+ */
4
+ import { i18next } from '@operato/i18n'
5
+
6
+ export const t = (key: string, options: Record<string, unknown> = {}) => String(i18next.t(key, options))
7
+
8
+ /** A day as the reader's locale writes it. */
9
+ export function day(value?: string | Date | null): string {
10
+ if (!value) return ''
11
+ return new Date(value).toLocaleDateString(i18next.language, { year: 'numeric', month: 'short', day: 'numeric' })
12
+ }
13
+
14
+ /** An event's days - one day, or its first and last. */
15
+ export function days(startAt: string, endAt: string): string {
16
+ const start = new Date(startAt)
17
+ const end = new Date(endAt)
18
+ return start.toDateString() === end.toDateString() ? day(start) : `${day(start)} – ${day(end)}`
19
+ }
20
+
21
+ /** The words of a refusal the server gave, or the fallback. */
22
+ export const reasonOf = (error: unknown) => (error as Error)?.message || t('gallery.text.failed')
@@ -0,0 +1,93 @@
1
+ /*
2
+ * A photo made ready in the browser before it goes up (ADR-0165 decision 3): when it was taken, read from the camera's
3
+ * own record (EXIF), and the photo shrunk to 2560 px on its long side so a phone's original does not fill the storage.
4
+ */
5
+
6
+ /** The longest side a photo keeps. */
7
+ export const LONG_SIDE = 2560
8
+ /* A photo already small enough goes up as it is - shrinking again would only lose quality. */
9
+ const SMALL_ENOUGH = 2.5 * 1024 * 1024
10
+
11
+ /** "YYYY:MM:DD HH:MM:SS" as the camera wrote it, in the camera's local time. */
12
+ export function exifDateOf(text?: string): Date | undefined {
13
+ const match = /^(\d{4}):(\d{2}):(\d{2})[ T](\d{2}):(\d{2}):(\d{2})/.exec(text ?? '')
14
+ if (!match) return undefined
15
+ const [, y, mo, d, h, mi, s] = match.map(Number)
16
+ const date = new Date(y, mo - 1, d, h, mi, s)
17
+ return isNaN(date.getTime()) || y < 1990 ? undefined : date
18
+ }
19
+
20
+ /** The date the camera recorded the photo, from a JPEG's EXIF - when it was taken (0x9003), else when it was saved (0x0132). */
21
+ export function exifTakenAt(buffer: ArrayBuffer): Date | undefined {
22
+ try {
23
+ const view = new DataView(buffer)
24
+ if (view.byteLength < 4 || view.getUint16(0) !== 0xffd8) return undefined
25
+ let offset = 2
26
+ while (offset + 10 <= view.byteLength) {
27
+ const marker = view.getUint16(offset)
28
+ if ((marker & 0xff00) !== 0xff00) return undefined
29
+ const size = view.getUint16(offset + 2)
30
+ /* APP1 starting with "Exif\0\0" */
31
+ if (marker === 0xffe1 && view.getUint32(offset + 4) === 0x45786966) return exifDateOf(tiffDate(view, offset + 10))
32
+ offset += 2 + size
33
+ }
34
+ } catch {
35
+ /* A broken or cut record says nothing. */
36
+ }
37
+ return undefined
38
+ }
39
+
40
+ function tiffDate(view: DataView, start: number): string | undefined {
41
+ const little = view.getUint16(start) === 0x4949
42
+ const u16 = (at: number) => view.getUint16(start + at, little)
43
+ const u32 = (at: number) => view.getUint32(start + at, little)
44
+ const entries = (ifd: number) => Array.from({ length: u16(ifd) }, (_, i) => ifd + 2 + i * 12)
45
+ const ascii = (entry: number) => {
46
+ const count = u32(entry + 4)
47
+ const at = count > 4 ? u32(entry + 8) : entry + 8
48
+ let text = ''
49
+ for (let i = 0; i < count - 1; i++) text += String.fromCharCode(view.getUint8(start + at + i))
50
+ return text
51
+ }
52
+ let saved: string | undefined
53
+ let exif: number | undefined
54
+ for (const entry of entries(u32(4))) {
55
+ const tag = u16(entry)
56
+ if (tag === 0x0132) saved = ascii(entry)
57
+ if (tag === 0x8769) exif = u32(entry + 8)
58
+ }
59
+ if (exif) for (const entry of entries(exif)) if (u16(entry) === 0x9003) return ascii(entry)
60
+ return saved
61
+ }
62
+
63
+ /** When the photo was taken - the camera's record, or nothing. */
64
+ export async function takenAtOf(file: File): Promise<Date | undefined> {
65
+ if (!/jpe?g/i.test(file.type) && !/\.jpe?g$/i.test(file.name)) return undefined
66
+ return exifTakenAt(await file.slice(0, 256 * 1024).arrayBuffer())
67
+ }
68
+
69
+ /** The photo shrunk to `LONG_SIDE` on its long side as a JPEG - or the photo itself when it is small or cannot be drawn. */
70
+ export async function shrunk(file: File): Promise<File> {
71
+ try {
72
+ const bitmap = await createImageBitmap(file, { imageOrientation: 'from-image' } as any)
73
+ const long = Math.max(bitmap.width, bitmap.height)
74
+ if (long <= LONG_SIDE && file.size <= SMALL_ENOUGH) {
75
+ bitmap.close()
76
+ return file
77
+ }
78
+ const scale = Math.min(1, LONG_SIDE / long)
79
+ const canvas = document.createElement('canvas')
80
+ canvas.width = Math.round(bitmap.width * scale)
81
+ canvas.height = Math.round(bitmap.height * scale)
82
+ canvas.getContext('2d')!.drawImage(bitmap, 0, 0, canvas.width, canvas.height)
83
+ bitmap.close()
84
+ const blob = await new Promise<Blob | null>(resolve => canvas.toBlob(resolve, 'image/jpeg', 0.88))
85
+ if (!blob) return file
86
+ return new File([blob], file.name.replace(/\.[^.]+$/, '') + '.jpg', {
87
+ type: 'image/jpeg',
88
+ lastModified: file.lastModified
89
+ })
90
+ } catch {
91
+ return file
92
+ }
93
+ }
@@ -0,0 +1 @@
1
+ export default function bootstrap(): void;
@@ -0,0 +1,16 @@
1
+ import { navigate, store } from '@operato/shell';
2
+ import { ADD_USER_MENU } from '@things-factory/apptool-base/client';
3
+ /* One's own gallery is in the menu the header's avatar opens, right after the profile (ADR-0165 decision 6). */
4
+ export default function bootstrap() {
5
+ store.dispatch({
6
+ type: ADD_USER_MENU,
7
+ item: {
8
+ name: 'my-gallery',
9
+ icon: 'photo_library',
10
+ label: 'gallery.title.my-gallery',
11
+ order: 12,
12
+ action: () => navigate('gallery-page?tab=mine')
13
+ }
14
+ });
15
+ }
16
+ //# sourceMappingURL=bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.js","sourceRoot":"","sources":["../client/bootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAA;AAEnE,gHAAgH;AAChH,MAAM,CAAC,OAAO,UAAU,SAAS;IAC/B,KAAK,CAAC,QAAQ,CAAC;QACb,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE;YACJ,IAAI,EAAE,YAAY;YAClB,IAAI,EAAE,eAAe;YACrB,KAAK,EAAE,0BAA0B;YACjC,KAAK,EAAE,EAAE;YACT,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,uBAAuB,CAAC;SAChD;KACF,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import { navigate, store } from '@operato/shell'\nimport { ADD_USER_MENU } from '@things-factory/apptool-base/client'\n\n/* One's own gallery is in the menu the header's avatar opens, right after the profile (ADR-0165 decision 6). */\nexport default function bootstrap() {\n store.dispatch({\n type: ADD_USER_MENU,\n item: {\n name: 'my-gallery',\n icon: 'photo_library',\n label: 'gallery.title.my-gallery',\n order: 12,\n action: () => navigate('gallery-page?tab=mine')\n }\n })\n}\n"]}
@@ -0,0 +1,31 @@
1
+ import '@material/web/button/filled-button.js';
2
+ import '@material/web/button/text-button.js';
3
+ import '@operato/popup/ox-popup-actions.js';
4
+ import { LitElement } from 'lit';
5
+ declare const GalleryEventForm_base: (new (...args: any[]) => LitElement) & typeof LitElement;
6
+ export declare class GalleryEventForm extends GalleryEventForm_base {
7
+ static styles: import("lit").CSSResult[];
8
+ private name;
9
+ private startAt;
10
+ private endAt;
11
+ private location;
12
+ private busy;
13
+ render(): import("lit-html").TemplateResult<1>;
14
+ /** A later start carries the end with it, the same length. */
15
+ private start;
16
+ private save;
17
+ /** What was made goes as the popup's result. */
18
+ private close;
19
+ }
20
+ declare const GalleryTagForm_base: (new (...args: any[]) => LitElement) & typeof LitElement;
21
+ export declare class GalleryTagForm extends GalleryTagForm_base {
22
+ static styles: import("lit").CSSResult[];
23
+ tag: string;
24
+ private to;
25
+ private busy;
26
+ render(): import("lit-html").TemplateResult<1>;
27
+ private save;
28
+ /** What was made goes as the popup's result. */
29
+ private close;
30
+ }
31
+ export {};
@@ -0,0 +1,223 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ /*
3
+ * The two small forms of the gallery's moderators (ADR-0165 decision 5): an event made from the gallery, for one nobody
4
+ * put on the calendar yet, and a tag renamed on every post - into one already there, the two become one.
5
+ */
6
+ import '@material/web/button/filled-button.js';
7
+ import '@material/web/button/text-button.js';
8
+ import '@operato/popup/ox-popup-actions.js';
9
+ import { css, html, LitElement } from 'lit';
10
+ import { customElement, property, state } from 'lit/decorators.js';
11
+ import { i18next, localize } from '@operato/i18n';
12
+ import { notify } from '@operato/layout';
13
+ import { CommonPopupStyles, PopupBodyStyles } from '@operato/styles';
14
+ import { galleryApi, galleryChanged } from '../utils/gallery-api.js';
15
+ import { reasonOf, t } from '../utils/gallery-words.js';
16
+ const FormStyles = css `
17
+ :host {
18
+ height: 100%;
19
+ }
20
+ .body {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: var(--spacing-medium, 8px);
24
+ }
25
+ label {
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 2px;
29
+ font-size: var(--ui-t-small, 12px);
30
+ font-weight: 600;
31
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
32
+ }
33
+ input {
34
+ box-sizing: border-box;
35
+ font: inherit;
36
+ font-size: var(--ui-t-body, 14px);
37
+ font-weight: normal;
38
+ padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);
39
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
40
+ border-radius: var(--border-radius, 4px);
41
+ background: var(--md-sys-color-surface);
42
+ color: var(--md-sys-color-on-surface);
43
+ }
44
+ .row {
45
+ display: flex;
46
+ flex-wrap: wrap;
47
+ gap: var(--spacing-medium, 8px);
48
+ }
49
+ .note {
50
+ font-size: var(--ui-t-small, 12px);
51
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
52
+ }
53
+ `;
54
+ const local = (value) => {
55
+ const pad = (n) => String(n).padStart(2, '0');
56
+ return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`;
57
+ };
58
+ let GalleryEventForm = class GalleryEventForm extends localize(i18next)(LitElement) {
59
+ constructor() {
60
+ super(...arguments);
61
+ this.name = '';
62
+ this.startAt = local(new Date(new Date().setHours(9, 0, 0, 0)));
63
+ this.endAt = local(new Date(new Date().setHours(18, 0, 0, 0)));
64
+ this.location = '';
65
+ this.busy = false;
66
+ }
67
+ static { this.styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]; }
68
+ render() {
69
+ const ready = this.name.trim() && this.startAt && this.endAt && this.endAt > this.startAt;
70
+ return html `<div class="body">
71
+ <label
72
+ >${t('gallery.field.event-name')}<input
73
+ .value=${this.name}
74
+ @input=${(e) => (this.name = e.target.value)}
75
+ /></label>
76
+ <div class="row">
77
+ <label
78
+ >${t('gallery.field.starts')}<input
79
+ type="datetime-local"
80
+ .value=${this.startAt}
81
+ @change=${(e) => this.start(e.target.value)}
82
+ /></label>
83
+ <label
84
+ >${t('gallery.field.ends')}<input
85
+ type="datetime-local"
86
+ .value=${this.endAt}
87
+ @change=${(e) => (this.endAt = e.target.value)}
88
+ /></label>
89
+ </div>
90
+ <label
91
+ >${t('gallery.field.location')}<input
92
+ .value=${this.location}
93
+ @input=${(e) => (this.location = e.target.value)}
94
+ /></label>
95
+ <div class="note">${t('gallery.text.event-form-note')}</div>
96
+ </div>
97
+ <ox-popup-actions>
98
+ <md-text-button slot="cancel" @click=${() => this.close()}>${t('gallery.button.cancel')}</md-text-button>
99
+ <md-filled-button slot="confirm" ?disabled=${this.busy || !ready} @click=${() => this.save()}
100
+ >${t('gallery.button.create-event')}</md-filled-button
101
+ >
102
+ </ox-popup-actions>`;
103
+ }
104
+ /** A later start carries the end with it, the same length. */
105
+ start(value) {
106
+ const length = new Date(this.endAt).getTime() - new Date(this.startAt).getTime();
107
+ this.startAt = value;
108
+ if (value && length > 0)
109
+ this.endAt = local(new Date(new Date(value).getTime() + length));
110
+ }
111
+ async save() {
112
+ this.busy = true;
113
+ try {
114
+ const event = await galleryApi.createEvent({
115
+ name: this.name.trim(),
116
+ startAt: new Date(this.startAt).toISOString(),
117
+ endAt: new Date(this.endAt).toISOString(),
118
+ location: this.location.trim() || undefined
119
+ });
120
+ galleryChanged();
121
+ this.close(event);
122
+ }
123
+ catch (error) {
124
+ notify({ level: 'error', message: reasonOf(error) });
125
+ }
126
+ finally {
127
+ this.busy = false;
128
+ }
129
+ }
130
+ /** What was made goes as the popup's result. */
131
+ close(result) {
132
+ this.dispatchEvent(new CustomEvent('done', { detail: result, bubbles: true, composed: true }));
133
+ }
134
+ };
135
+ __decorate([
136
+ state(),
137
+ __metadata("design:type", Object)
138
+ ], GalleryEventForm.prototype, "name", void 0);
139
+ __decorate([
140
+ state(),
141
+ __metadata("design:type", Object)
142
+ ], GalleryEventForm.prototype, "startAt", void 0);
143
+ __decorate([
144
+ state(),
145
+ __metadata("design:type", Object)
146
+ ], GalleryEventForm.prototype, "endAt", void 0);
147
+ __decorate([
148
+ state(),
149
+ __metadata("design:type", Object)
150
+ ], GalleryEventForm.prototype, "location", void 0);
151
+ __decorate([
152
+ state(),
153
+ __metadata("design:type", Object)
154
+ ], GalleryEventForm.prototype, "busy", void 0);
155
+ GalleryEventForm = __decorate([
156
+ customElement('gallery-event-form')
157
+ ], GalleryEventForm);
158
+ export { GalleryEventForm };
159
+ let GalleryTagForm = class GalleryTagForm extends localize(i18next)(LitElement) {
160
+ constructor() {
161
+ super(...arguments);
162
+ this.tag = '';
163
+ this.to = '';
164
+ this.busy = false;
165
+ }
166
+ static { this.styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]; }
167
+ render() {
168
+ const ready = this.to.trim().replace(/^#+/, '') && this.to.trim().replace(/^#+/, '') !== this.tag;
169
+ return html `<div class="body">
170
+ <label
171
+ >${t('gallery.field.new-tag')}<input
172
+ .value=${this.to}
173
+ placeholder=${this.tag}
174
+ maxlength="31"
175
+ @input=${(e) => (this.to = e.target.value)}
176
+ /></label>
177
+ <div class="note">${t('gallery.text.tag-form-note', { tag: this.tag })}</div>
178
+ </div>
179
+ <ox-popup-actions>
180
+ <md-text-button slot="cancel" @click=${() => this.close()}>${t('gallery.button.cancel')}</md-text-button>
181
+ <md-filled-button slot="confirm" ?disabled=${this.busy || !ready} @click=${() => this.save()}
182
+ >${t('gallery.button.rename-tag')}</md-filled-button
183
+ >
184
+ </ox-popup-actions>`;
185
+ }
186
+ async save() {
187
+ this.busy = true;
188
+ try {
189
+ const to = this.to.trim().replace(/^#+/, '');
190
+ const count = await galleryApi.renameTag(this.tag, to);
191
+ notify({ message: t('gallery.text.tag-renamed', { count }) });
192
+ galleryChanged();
193
+ this.close(to);
194
+ }
195
+ catch (error) {
196
+ notify({ level: 'error', message: reasonOf(error) });
197
+ }
198
+ finally {
199
+ this.busy = false;
200
+ }
201
+ }
202
+ /** What was made goes as the popup's result. */
203
+ close(result) {
204
+ this.dispatchEvent(new CustomEvent('done', { detail: result, bubbles: true, composed: true }));
205
+ }
206
+ };
207
+ __decorate([
208
+ property(),
209
+ __metadata("design:type", Object)
210
+ ], GalleryTagForm.prototype, "tag", void 0);
211
+ __decorate([
212
+ state(),
213
+ __metadata("design:type", Object)
214
+ ], GalleryTagForm.prototype, "to", void 0);
215
+ __decorate([
216
+ state(),
217
+ __metadata("design:type", Object)
218
+ ], GalleryTagForm.prototype, "busy", void 0);
219
+ GalleryTagForm = __decorate([
220
+ customElement('gallery-tag-form')
221
+ ], GalleryTagForm);
222
+ export { GalleryTagForm };
223
+ //# sourceMappingURL=gallery-forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gallery-forms.js","sourceRoot":"","sources":["../../client/components/gallery-forms.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The two small forms of the gallery's moderators (ADR-0165 decision 5): an event made from the gallery, for one nobody\n * put on the calendar yet, and a tag renamed on every post - into one already there, the two become one.\n */\nimport '@material/web/button/filled-button.js'\nimport '@material/web/button/text-button.js'\nimport '@operato/popup/ox-popup-actions.js'\n\nimport { css, html, LitElement } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next, localize } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { CommonPopupStyles, PopupBodyStyles } from '@operato/styles'\n\nimport { galleryApi, galleryChanged } from '../utils/gallery-api.js'\nimport { reasonOf, t } from '../utils/gallery-words.js'\n\nconst FormStyles = css`\n :host {\n height: 100%;\n }\n .body {\n display: flex;\n flex-direction: column;\n gap: var(--spacing-medium, 8px);\n }\n label {\n display: flex;\n flex-direction: column;\n gap: 2px;\n font-size: var(--ui-t-small, 12px);\n font-weight: 600;\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n input {\n box-sizing: border-box;\n font: inherit;\n font-size: var(--ui-t-body, 14px);\n font-weight: normal;\n padding: var(--spacing-small, 4px) var(--spacing-medium, 8px);\n border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));\n border-radius: var(--border-radius, 4px);\n background: var(--md-sys-color-surface);\n color: var(--md-sys-color-on-surface);\n }\n .row {\n display: flex;\n flex-wrap: wrap;\n gap: var(--spacing-medium, 8px);\n }\n .note {\n font-size: var(--ui-t-small, 12px);\n color: var(--ui-muted, var(--md-sys-color-on-surface-variant));\n }\n`\n\nconst local = (value: Date) => {\n const pad = (n: number) => String(n).padStart(2, '0')\n return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}T${pad(value.getHours())}:${pad(value.getMinutes())}`\n}\n\n@customElement('gallery-event-form')\nexport class GalleryEventForm extends localize(i18next)(LitElement) {\n static styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]\n\n @state() private name = ''\n @state() private startAt = local(new Date(new Date().setHours(9, 0, 0, 0)))\n @state() private endAt = local(new Date(new Date().setHours(18, 0, 0, 0)))\n @state() private location = ''\n @state() private busy = false\n\n render() {\n const ready = this.name.trim() && this.startAt && this.endAt && this.endAt > this.startAt\n return html`<div class=\"body\">\n <label\n >${t('gallery.field.event-name')}<input\n .value=${this.name}\n @input=${(e: any) => (this.name = e.target.value)}\n /></label>\n <div class=\"row\">\n <label\n >${t('gallery.field.starts')}<input\n type=\"datetime-local\"\n .value=${this.startAt}\n @change=${(e: any) => this.start(e.target.value)}\n /></label>\n <label\n >${t('gallery.field.ends')}<input\n type=\"datetime-local\"\n .value=${this.endAt}\n @change=${(e: any) => (this.endAt = e.target.value)}\n /></label>\n </div>\n <label\n >${t('gallery.field.location')}<input\n .value=${this.location}\n @input=${(e: any) => (this.location = e.target.value)}\n /></label>\n <div class=\"note\">${t('gallery.text.event-form-note')}</div>\n </div>\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => this.close()}>${t('gallery.button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${this.busy || !ready} @click=${() => this.save()}\n >${t('gallery.button.create-event')}</md-filled-button\n >\n </ox-popup-actions>`\n }\n\n /** A later start carries the end with it, the same length. */\n private start(value: string) {\n const length = new Date(this.endAt).getTime() - new Date(this.startAt).getTime()\n this.startAt = value\n if (value && length > 0) this.endAt = local(new Date(new Date(value).getTime() + length))\n }\n\n private async save() {\n this.busy = true\n try {\n const event = await galleryApi.createEvent({\n name: this.name.trim(),\n startAt: new Date(this.startAt).toISOString(),\n endAt: new Date(this.endAt).toISOString(),\n location: this.location.trim() || undefined\n })\n galleryChanged()\n this.close(event)\n } catch (error) {\n notify({ level: 'error', message: reasonOf(error) })\n } finally {\n this.busy = false\n }\n }\n\n /** What was made goes as the popup's result. */\n private close(result?: unknown) {\n this.dispatchEvent(new CustomEvent('done', { detail: result, bubbles: true, composed: true }))\n }\n}\n\n@customElement('gallery-tag-form')\nexport class GalleryTagForm extends localize(i18next)(LitElement) {\n static styles = [CommonPopupStyles, PopupBodyStyles, FormStyles]\n\n @property() tag = ''\n @state() private to = ''\n @state() private busy = false\n\n render() {\n const ready = this.to.trim().replace(/^#+/, '') && this.to.trim().replace(/^#+/, '') !== this.tag\n return html`<div class=\"body\">\n <label\n >${t('gallery.field.new-tag')}<input\n .value=${this.to}\n placeholder=${this.tag}\n maxlength=\"31\"\n @input=${(e: any) => (this.to = e.target.value)}\n /></label>\n <div class=\"note\">${t('gallery.text.tag-form-note', { tag: this.tag })}</div>\n </div>\n <ox-popup-actions>\n <md-text-button slot=\"cancel\" @click=${() => this.close()}>${t('gallery.button.cancel')}</md-text-button>\n <md-filled-button slot=\"confirm\" ?disabled=${this.busy || !ready} @click=${() => this.save()}\n >${t('gallery.button.rename-tag')}</md-filled-button\n >\n </ox-popup-actions>`\n }\n\n private async save() {\n this.busy = true\n try {\n const to = this.to.trim().replace(/^#+/, '')\n const count = await galleryApi.renameTag(this.tag, to)\n notify({ message: t('gallery.text.tag-renamed', { count }) })\n galleryChanged()\n this.close(to)\n } catch (error) {\n notify({ level: 'error', message: reasonOf(error) })\n } finally {\n this.busy = false\n }\n }\n\n /** What was made goes as the popup's result. */\n private close(result?: unknown) {\n this.dispatchEvent(new CustomEvent('done', { detail: result, bubbles: true, composed: true }))\n }\n}\n"]}