@foliokit/cms-admin-ui 0.0.0 → 0.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.
Files changed (50) hide show
  1. package/esm2022/index.js +14 -0
  2. package/esm2022/index.js.map +1 -1
  3. package/esm2022/lib/cms-admin-ui/cms-admin-ui.js +3 -3
  4. package/esm2022/lib/page-editor/about-editor-form.component.js +84 -0
  5. package/esm2022/lib/page-editor/about-editor-form.component.js.map +1 -0
  6. package/esm2022/lib/page-editor/links-editor-form.component.js +474 -0
  7. package/esm2022/lib/page-editor/links-editor-form.component.js.map +1 -0
  8. package/esm2022/lib/page-editor/page-editor-hero-image.component.js +216 -0
  9. package/esm2022/lib/page-editor/page-editor-hero-image.component.js.map +1 -0
  10. package/esm2022/lib/page-editor/page-editor.store.js +198 -0
  11. package/esm2022/lib/page-editor/page-editor.store.js.map +1 -0
  12. package/esm2022/lib/post-editor/post-editor-cover-image.component.js +251 -0
  13. package/esm2022/lib/post-editor/post-editor-cover-image.component.js.map +1 -0
  14. package/esm2022/lib/post-editor/post-editor-embedded-media-item.component.js +99 -0
  15. package/esm2022/lib/post-editor/post-editor-embedded-media-item.component.js.map +1 -0
  16. package/esm2022/lib/post-editor/post-editor-embedded-media.component.js +173 -0
  17. package/esm2022/lib/post-editor/post-editor-embedded-media.component.js.map +1 -0
  18. package/esm2022/lib/post-editor/post-editor-media-tab.component.js +23 -0
  19. package/esm2022/lib/post-editor/post-editor-media-tab.component.js.map +1 -0
  20. package/esm2022/lib/post-editor/post-editor.store.js +189 -0
  21. package/esm2022/lib/post-editor/post-editor.store.js.map +1 -0
  22. package/esm2022/lib/posts-list/posts-board.component.js +66 -0
  23. package/esm2022/lib/posts-list/posts-board.component.js.map +1 -0
  24. package/esm2022/lib/posts-list/posts-draft-column.component.js +71 -0
  25. package/esm2022/lib/posts-list/posts-draft-column.component.js.map +1 -0
  26. package/esm2022/lib/posts-list/posts-list.component.js +79 -0
  27. package/esm2022/lib/posts-list/posts-list.component.js.map +1 -0
  28. package/esm2022/lib/posts-list/posts-list.store.js +43 -0
  29. package/esm2022/lib/posts-list/posts-list.store.js.map +1 -0
  30. package/esm2022/lib/posts-list/posts-published-column.component.js +129 -0
  31. package/esm2022/lib/posts-list/posts-published-column.component.js.map +1 -0
  32. package/esm2022/lib/posts-list/posts-queue-column.component.js +112 -0
  33. package/esm2022/lib/posts-list/posts-queue-column.component.js.map +1 -0
  34. package/index.d.ts +14 -0
  35. package/lib/page-editor/about-editor-form.component.d.ts +32 -0
  36. package/lib/page-editor/links-editor-form.component.d.ts +52 -0
  37. package/lib/page-editor/page-editor-hero-image.component.d.ts +51 -0
  38. package/lib/page-editor/page-editor.store.d.ts +37 -0
  39. package/lib/post-editor/post-editor-cover-image.component.d.ts +50 -0
  40. package/lib/post-editor/post-editor-embedded-media-item.component.d.ts +37 -0
  41. package/lib/post-editor/post-editor-embedded-media.component.d.ts +43 -0
  42. package/lib/post-editor/post-editor-media-tab.component.d.ts +5 -0
  43. package/lib/post-editor/post-editor.store.d.ts +37 -0
  44. package/lib/posts-list/posts-board.component.d.ts +24 -0
  45. package/lib/posts-list/posts-draft-column.component.d.ts +8 -0
  46. package/lib/posts-list/posts-list.component.d.ts +28 -0
  47. package/lib/posts-list/posts-list.store.d.ts +24 -0
  48. package/lib/posts-list/posts-published-column.component.d.ts +10 -0
  49. package/lib/posts-list/posts-queue-column.component.d.ts +14 -0
  50. package/package.json +1 -1
package/esm2022/index.js CHANGED
@@ -1,2 +1,16 @@
1
1
  export * from './lib/cms-admin-ui/cms-admin-ui';
