@alliance-droid/status-feedback-system 1.3.10 → 1.3.12

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.
@@ -16,6 +16,8 @@
16
16
  interface Props {
17
17
  apiUrl: string;
18
18
  apiKey: string;
19
+ /** Authenticated user email. Required for submitting feedback and replies. */
20
+ userEmail?: string;
19
21
  /** Category labels and icons — order determines sidebar order */
20
22
  categoryConfig?: Record<string, CategoryConfig>;
21
23
  /** Title shown at the top */
@@ -29,6 +31,7 @@
29
31
  let {
30
32
  apiUrl,
31
33
  apiKey,
34
+ userEmail,
32
35
  categoryConfig = {},
33
36
  title = 'Feedback Board',
34
37
  showSubmit = true,
@@ -36,7 +39,7 @@
36
39
  }: Props = $props();
37
40
 
38
41
  let board: FeedbackBoardState = $derived(
39
- createFeedbackBoard({ apiUrl, apiKey, categoryConfig })
42
+ createFeedbackBoard({ apiUrl, apiKey, userEmail, categoryConfig })
40
43
  );
41
44
 
42
45
  let showForm = $state(false);
@@ -2,6 +2,8 @@ import { type CategoryConfig } from './board/index.js';
2
2
  interface Props {
3
3
  apiUrl: string;
4
4
  apiKey: string;
5
+ /** Authenticated user email. Required for submitting feedback and replies. */
6
+ userEmail?: string;
5
7
  /** Category labels and icons — order determines sidebar order */
6
8
  categoryConfig?: Record<string, CategoryConfig>;
7
9
  /** Title shown at the top */
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte';
3
- import { Badge, Spinner, Button, Textarea, Input } from '@alliance-droid/svelte-component-library';
3
+ import { Badge, Spinner, Button, Textarea } from '@alliance-droid/svelte-component-library';
4
4
  import type { BoardItem } from '../../api.js';
5
5
  import type { FeedbackResponse } from '../../types.js';
6
6
  import type { FeedbackBoardState } from './state.svelte.js';
@@ -18,29 +18,26 @@
18
18
 
19
19
  // Response form state
20
20
  let replyMessage = $state('');
21
- let replyAuthor = $state('');
22
21
  let submittingReply = $state(false);
23
22
  let replyError = $state('');
24
23
  let replySuccess = $state(false);
25
24
 
25
+ let isAuthenticated = $derived(!!board.userEmail);
26
+
26
27
  onMount(async () => {
27
28
  responses = await board.getResponses(item.id);
28
29
  loadingResponses = false;
29
30
  });
30
31
 
31
32
  async function handleSubmitReply() {
32
- if (!replyMessage.trim()) return;
33
+ if (!replyMessage.trim() || !isAuthenticated) return;
33
34
  submittingReply = true;
34
35
  replyError = '';
35
36
  try {
36
- await board.submitResponse(item.id, {
37
- message: replyMessage.trim(),
38
- author: replyAuthor.trim() || undefined,
39
- });
37
+ await board.submitResponse(item.id, replyMessage.trim());
40
38
  // Refresh responses
41
39
  responses = await board.getResponses(item.id);
42
40
  replyMessage = '';
43
- replyAuthor = '';
44
41
  replySuccess = true;
45
42
  setTimeout(() => { replySuccess = false; }, 2000);
46
43
  } catch (err) {
@@ -134,24 +131,27 @@
134
131
  <h4 class="text-sm font-semibold text-foreground">
135
132
  <i class="fa-solid fa-reply mr-1" aria-hidden="true"></i> Add a Response
136
133
  </h4>
137
- {#if replySuccess}
134
+ {#if !isAuthenticated}
135
+ <p class="text-sm text-foreground-tertiary">
136
+ <i class="fa-solid fa-lock mr-1" aria-hidden="true"></i>
137
+ Sign in to respond
138
+ </p>
139
+ {:else if replySuccess}
138
140
  <p class="text-sm text-green-400 font-semibold">
139
141
  <i class="fa-solid fa-check-circle mr-1" aria-hidden="true"></i> Response submitted!
140
142
  </p>
141
143
  {:else}
144
+ <div class="flex items-center gap-1.5 text-xs text-foreground-tertiary">
145
+ <i class="fa-solid fa-user" aria-hidden="true"></i>
146
+ <span>Replying as <span class="font-medium text-foreground-secondary">{board.userEmail}</span></span>
147
+ </div>
142
148
  <Textarea
143
149
  value={replyMessage}
144
150
  oninput={(e) => (replyMessage = (e.currentTarget as HTMLTextAreaElement).value)}
145
151
  placeholder="Share your thoughts..."
146
152
  rows={3}
147
153
  />
148
- <div class="flex gap-2 items-center">
149
- <div class="flex-1">
150
- <Input
151
- bind:value={replyAuthor}
152
- placeholder="Your name (optional)"
153
- />
154
- </div>
154
+ <div class="flex gap-2 items-center justify-end">
155
155
  <Button
156
156
  variant="primary"
157
157
  size="sm"
@@ -6,26 +6,25 @@
6
6
  board: FeedbackBoardState;
7
7
  /** Available category options for the dropdown */
8
8
  categories?: string[];
9
- /** Pre-fill the email field (e.g. from the logged-in user's session) */
10
- userEmail?: string;
11
9
  }
12
10
 
13
- let { board, categories = [], userEmail }: Props = $props();
11
+ let { board, categories = [] }: Props = $props();
14
12
 
15
13
  let title = $state('');
16
14
  let message = $state('');
17
15
  let category = $state('');
18
- let email = $derived(userEmail ?? '');
19
16
  let submitting = $state(false);
20
17
  let success = $state(false);
21
18
  let error = $state('');
22
19
 
20
+ let isAuthenticated = $derived(!!board.userEmail);
21
+
23
22
  let categoryOptions = $derived(
24
23
  categories.map((c) => ({ value: c, label: board.getCategoryLabel(c) }))
25
24
  );
26
25
 
27
26
  async function handleSubmit() {
28
- if (!message.trim()) return;
27
+ if (!message.trim() || !isAuthenticated) return;
29
28
  submitting = true;
30
29
  error = '';
31
30
  try {
@@ -33,13 +32,11 @@
33
32
  title: title.trim() || undefined,
34
33
  message: message.trim(),
35
34
  category: category || undefined,
36
- email: email.trim() || undefined,
37
35
  });
38
36
  success = true;
39
37
  title = '';
40
38
  message = '';
41
39
  category = '';
42
- email = '';
43
40
  setTimeout(() => { success = false; }, 2000);
44
41
  } catch (err) {
45
42
  error = err instanceof Error ? err.message : 'Failed to submit';
@@ -50,12 +47,21 @@
50
47
  </script>
51
48
 
52
49
  <Card variant="outlined" padding="md">
53
- {#if success}
50
+ {#if !isAuthenticated}
51
+ <div class="text-center py-3 text-foreground-tertiary">
52
+ <i class="fa-solid fa-lock mr-1" aria-hidden="true"></i>
53
+ <span class="text-sm">Sign in to submit feedback</span>
54
+ </div>
55
+ {:else if success}
54
56
  <div class="text-center py-2 text-green-400 font-semibold">
55
57
  <i class="fa-solid fa-check-circle mr-1"></i> Submitted!
56
58
  </div>
57
59
  {:else}
58
60
  <div class="space-y-3">
61
+ <div class="flex items-center gap-1.5 text-xs text-foreground-tertiary">
62
+ <i class="fa-solid fa-user" aria-hidden="true"></i>
63
+ <span>Submitting as <span class="font-medium text-foreground-secondary">{board.userEmail}</span></span>
64
+ </div>
59
65
  <Input
60
66
  bind:value={title}
61
67
  placeholder="Title (optional)"
@@ -81,13 +87,7 @@
81
87
  />
82
88
  </div>
83
89
  {/if}
84
- <div class="flex-1">
85
- <Input
86
- type="email"
87
- bind:value={email}
88
- placeholder="Email (optional)"
89
- />
90
- </div>
90
+ <div class="flex-1"></div>
91
91
  <Button
92
92
  variant="primary"
93
93
  disabled={submitting || !message.trim()}
@@ -3,8 +3,6 @@ interface Props {
3
3
  board: FeedbackBoardState;
4
4
  /** Available category options for the dropdown */
5
5
  categories?: string[];
6
- /** Pre-fill the email field (e.g. from the logged-in user's session) */
7
- userEmail?: string;
8
6
  }
9
7
  declare const FeedbackSubmitForm: import("svelte").Component<Props, {}, "">;
10
8
  type FeedbackSubmitForm = ReturnType<typeof FeedbackSubmitForm>;
@@ -17,12 +17,15 @@ export interface FeedbackBoardOptions {
17
17
  apiUrl: string;
18
18
  apiKey: string;
19
19
  categoryConfig?: Record<string, CategoryConfig>;
20
+ /** Authenticated user email. Required for submitting feedback and replies. */
21
+ userEmail?: string;
20
22
  /** Items per page. Default: 50 */
21
23
  pageSize?: number;
22
24
  }
23
25
  export interface FeedbackBoardState {
24
26
  readonly client: StatusClient;
25
27
  readonly categoryConfig: Record<string, CategoryConfig>;
28
+ readonly userEmail: string | null;
26
29
  readonly items: BoardItem[];
27
30
  readonly categories: BoardCategory[];
28
31
  readonly total: number;
@@ -43,12 +46,8 @@ export interface FeedbackBoardState {
43
46
  title?: string;
44
47
  message: string;
45
48
  category?: string;
46
- email?: string;
47
- }): Promise<void>;
48
- submitResponse(itemId: string, data: {
49
- message: string;
50
- author?: string;
51
49
  }): Promise<void>;
50
+ submitResponse(itemId: string, message: string): Promise<void>;
52
51
  getCategoryIcon(name: string): string;
53
52
  getCategoryLabel(name: string): string;
54
53
  getCategoryDescription(name: string): string;
@@ -8,8 +8,9 @@
8
8
  */
9
9
  import { createStatusClient } from '../../api.js';
10
10
  export function createFeedbackBoard(options) {
11
- const { apiUrl, apiKey, categoryConfig = {}, pageSize = 50 } = options;
11
+ const { apiUrl, apiKey, categoryConfig = {}, userEmail, pageSize = 50 } = options;
12
12
  const client = createStatusClient({ apiUrl, apiKey });
13
+ const resolvedEmail = userEmail?.trim() || null;
13
14
  // Reactive state
14
15
  let items = $state([]);
15
16
  let categories = $state([]);
@@ -24,6 +25,8 @@ export function createFeedbackBoard(options) {
24
25
  let voteErrors = $state({});
25
26
  // Response cache
26
27
  const responseCache = {};
28
+ // Track in-flight votes to prevent duplicate requests
29
+ const pendingVotes = new Set();
27
30
  async function load() {
28
31
  loading = true;
29
32
  error = '';
@@ -76,6 +79,9 @@ export function createFeedbackBoard(options) {
76
79
  load();
77
80
  });
78
81
  async function vote(itemId, direction) {
82
+ if (pendingVotes.has(itemId))
83
+ return;
84
+ pendingVotes.add(itemId);
79
85
  const prev = votedItems[itemId];
80
86
  // Clear any prior error for this item
81
87
  if (voteErrors[itemId]) {
@@ -103,6 +109,9 @@ export function createFeedbackBoard(options) {
103
109
  voteErrors = rest;
104
110
  }, 3000);
105
111
  }
112
+ finally {
113
+ pendingVotes.delete(itemId);
114
+ }
106
115
  }
107
116
  async function getResponses(itemId) {
108
117
  if (responseCache[itemId])
@@ -117,14 +126,19 @@ export function createFeedbackBoard(options) {
117
126
  }
118
127
  }
119
128
  async function submitFeedback(data) {
129
+ if (!resolvedEmail)
130
+ throw new Error('Authentication required to submit feedback');
120
131
  await client.submitFeedback({
121
132
  ...data,
133
+ email: resolvedEmail,
122
134
  pageUrl: typeof window !== 'undefined' ? window.location.href : undefined,
123
135
  });
124
136
  await load();
125
137
  }
126
- async function submitResponse(itemId, data) {
127
- await client.submitResponse(itemId, data);
138
+ async function submitResponse(itemId, message) {
139
+ if (!resolvedEmail)
140
+ throw new Error('Authentication required to submit a response');
141
+ await client.submitResponse(itemId, { message, author: resolvedEmail });
128
142
  // Invalidate cache so next getResponses fetches fresh data
129
143
  delete responseCache[itemId];
130
144
  // Update response count on the item
@@ -135,6 +149,7 @@ export function createFeedbackBoard(options) {
135
149
  return {
136
150
  get client() { return client; },
137
151
  get categoryConfig() { return categoryConfig; },
152
+ get userEmail() { return resolvedEmail; },
138
153
  get items() { return items; },
139
154
  get categories() { return categories; },
140
155
  get total() { return total; },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alliance-droid/status-feedback-system",
3
- "version": "1.3.10",
3
+ "version": "1.3.12",
4
4
  "type": "module",
5
5
  "description": "Drop-in system status pages and user feedback forms for SvelteKit apps",
6
6
  "svelte": "./dist/index.js",