-
Notifications
You must be signed in to change notification settings - Fork 6
ListsController #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
ListsController #79
Changes from all commits
4df20e8
dae2f1a
10480a9
a65992e
e6dff7d
0a341aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| <template> | ||
| <div | ||
| v-if="isOpen" | ||
| class="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-0" | ||
| aria-labelledby="add-subscribers-modal-title" | ||
| role="dialog" | ||
| aria-modal="true" | ||
| > | ||
| <div class="fixed inset-0 bg-slate-900/50 transition-opacity" aria-hidden="true" @click="close"></div> | ||
|
|
||
| <div class="relative z-10 w-full overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:max-w-lg sm:w-full"> | ||
| <form class="mt-4 space-y-4" @submit.prevent="submitAddSubscribers"> | ||
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6"> | ||
| <div class="flex items-center justify-between"> | ||
| <h3 id="add-subscribers-modal-title" class="text-lg font-medium leading-6 text-slate-900"> | ||
| Add subscribers | ||
| </h3> | ||
|
|
||
| <button type="button" class="text-slate-400 hover:text-slate-500" @click="close"> | ||
| <BaseIcon name="close" class="w-5 h-5" /> | ||
| </button> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label for="subscriber-emails" class="block text-sm font-medium text-slate-700"> | ||
| Email addresses | ||
| </label> | ||
| <textarea | ||
| id="subscriber-emails" | ||
| v-model.trim="addSubsForm.emails" | ||
| rows="8" | ||
| placeholder="john@example.com jane@example.com team@example.com" | ||
| class="mt-1 block w-full rounded-md border border-slate-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 sm:text-sm" | ||
| ></textarea> | ||
| <p class="mt-1 text-xs text-slate-500"> | ||
| Enter one email per line, or separate multiple emails with commas. | ||
| </p> | ||
| </div> | ||
|
|
||
| <p v-if="addSubsError" class="text-sm text-red-600"> | ||
| {{ addSubsError }} | ||
| </p> | ||
| </div> | ||
|
|
||
| <div class="bg-slate-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 gap-2"> | ||
| <button | ||
| type="submit" | ||
| :disabled="addingSubscribers || !addSubsForm.emails.trim()" | ||
| class="w-full inline-flex justify-center rounded-md border border-transparent bg-ext-wf1 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-ext-wf3 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:w-auto sm:text-sm disabled:opacity-50" | ||
| > | ||
| {{ addingSubscribers ? 'Adding...' : 'Add subscribers' }} | ||
| </button> | ||
|
|
||
| <button | ||
| type="button" | ||
| class="mt-3 w-full inline-flex justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-base font-medium text-slate-700 shadow-sm hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm" | ||
| @click="close" | ||
| > | ||
| Cancel | ||
| </button> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script setup> | ||
| import { ref, watch } from 'vue' | ||
| import BaseIcon from '../base/BaseIcon.vue' | ||
| import { subscriptionClient } from '../../api' | ||
|
|
||
| const props = defineProps({ | ||
| isOpen: Boolean, | ||
| list: { | ||
| type: Object, | ||
| default: null | ||
| } | ||
| }) | ||
|
|
||
| const emit = defineEmits(['close', 'added']) | ||
|
|
||
| const addingSubscribers = ref(false) | ||
| const addSubsError = ref('') | ||
| const addSubsForm = ref({ | ||
| emails: '' | ||
| }) | ||
|
|
||
| const resetAddSubsForm = () => { | ||
| addSubsForm.value = { | ||
| emails: '' | ||
| } | ||
| addSubsError.value = '' | ||
| } | ||
|
|
||
| watch( | ||
| () => props.isOpen, | ||
| (isOpen) => { | ||
| if (isOpen) { | ||
| resetAddSubsForm() | ||
| } | ||
| } | ||
| ) | ||
|
|
||
| const close = () => { | ||
| if (addingSubscribers.value) return | ||
| emit('close') | ||
| } | ||
|
|
||
| const parseEmails = (raw) => { | ||
| return raw | ||
| .split(/[\n,;]/) | ||
| .map(email => email.trim()) | ||
| .filter(Boolean) | ||
| } | ||
|
|
||
| const isValidEmail = (email) => { | ||
| return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) | ||
| } | ||
|
|
||
| const submitAddSubscribers = async () => { | ||
| if (addingSubscribers.value) return | ||
|
|
||
| if (!props.list?.id) { | ||
| addSubsError.value = 'No mailing list selected.' | ||
| return | ||
| } | ||
|
|
||
| const emails = parseEmails(addSubsForm.value.emails) | ||
|
|
||
| if (!emails.length) { | ||
| addSubsError.value = 'At least one email is required.' | ||
| return | ||
| } | ||
|
|
||
| const invalidEmails = emails.filter(email => !isValidEmail(email)) | ||
| if (invalidEmails.length) { | ||
| addSubsError.value = `Invalid email(s): ${invalidEmails.join(', ')}` | ||
| return | ||
| } | ||
|
|
||
| addingSubscribers.value = true | ||
| addSubsError.value = '' | ||
|
|
||
| try { | ||
| await subscriptionClient.createSubscriptions(emails, props.list.id) | ||
| emit('added') | ||
| emit('close') | ||
| } catch (error) { | ||
| addSubsError.value = error?.message || 'Failed to add subscribers to the list.' | ||
| } finally { | ||
| addingSubscribers.value = false | ||
| } | ||
| } | ||
| </script> |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,177 @@ | ||||||||
| <template> | ||||||||
| <div | ||||||||
| v-if="isOpen" | ||||||||
| class="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-0" | ||||||||
| aria-labelledby="create-list-modal-title" | ||||||||
| role="dialog" | ||||||||
| aria-modal="true" | ||||||||
| > | ||||||||
| <div class="fixed inset-0 bg-slate-900/50 transition-opacity" aria-hidden="true" @click="close"></div> | ||||||||
| <form class="mt-4 space-y-4" @submit.prevent="submitCreateList"> | ||||||||
| <div class="relative bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:max-w-lg md:min-w-xl sm:w-full z-10"> | ||||||||
| <div class="bg-white px-4 pt-5 pb-4 sm:p-6"> | ||||||||
| <div class="flex justify-between items-center"> | ||||||||
| <h3 id="create-list-modal-title" class="text-lg leading-6 font-medium text-slate-900"> | ||||||||
| Create New List | ||||||||
| </h3> | ||||||||
| <button type="button" class="text-slate-400 hover:text-slate-500" @click="close"> | ||||||||
| <BaseIcon name="close" class="w-5 h-5" /> | ||||||||
| </button> | ||||||||
| </div> | ||||||||
|
|
||||||||
| <div> | ||||||||
| <label for="list-name" class="block text-sm font-medium text-slate-700">Name</label> | ||||||||
| <input | ||||||||
| id="list-name" | ||||||||
| v-model.trim="createForm.name" | ||||||||
| type="text" | ||||||||
| required | ||||||||
| class="mt-1 block w-full border border-slate-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" | ||||||||
| > | ||||||||
| </div> | ||||||||
|
|
||||||||
| <div class="flex items-center"> | ||||||||
| <input | ||||||||
| id="list-public" | ||||||||
| v-model="createForm.public" | ||||||||
| type="checkbox" | ||||||||
| class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-slate-300 rounded" | ||||||||
| > | ||||||||
| <label for="list-public" class="ml-2 block text-sm text-slate-900"> | ||||||||
| Public | ||||||||
| </label> | ||||||||
| </div> | ||||||||
|
|
||||||||
| <div> | ||||||||
| <label for="list-position" class="block text-sm font-medium text-slate-700">List Position (optional)</label> | ||||||||
| <input | ||||||||
| id="list-position" | ||||||||
| v-model="createForm.listPosition" | ||||||||
| type="number" | ||||||||
| min="0" | ||||||||
| step="1" | ||||||||
| class="mt-1 block w-full border border-slate-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" | ||||||||
| > | ||||||||
| </div> | ||||||||
|
|
||||||||
| <div> | ||||||||
| <label for="list-description" class="block text-sm font-medium text-slate-700">Description (optional)</label> | ||||||||
| <textarea | ||||||||
| id="list-description" | ||||||||
| v-model.trim="createForm.description" | ||||||||
| rows="3" | ||||||||
| class="mt-1 block w-full border border-slate-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" | ||||||||
| ></textarea> | ||||||||
| </div> | ||||||||
|
|
||||||||
| <p v-if="createError" class="text-sm text-red-600">{{ createError }}</p> | ||||||||
| </div> | ||||||||
|
|
||||||||
| <div class="bg-slate-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse gap-2"> | ||||||||
| <button | ||||||||
| type="submit" | ||||||||
| :disabled="creatingList || !createForm.name.trim()" | ||||||||
| class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-ext-wf1 text-base font-medium text-white hover:bg-ext-wf3 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto sm:text-sm disabled:opacity-50" | ||||||||
| > | ||||||||
| {{ creatingList ? 'Creating...' : 'Create' }} | ||||||||
| </button> | ||||||||
| <button | ||||||||
| type="button" | ||||||||
| class="mt-3 w-full inline-flex justify-center rounded-md border border-slate-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:w-auto sm:text-sm" | ||||||||
| @click="close" | ||||||||
| > | ||||||||
| Cancel | ||||||||
| </button> | ||||||||
| </div> | ||||||||
| </div> | ||||||||
| </form> | ||||||||
| </div> | ||||||||
| </template> | ||||||||
|
|
||||||||
| <script setup> | ||||||||
| import { ref, watch } from 'vue' | ||||||||
| import { Requests } from '@tatevikgr/rest-api-client' | ||||||||
| import BaseIcon from '../base/BaseIcon.vue' | ||||||||
| import { listClient } from '../../api' | ||||||||
|
|
||||||||
| const props = defineProps({ | ||||||||
| isOpen: Boolean | ||||||||
| }) | ||||||||
|
|
||||||||
| const emit = defineEmits(['close', 'created']) | ||||||||
|
|
||||||||
| const creatingList = ref(false) | ||||||||
| const createError = ref('') | ||||||||
| const createForm = ref({ | ||||||||
| name: '', | ||||||||
| public: false, | ||||||||
| listPosition: '', | ||||||||
| description: '' | ||||||||
| }) | ||||||||
|
|
||||||||
| const resetCreateForm = () => { | ||||||||
| createForm.value = { | ||||||||
| name: '', | ||||||||
| public: false, | ||||||||
| listPosition: '', | ||||||||
| description: '' | ||||||||
| } | ||||||||
| createError.value = '' | ||||||||
| } | ||||||||
|
|
||||||||
| watch( | ||||||||
| () => props.isOpen, | ||||||||
| (isOpen) => { | ||||||||
| if (isOpen) { | ||||||||
| resetCreateForm() | ||||||||
| } | ||||||||
| } | ||||||||
| ) | ||||||||
|
|
||||||||
| const close = () => { | ||||||||
| if (creatingList.value) { | ||||||||
| return | ||||||||
| } | ||||||||
|
|
||||||||
| emit('close') | ||||||||
| } | ||||||||
|
|
||||||||
| const submitCreateList = async () => { | ||||||||
| if (creatingList.value) { | ||||||||
| return | ||||||||
| } | ||||||||
|
|
||||||||
| const name = createForm.value.name.trim() | ||||||||
| if (!name) { | ||||||||
| createError.value = 'Name is required.' | ||||||||
| return | ||||||||
| } | ||||||||
|
|
||||||||
| const parsedPosition = createForm.value.listPosition === '' ? null : Number(createForm.value.listPosition) | ||||||||
|
|
||||||||
|
Comment on lines
+150
to
+151
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trim
💡 Suggested fix- const parsedPosition = createForm.value.listPosition === '' ? null : Number(createForm.value.listPosition)
+ const rawPosition = String(createForm.value.listPosition).trim()
+ const parsedPosition = rawPosition === '' ? null : Number(rawPosition)📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
| if (parsedPosition !== null && (!Number.isInteger(parsedPosition) || parsedPosition < 0)) { | ||||||||
| createError.value = 'List Position must be a whole number greater than or equal to 0.' | ||||||||
| return | ||||||||
| } | ||||||||
|
|
||||||||
| creatingList.value = true | ||||||||
| createError.value = '' | ||||||||
|
|
||||||||
| try { | ||||||||
| const request = new Requests.CreateSubscriberListRequest( | ||||||||
| name, | ||||||||
| !!createForm.value.public, | ||||||||
| parsedPosition, | ||||||||
| createForm.value.description.trim() || null | ||||||||
| ) | ||||||||
|
|
||||||||
| const createdList = await listClient.createList(request) | ||||||||
| emit('created', createdList) | ||||||||
| emit('close') | ||||||||
| } catch (error) { | ||||||||
| createError.value = error?.message || 'Failed to create list.' | ||||||||
| } finally { | ||||||||
| creatingList.value = false | ||||||||
| } | ||||||||
| } | ||||||||
| </script> | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add an accessible name to the icon-only close button.
Like the edit modal, this dismiss button should expose an explicit label for assistive tech.
💡 Suggested fix
🤖 Prompt for AI Agents