2
+ export * from './lib/post-editor/post-editor.store';
3
+ export * from './lib/page-editor/page-editor.store';
4
+ export * from './lib/page-editor/about-editor-form.component';
5
+ export * from './lib/page-editor/links-editor-form.component';
6
+ export * from './lib/post-editor/post-editor-media-tab.component';
7
+ export * from './lib/post-editor/post-editor-cover-image.component';
8
+ export * from './lib/post-editor/post-editor-embedded-media.component';
9
+ export * from './lib/post-editor/post-editor-embedded-media-item.component';
10
+ export * from './lib/posts-list/posts-list.store';
11
+ export * from './lib/posts-list/posts-board.component';
12
+ export * from './lib/posts-list/posts-draft-column.component';
13
+ export * from './lib/posts-list/posts-queue-column.component';
14
+ export * from './lib/posts-list/posts-published-column.component';
15
+ export * from './lib/posts-list/posts-list.component';
2
16
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/cms-admin-ui/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC","sourcesContent":["export * from './lib/cms-admin-ui/cms-admin-ui';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/cms-admin-ui/src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,mDAAmD,CAAC;AAClE,cAAc,qDAAqD,CAAC;AACpE,cAAc,wDAAwD,CAAC;AACvE,cAAc,6DAA6D,CAAC;AAC5E,cAAc,mCAAmC,CAAC;AAClD,cAAc,wCAAwC,CAAC;AACvD,cAAc,+CAA+C,CAAC;AAC9D,cAAc,+CAA+C,CAAC;AAC9D,cAAc,mDAAmD,CAAC;AAClE,cAAc,uCAAuC,CAAC","sourcesContent":["export * from './lib/cms-admin-ui/cms-admin-ui';\nexport * from './lib/post-editor/post-editor.store';\nexport * from './lib/page-editor/page-editor.store';\nexport * from './lib/page-editor/about-editor-form.component';\nexport * from './lib/page-editor/links-editor-form.component';\nexport * from './lib/post-editor/post-editor-media-tab.component';\nexport * from './lib/post-editor/post-editor-cover-image.component';\nexport * from './lib/post-editor/post-editor-embedded-media.component';\nexport * from './lib/post-editor/post-editor-embedded-media-item.component';\nexport * from './lib/posts-list/posts-list.store';\nexport * from './lib/posts-list/posts-board.component';\nexport * from './lib/posts-list/posts-draft-column.component';\nexport * from './lib/posts-list/posts-queue-column.component';\nexport * from './lib/posts-list/posts-published-column.component';\nexport * from './lib/posts-list/posts-list.component';\n"]}
@@ -1,10 +1,10 @@
1
1
  import { Component } from '@angular/core';
2
2
  import * as i0 from "@angular/core";
3
3
  export class CmsAdminUi {
4
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: CmsAdminUi, deps: [], target: i0.ɵɵFactoryTarget.Component });
5
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.6", type: CmsAdminUi, isStandalone: true, selector: "lib-cms-admin-ui", ngImport: i0, template: "<p>CmsAdminUi works!</p>\n", styles: [""] });
4
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CmsAdminUi, deps: [], target: i0.ɵɵFactoryTarget.Component });
5
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.4", type: CmsAdminUi, isStandalone: true, selector: "lib-cms-admin-ui", ngImport: i0, template: "<p>CmsAdminUi works!</p>\n", styles: [""] });
6
6
  }
7
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.6", ngImport: i0, type: CmsAdminUi, decorators: [{
7
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: CmsAdminUi, decorators: [{
8
8
  type: Component,
9
9
  args: [{ selector: 'lib-cms-admin-ui', imports: [], template: "<p>CmsAdminUi works!</p>\n" }]
10
10
  }] });
@@ -0,0 +1,84 @@
1
+ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
2
+ import { FormsModule } from '@angular/forms';
3
+ import { MatFormFieldModule } from '@angular/material/form-field';
4
+ import { MatInputModule } from '@angular/material/input';
5
+ import { PageEditorStore } from './page-editor.store';
6
+ import { PageEditorHeroImageComponent } from './page-editor-hero-image.component';
7
+ import * as i0 from "@angular/core";
8
+ import * as i1 from "@angular/material/form-field";
9
+ import * as i2 from "@angular/material/input";
10
+ export class AboutEditorFormComponent {
11
+ store = inject(PageEditorStore);
12
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AboutEditorFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
13
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: AboutEditorFormComponent, isStandalone: true, selector: "admin-about-editor-form", ngImport: i0, template: `
14
+ @if (store.page(); as page) {
15
+ @if (page.type === 'about') {
16
+ <div class="flex flex-col gap-4 p-4">
17
+ <mat-form-field class="w-full shrink-0">
18
+ <mat-label>Title</mat-label>
19
+ <input
20
+ matInput
21
+ [value]="page.title"
22
+ (input)="store.updateField('title', $any($event.target).value)"
23
+ placeholder="About"
24
+ />
25
+ </mat-form-field>
26
+
27
+ <admin-page-editor-hero-image class="shrink-0" />
28
+
29
+ <div class="flex flex-col gap-1">
30
+ <label class="text-xs font-medium opacity-60">Body (Markdown)</label>
31
+ <textarea
32
+ class="markdown-textarea"
33
+ [value]="page.body"
34
+ (input)="store.updateField('body', $any($event.target).value)"
35
+ (click)="store.setCursorPosition($any($event.target).selectionStart)"
36
+ (keyup)="store.setCursorPosition($any($event.target).selectionStart)"
37
+ placeholder="Write your about page content in Markdown…"
38
+ ></textarea>
39
+ </div>
40
+ </div>
41
+ }
42
+ }
43
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto}.markdown-textarea{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.875rem;line-height:1.5;resize:none;flex:1;min-height:200px;width:100%;padding:.5rem;outline:none;background:transparent;color:inherit;border:1px solid color-mix(in srgb,currentColor 20%,transparent);border-radius:4px}.markdown-textarea:focus{border-color:var(--mat-sys-primary);outline:none}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i1.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i1.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i2.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "component", type: PageEditorHeroImageComponent, selector: "admin-page-editor-hero-image" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
44
+ }
45
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: AboutEditorFormComponent, decorators: [{
46
+ type: Component,
47
+ args: [{ selector: 'admin-about-editor-form', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
48
+ FormsModule,
49
+ MatFormFieldModule,
50
+ MatInputModule,
51
+ PageEditorHeroImageComponent,
52
+ ], template: `
53
+ @if (store.page(); as page) {
54
+ @if (page.type === 'about') {
55
+ <div class="flex flex-col gap-4 p-4">
56
+ <mat-form-field class="w-full shrink-0">
57
+ <mat-label>Title</mat-label>
58
+ <input
59
+ matInput
60
+ [value]="page.title"
61
+ (input)="store.updateField('title', $any($event.target).value)"
62
+ placeholder="About"
63
+ />
64
+ </mat-form-field>
65
+
66
+ <admin-page-editor-hero-image class="shrink-0" />
67
+
68
+ <div class="flex flex-col gap-1">
69
+ <label class="text-xs font-medium opacity-60">Body (Markdown)</label>
70
+ <textarea
71
+ class="markdown-textarea"
72
+ [value]="page.body"
73
+ (input)="store.updateField('body', $any($event.target).value)"
74
+ (click)="store.setCursorPosition($any($event.target).selectionStart)"
75
+ (keyup)="store.setCursorPosition($any($event.target).selectionStart)"
76
+ placeholder="Write your about page content in Markdown…"
77
+ ></textarea>
78
+ </div>
79
+ </div>
80
+ }
81
+ }
82
+ `, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto}.markdown-textarea{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;font-size:.875rem;line-height:1.5;resize:none;flex:1;min-height:200px;width:100%;padding:.5rem;outline:none;background:transparent;color:inherit;border:1px solid color-mix(in srgb,currentColor 20%,transparent);border-radius:4px}.markdown-textarea:focus{border-color:var(--mat-sys-primary);outline:none}\n"] }]
83
+ }] });
84
+ //# sourceMappingURL=about-editor-form.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"about-editor-form.component.js","sourceRoot":"","sources":["../../../../../../libs/cms-admin-ui/src/lib/page-editor/about-editor-form.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC3E,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,4BAA4B,EAAE,MAAM,oCAAoC,CAAC;;;;AA0ElF,MAAM,OAAO,wBAAwB;IAC1B,KAAK,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;uGAD9B,wBAAwB;2FAAxB,wBAAwB,mFAhCzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BT,iiBAjEC,WAAW,8BACX,kBAAkB,0SAClB,cAAc,kYACd,4BAA4B;;2FAgEnB,wBAAwB;kBAxEpC,SAAS;+BACE,yBAAyB,cACvB,IAAI,mBACC,uBAAuB,CAAC,MAAM,WACtC;wBACP,WAAW;wBACX,kBAAkB;wBAClB,cAAc;wBACd,4BAA4B;qBAC7B,YA+BS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BT","sourcesContent":["import { ChangeDetectionStrategy, Component, inject } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { MatFormFieldModule } from '@angular/material/form-field';\nimport { MatInputModule } from '@angular/material/input';\nimport { PageEditorStore } from './page-editor.store';\nimport { PageEditorHeroImageComponent } from './page-editor-hero-image.component';\n\n@Component({\n selector: 'admin-about-editor-form',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [\n FormsModule,\n MatFormFieldModule,\n MatInputModule,\n PageEditorHeroImageComponent,\n ],\n styles: [\n `\n :host {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n }\n .markdown-textarea {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n font-size: 0.875rem;\n line-height: 1.5;\n resize: none;\n flex: 1;\n min-height: 200px;\n width: 100%;\n padding: 0.5rem;\n outline: none;\n background: transparent;\n color: inherit;\n border: 1px solid color-mix(in srgb, currentColor 20%, transparent);\n border-radius: 4px;\n }\n .markdown-textarea:focus {\n border-color: var(--mat-sys-primary);\n outline: none;\n }\n `,\n ],\n template: `\n @if (store.page(); as page) {\n @if (page.type === 'about') {\n <div class=\"flex flex-col gap-4 p-4\">\n <mat-form-field class=\"w-full shrink-0\">\n <mat-label>Title</mat-label>\n <input\n matInput\n [value]=\"page.title\"\n (input)=\"store.updateField('title', $any($event.target).value)\"\n placeholder=\"About\"\n />\n </mat-form-field>\n\n <admin-page-editor-hero-image class=\"shrink-0\" />\n\n <div class=\"flex flex-col gap-1\">\n <label class=\"text-xs font-medium opacity-60\">Body (Markdown)</label>\n <textarea\n class=\"markdown-textarea\"\n [value]=\"page.body\"\n (input)=\"store.updateField('body', $any($event.target).value)\"\n (click)=\"store.setCursorPosition($any($event.target).selectionStart)\"\n (keyup)=\"store.setCursorPosition($any($event.target).selectionStart)\"\n placeholder=\"Write your about page content in Markdown…\"\n ></textarea>\n </div>\n </div>\n }\n }\n `,\n})\nexport class AboutEditorFormComponent {\n readonly store = inject(PageEditorStore);\n}\n"]}
@@ -0,0 +1,474 @@
1
+ import { ChangeDetectionStrategy, Component, PLATFORM_ID, ViewChild, inject, signal, } from '@angular/core';
2
+ import { isPlatformBrowser } from '@angular/common';
3
+ import { FormsModule } from '@angular/forms';
4
+ import { MatButtonModule } from '@angular/material/button';
5
+ import { MatFormFieldModule } from '@angular/material/form-field';
6
+ import { MatIconModule } from '@angular/material/icon';
7
+ import { MatInputModule } from '@angular/material/input';
8
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
9
+ import { MatSelectModule } from '@angular/material/select';
10
+ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
11
+ import { DragDropModule, moveItemInArray, } from '@angular/cdk/drag-drop';
12
+ import { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage';
13
+ import { FIREBASE_STORAGE, PageService } from '@foliokit/cms-core';
14
+ import { PageEditorStore } from './page-editor.store';
15
+ import * as i0 from "@angular/core";
16
+ import * as i1 from "@angular/material/button";
17
+ import * as i2 from "@angular/material/form-field";
18
+ import * as i3 from "@angular/material/icon";
19
+ import * as i4 from "@angular/material/input";
20
+ import * as i5 from "@angular/material/progress-bar";
21
+ import * as i6 from "@angular/material/select";
22
+ import * as i7 from "@angular/material/slide-toggle";
23
+ import * as i8 from "@angular/cdk/drag-drop";
24
+ const PLATFORM_OPTIONS = [
25
+ 'twitter',
26
+ 'instagram',
27
+ 'github',
28
+ 'linkedin',
29
+ 'youtube',
30
+ 'tiktok',
31
+ 'facebook',
32
+ 'email',
33
+ 'website',
34
+ ];
35
+ export class LinksEditorFormComponent {
36
+ avatarInput;
37
+ store = inject(PageEditorStore);
38
+ storage = inject(FIREBASE_STORAGE);
39
+ pageService = inject(PageService);
40
+ platformId = inject(PLATFORM_ID);
41
+ isBrowser = isPlatformBrowser(this.platformId);
42
+ platformOptions = PLATFORM_OPTIONS;
43
+ avatarUploading = signal(false, ...(ngDevMode ? [{ debugName: "avatarUploading" }] : /* istanbul ignore next */ []));
44
+ avatarProgress = signal(0, ...(ngDevMode ? [{ debugName: "avatarProgress" }] : /* istanbul ignore next */ []));
45
+ avatarError = signal(null, ...(ngDevMode ? [{ debugName: "avatarError" }] : /* istanbul ignore next */ []));
46
+ avatarStoragePath = signal(null, ...(ngDevMode ? [{ debugName: "avatarStoragePath" }] : /* istanbul ignore next */ []));
47
+ onAvatarSelected(files) {
48
+ if (!files?.length)
49
+ return;
50
+ this.uploadAvatar(files[0]);
51
+ if (this.avatarInput?.nativeElement) {
52
+ this.avatarInput.nativeElement.value = '';
53
+ }
54
+ }
55
+ onDeleteAvatar() {
56
+ if (!window.confirm('Remove avatar?'))
57
+ return;
58
+ const path = this.avatarStoragePath();
59
+ const clear = () => {
60
+ this.store.updateField('avatarUrl', undefined);
61
+ this.store.updateField('avatarAlt', undefined);
62
+ this.avatarStoragePath.set(null);
63
+ };
64
+ if (path) {
65
+ this.pageService.deleteStorageFile(path).subscribe({ next: clear, error: clear });
66
+ }
67
+ else {
68
+ clear();
69
+ }
70
+ }
71
+ addLink(links) {
72
+ const newLink = {
73
+ id: crypto.randomUUID(),
74
+ label: '',
75
+ url: '',
76
+ order: links.length,
77
+ };
78
+ this.store.updateField('links', [...links, newLink]);
79
+ }
80
+ deleteLink(links, id) {
81
+ const updated = links
82
+ .filter((l) => l.id !== id)
83
+ .map((l, i) => ({ ...l, order: i }));
84
+ this.store.updateField('links', updated);
85
+ }
86
+ updateLink(links, id, field, value) {
87
+ const updated = links.map((l) => l.id === id ? { ...l, [field]: value } : l);
88
+ this.store.updateField('links', updated);
89
+ }
90
+ onDrop(event, links) {
91
+ const reordered = [...links];
92
+ moveItemInArray(reordered, event.previousIndex, event.currentIndex);
93
+ const withOrder = reordered.map((l, i) => ({ ...l, order: i }));
94
+ this.store.updateField('links', withOrder);
95
+ }
96
+ uploadAvatar(file) {
97
+ const previous = this.avatarStoragePath();
98
+ const pageId = this.store.page()?.id || this.store.tempPageId();
99
+ const storagePath = `pages/${pageId}/avatar/${file.name}`;
100
+ if (previous) {
101
+ this.pageService.deleteStorageFile(previous).subscribe();
102
+ }
103
+ const fileRef = ref(this.storage, storagePath);
104
+ this.avatarUploading.set(true);
105
+ this.avatarProgress.set(0);
106
+ this.avatarError.set(null);
107
+ const task = uploadBytesResumable(fileRef, file);
108
+ task.on('state_changed', (snapshot) => {
109
+ this.avatarProgress.set(Math.round((snapshot.bytesTransferred / snapshot.totalBytes) * 100));
110
+ }, (error) => {
111
+ this.avatarUploading.set(false);
112
+ this.avatarError.set(error.message);
113
+ }, () => {
114
+ getDownloadURL(task.snapshot.ref).then((downloadUrl) => {
115
+ this.store.updateField('avatarUrl', downloadUrl);
116
+ this.store.updateField('avatarAlt', file.name);
117
+ this.avatarStoragePath.set(storagePath);
118
+ this.avatarUploading.set(false);
119
+ });
120
+ });
121
+ }
122
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LinksEditorFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
123
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.4", type: LinksEditorFormComponent, isStandalone: true, selector: "admin-links-editor-form", viewQueries: [{ propertyName: "avatarInput", first: true, predicate: ["avatarInput"], descendants: true }], ngImport: i0, template: `
124
+ @if (store.page(); as page) {
125
+ @if (page.type === 'links') {
126
+ <div class="flex flex-col gap-6 p-4">
127
+ <!-- Title -->
128
+ <mat-form-field class="w-full">
129
+ <mat-label>Title</mat-label>
130
+ <input
131
+ matInput
132
+ [value]="page.title"
133
+ (input)="store.updateField('title', $any($event.target).value)"
134
+ placeholder="Links"
135
+ />
136
+ </mat-form-field>
137
+
138
+ <!-- Avatar upload -->
139
+ <div class="flex flex-col gap-2">
140
+ <span class="text-sm font-semibold">Avatar</span>
141
+ @if (avatarUploading()) {
142
+ <mat-progress-bar mode="determinate" [value]="avatarProgress()" />
143
+ }
144
+ @if (avatarError()) {
145
+ <p class="text-sm text-red-500">{{ avatarError() }}</p>
146
+ }
147
+ <div class="flex items-center gap-4">
148
+ @if (page.avatarUrl) {
149
+ <img
150
+ [src]="page.avatarUrl"
151
+ [alt]="page.avatarAlt || 'Avatar'"
152
+ class="w-16 h-16 rounded-full object-cover shrink-0"
153
+ />
154
+ } @else {
155
+ <div class="w-16 h-16 rounded-full flex items-center justify-center shrink-0"
156
+ style="background: color-mix(in srgb, currentColor 10%, transparent)">
157
+ <mat-icon class="opacity-40">person</mat-icon>
158
+ </div>
159
+ }
160
+ <button mat-stroked-button [disabled]="avatarUploading()" (click)="isBrowser && avatarInput.click()">
161
+ {{ page.avatarUrl ? 'Replace' : 'Upload' }}
162
+ </button>
163
+ @if (page.avatarUrl) {
164
+ <button mat-icon-button (click)="onDeleteAvatar()" title="Remove avatar">
165
+ <mat-icon>delete</mat-icon>
166
+ </button>
167
+ }
168
+ </div>
169
+ <input
170
+ #avatarInput
171
+ type="file"
172
+ accept="image/*"
173
+ class="hidden"
174
+ (change)="onAvatarSelected($any($event.target).files)"
175
+ />
176
+ </div>
177
+
178
+ <!-- Headline -->
179
+ <mat-form-field class="w-full">
180
+ <mat-label>Headline</mat-label>
181
+ <input
182
+ matInput
183
+ [value]="page.headline ?? ''"
184
+ (input)="store.updateField('headline', $any($event.target).value)"
185
+ placeholder="Your name or tagline"
186
+ />
187
+ </mat-form-field>
188
+
189
+ <!-- Bio -->
190
+ <mat-form-field class="w-full">
191
+ <mat-label>Bio</mat-label>
192
+ <textarea
193
+ matInput
194
+ rows="3"
195
+ [value]="page.bio ?? ''"
196
+ (input)="store.updateField('bio', $any($event.target).value)"
197
+ placeholder="Short bio shown below your headline"
198
+ ></textarea>
199
+ </mat-form-field>
200
+
201
+ <!-- Link list -->
202
+ <div class="flex flex-col gap-3">
203
+ <div class="flex items-center justify-between">
204
+ <span class="text-sm font-semibold">Links</span>
205
+ <button mat-stroked-button (click)="addLink(page.links)">
206
+ <mat-icon>add</mat-icon>
207
+ Add Link
208
+ </button>
209
+ </div>
210
+
211
+ <div
212
+ cdkDropList
213
+ (cdkDropListDropped)="onDrop($event, page.links)"
214
+ class="flex flex-col gap-2"
215
+ >
216
+ @for (link of page.links; track link.id) {
217
+ <div
218
+ cdkDrag
219
+ class="flex flex-col gap-3 p-3 rounded-lg"
220
+ style="background: color-mix(in srgb, currentColor 5%, transparent); border: 1px solid color-mix(in srgb, currentColor 10%, transparent)"
221
+ >
222
+ <!-- Drag handle row -->
223
+ <div class="flex items-center gap-2">
224
+ <mat-icon cdkDragHandle class="drag-handle opacity-40 shrink-0" style="font-size: 1.25rem; width: 1.25rem; height: 1.25rem">
225
+ drag_indicator
226
+ </mat-icon>
227
+ <span class="flex-1 text-sm font-medium truncate">{{ link.label || '(untitled)' }}</span>
228
+ <mat-slide-toggle
229
+ [checked]="!!link.highlighted"
230
+ (change)="updateLink(page.links, link.id, 'highlighted', $event.checked)"
231
+ class="shrink-0"
232
+ >
233
+ Highlighted
234
+ </mat-slide-toggle>
235
+ <button mat-icon-button (click)="deleteLink(page.links, link.id)" title="Delete link">
236
+ <mat-icon>delete</mat-icon>
237
+ </button>
238
+ </div>
239
+
240
+ <!-- Label + URL -->
241
+ <div class="flex gap-3">
242
+ <mat-form-field class="flex-1">
243
+ <mat-label>Label</mat-label>
244
+ <input
245
+ matInput
246
+ [value]="link.label"
247
+ (input)="updateLink(page.links, link.id, 'label', $any($event.target).value)"
248
+ placeholder="My Website"
249
+ />
250
+ </mat-form-field>
251
+ <mat-form-field class="flex-1">
252
+ <mat-label>URL</mat-label>
253
+ <input
254
+ matInput
255
+ type="url"
256
+ [value]="link.url"
257
+ (input)="updateLink(page.links, link.id, 'url', $any($event.target).value)"
258
+ placeholder="https://example.com"
259
+ />
260
+ </mat-form-field>
261
+ </div>
262
+
263
+ <!-- Platform -->
264
+ <mat-form-field class="w-full">
265
+ <mat-label>Platform</mat-label>
266
+ <mat-select
267
+ [value]="link.platform ?? null"
268
+ (selectionChange)="updateLink(page.links, link.id, 'platform', $event.value)"
269
+ >
270
+ <mat-option [value]="null">— none —</mat-option>
271
+ @for (p of platformOptions; track p) {
272
+ <mat-option [value]="p">{{ p }}</mat-option>
273
+ }
274
+ </mat-select>
275
+ </mat-form-field>
276
+ </div>
277
+ }
278
+
279
+ @if (!page.links.length) {
280
+ <div class="flex items-center justify-center py-8 opacity-40 text-sm">
281
+ No links yet. Add one above.
282
+ </div>
283
+ }
284
+ </div>
285
+ </div>
286
+ </div>
287
+ }
288
+ }
289
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto}.drag-handle{cursor:grab;touch-action:none}.drag-handle:active{cursor:grabbing}.cdk-drag-preview{box-shadow:0 4px 16px #00000026;border-radius:8px;opacity:.95}.cdk-drag-placeholder{opacity:.3}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-zone{border:2px dashed color-mix(in srgb,currentColor 25%,transparent);border-radius:8px}.drop-zone.drag-over{border-color:var(--mat-sys-primary);background:color-mix(in srgb,var(--mat-sys-primary) 8%,transparent)}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i2.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i2.MatLabel, selector: "mat-label" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i5.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i6.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i6.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatSlideToggleModule }, { kind: "component", type: i7.MatSlideToggle, selector: "mat-slide-toggle", inputs: ["name", "id", "labelPosition", "aria-label", "aria-labelledby", "aria-describedby", "required", "color", "disabled", "disableRipple", "tabIndex", "checked", "hideIcon", "disabledInteractive"], outputs: ["change", "toggleChange"], exportAs: ["matSlideToggle"] }, { kind: "ngmodule", type: DragDropModule }, { kind: "directive", type: i8.CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "directive", type: i8.CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: i8.CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
290
+ }
291
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.4", ngImport: i0, type: LinksEditorFormComponent, decorators: [{
292
+ type: Component,
293
+ args: [{ selector: 'admin-links-editor-form', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
294
+ FormsModule,
295
+ MatButtonModule,
296
+ MatFormFieldModule,
297
+ MatIconModule,
298
+ MatInputModule,
299
+ MatProgressBarModule,
300
+ MatSelectModule,
301
+ MatSlideToggleModule,
302
+ DragDropModule,
303
+ ], template: `
304
+ @if (store.page(); as page) {
305
+ @if (page.type === 'links') {
306
+ <div class="flex flex-col gap-6 p-4">
307
+ <!-- Title -->
308
+ <mat-form-field class="w-full">
309
+ <mat-label>Title</mat-label>
310
+ <input
311
+ matInput
312
+ [value]="page.title"
313
+ (input)="store.updateField('title', $any($event.target).value)"
314
+ placeholder="Links"
315
+ />
316
+ </mat-form-field>
317
+
318
+ <!-- Avatar upload -->
319
+ <div class="flex flex-col gap-2">
320
+ <span class="text-sm font-semibold">Avatar</span>
321
+ @if (avatarUploading()) {
322
+ <mat-progress-bar mode="determinate" [value]="avatarProgress()" />
323
+ }
324
+ @if (avatarError()) {
325
+ <p class="text-sm text-red-500">{{ avatarError() }}</p>
326
+ }
327
+ <div class="flex items-center gap-4">
328
+ @if (page.avatarUrl) {
329
+ <img
330
+ [src]="page.avatarUrl"
331
+ [alt]="page.avatarAlt || 'Avatar'"
332
+ class="w-16 h-16 rounded-full object-cover shrink-0"
333
+ />
334
+ } @else {
335
+ <div class="w-16 h-16 rounded-full flex items-center justify-center shrink-0"
336
+ style="background: color-mix(in srgb, currentColor 10%, transparent)">
337
+ <mat-icon class="opacity-40">person</mat-icon>
338
+ </div>
339
+ }
340
+ <button mat-stroked-button [disabled]="avatarUploading()" (click)="isBrowser && avatarInput.click()">
341
+ {{ page.avatarUrl ? 'Replace' : 'Upload' }}
342
+ </button>
343
+ @if (page.avatarUrl) {
344
+ <button mat-icon-button (click)="onDeleteAvatar()" title="Remove avatar">
345
+ <mat-icon>delete</mat-icon>
346
+ </button>
347
+ }
348
+ </div>
349
+ <input
350
+ #avatarInput
351
+ type="file"
352
+ accept="image/*"
353
+ class="hidden"
354
+ (change)="onAvatarSelected($any($event.target).files)"
355
+ />
356
+ </div>
357
+
358
+ <!-- Headline -->
359
+ <mat-form-field class="w-full">
360
+ <mat-label>Headline</mat-label>
361
+ <input
362
+ matInput
363
+ [value]="page.headline ?? ''"
364
+ (input)="store.updateField('headline', $any($event.target).value)"
365
+ placeholder="Your name or tagline"
366
+ />
367
+ </mat-form-field>
368
+
369
+ <!-- Bio -->
370
+ <mat-form-field class="w-full">
371
+ <mat-label>Bio</mat-label>
372
+ <textarea
373
+ matInput
374
+ rows="3"
375
+ [value]="page.bio ?? ''"
376
+ (input)="store.updateField('bio', $any($event.target).value)"
377
+ placeholder="Short bio shown below your headline"
378
+ ></textarea>
379
+ </mat-form-field>
380
+
381
+ <!-- Link list -->
382
+ <div class="flex flex-col gap-3">
383
+ <div class="flex items-center justify-between">
384
+ <span class="text-sm font-semibold">Links</span>
385
+ <button mat-stroked-button (click)="addLink(page.links)">
386
+ <mat-icon>add</mat-icon>
387
+ Add Link
388
+ </button>
389
+ </div>
390
+
391
+ <div
392
+ cdkDropList
393
+ (cdkDropListDropped)="onDrop($event, page.links)"
394
+ class="flex flex-col gap-2"
395
+ >
396
+ @for (link of page.links; track link.id) {
397
+ <div
398
+ cdkDrag
399
+ class="flex flex-col gap-3 p-3 rounded-lg"
400
+ style="background: color-mix(in srgb, currentColor 5%, transparent); border: 1px solid color-mix(in srgb, currentColor 10%, transparent)"
401
+ >
402
+ <!-- Drag handle row -->
403
+ <div class="flex items-center gap-2">
404
+ <mat-icon cdkDragHandle class="drag-handle opacity-40 shrink-0" style="font-size: 1.25rem; width: 1.25rem; height: 1.25rem">
405
+ drag_indicator
406
+ </mat-icon>
407
+ <span class="flex-1 text-sm font-medium truncate">{{ link.label || '(untitled)' }}</span>
408
+ <mat-slide-toggle
409
+ [checked]="!!link.highlighted"
410
+ (change)="updateLink(page.links, link.id, 'highlighted', $event.checked)"
411
+ class="shrink-0"
412
+ >
413
+ Highlighted
414
+ </mat-slide-toggle>
415
+ <button mat-icon-button (click)="deleteLink(page.links, link.id)" title="Delete link">
416
+ <mat-icon>delete</mat-icon>
417
+ </button>
418
+ </div>
419
+
420
+ <!-- Label + URL -->
421
+ <div class="flex gap-3">
422
+ <mat-form-field class="flex-1">
423
+ <mat-label>Label</mat-label>
424
+ <input
425
+ matInput
426
+ [value]="link.label"
427
+ (input)="updateLink(page.links, link.id, 'label', $any($event.target).value)"
428
+ placeholder="My Website"
429
+ />
430
+ </mat-form-field>
431
+ <mat-form-field class="flex-1">
432
+ <mat-label>URL</mat-label>
433
+ <input
434
+ matInput
435
+ type="url"
436
+ [value]="link.url"
437
+ (input)="updateLink(page.links, link.id, 'url', $any($event.target).value)"
438
+ placeholder="https://example.com"
439
+ />
440
+ </mat-form-field>
441
+ </div>
442
+
443
+ <!-- Platform -->
444
+ <mat-form-field class="w-full">
445
+ <mat-label>Platform</mat-label>
446
+ <mat-select
447
+ [value]="link.platform ?? null"
448
+ (selectionChange)="updateLink(page.links, link.id, 'platform', $event.value)"
449
+ >
450
+ <mat-option [value]="null">— none —</mat-option>
451
+ @for (p of platformOptions; track p) {
452
+ <mat-option [value]="p">{{ p }}</mat-option>
453
+ }
454
+ </mat-select>
455
+ </mat-form-field>
456
+ </div>
457
+ }
458
+
459
+ @if (!page.links.length) {
460
+ <div class="flex items-center justify-center py-8 opacity-40 text-sm">
461
+ No links yet. Add one above.
462
+ </div>
463
+ }
464
+ </div>
465
+ </div>
466
+ </div>
467
+ }
468
+ }
469
+ `, styles: [":host{display:flex;flex-direction:column;flex:1;min-height:0;overflow-y:auto}.drag-handle{cursor:grab;touch-action:none}.drag-handle:active{cursor:grabbing}.cdk-drag-preview{box-shadow:0 4px 16px #00000026;border-radius:8px;opacity:.95}.cdk-drag-placeholder{opacity:.3}.cdk-drag-animating{transition:transform .25s cubic-bezier(0,0,.2,1)}.drop-zone{border:2px dashed color-mix(in srgb,currentColor 25%,transparent);border-radius:8px}.drop-zone.drag-over{border-color:var(--mat-sys-primary);background:color-mix(in srgb,var(--mat-sys-primary) 8%,transparent)}\n"] }]
470
+ }], propDecorators: { avatarInput: [{
471
+ type: ViewChild,
472
+ args: ['avatarInput']
473
+ }] } });
474
+ //# sourceMappingURL=links-editor-form.component.js.map