diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e5f449..77bcc3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,8 @@ jobs: DB_USERNAME: root DB_PASSWORD: phplist BROADCAST_DRIVER: log + API_BASE_URL: http://api.phplist.local/ + REST_API_BASE_URL: http://api.phplist.local/api/v2 services: mysql: image: mysql:5.7 @@ -31,7 +33,20 @@ jobs: with: php-version: ${{ matrix.php-versions }} extensions: mbstring, dom, fileinfo, mysql - coverage: xdebug #optional + coverage: xdebug + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: yarn + + - name: Install JS dependencies + run: yarn install --frozen-lockfile + + - name: Build frontend assets + run: yarn encore dev + - name: Install Symfony CLI run: | curl -sS https://get.symfony.com/cli/installer | bash @@ -43,14 +58,11 @@ jobs: echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google.gpg] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list sudo apt-get update sudo apt-get install -y google-chrome-stable - - name: Set Panther to use Chrome - run: | - echo "PANTHER_NO_HEADLESS=0" >> .env.test - echo "PANTHER_CHROME_BINARY=/usr/bin/google-chrome" >> .env.test - - name: Start mysql service - run: sudo /etc/init.d/mysql start + sudo apt install socat + - name: Verify MySQL connection on host run: mysql --host 127.0.0.1 --port ${{ job.services.mysql.ports['3306'] }} -u${{ env.DB_USERNAME }} -p${{ env.DB_PASSWORD }} -e "SHOW DATABASES" + - name: Get composer cache directory id: composer-cache run: echo "::set-output name=dir::$(composer config cache-files-dir)" @@ -60,21 +72,82 @@ jobs: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-composer- - - name: Install the latest dependencies - run: composer install + - name: Install PHP dependencies + run: composer install --no-interaction --prefer-dist + - name: Set up database schema run: mysql --host 127.0.0.1 --port ${{ job.services.mysql.ports['3306'] }} -u${{ env.DB_USERNAME }} -p${{ env.DB_PASSWORD }} ${{ env.DB_DATABASE }} < vendor/phplist/core/resources/Database/Schema.sql - - name: Validating composer.json - run: composer validate --no-check-all --no-check-lock --strict; - - name: Linting all php files - run: find src/ tests/ public/ -name ''*.php'' -print0 | xargs -0 -n 1 -P 4 php -l; php -l; - - name: Running integration tests with phpunit - run: vendor/bin/phpunit tests/Integration/; - - name: Running the system tests - run: vendor/bin/phpunit tests/System/; - - name: Running static analysis - run: vendor/bin/phpstan analyse -l 5 src/ tests/; - - name: Running PHPMD - run: vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml; - - name: Running PHP_CodeSniffer - run: vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/; + - name: Validate composer.json + run: composer validate --no-check-all --no-check-lock --strict + - name: Lint PHP files + run: find src/ tests/ public/ -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l + + - name: Run static analysis + run: vendor/bin/phpstan analyse -l 5 src/ tests/ + - name: Run PHPMD + run: vendor/bin/phpmd src/ text vendor/phplist/core/config/PHPMD/rules.xml + - name: Run PHP_CodeSniffer + run: vendor/bin/phpcs --standard=vendor/phplist/core/config/PhpCodeSniffer/ src/ tests/ + + - name: Install Prism + run: npm install -g @stoplight/prism-cli + - name: Start Prism Mock Server + run: | + prism mock --host 127.0.0.1 --port 4010 ./openapi.json & + - name: Add local hostname + run: echo "127.0.0.1 api.phplist.local" | sudo tee -a /etc/hosts + - name: Proxy port 80 to 4010 + run: | + sudo socat -d -d TCP-LISTEN:80,reuseaddr,fork TCP:127.0.0.1:4010 & + - name: Wait for Prism and proxy + run: | + set -euo pipefail + prism_ready=0 + for i in $(seq 1 30); do + if curl -sS -o /tmp/prism-health-body.txt -w "%{http_code}" \ + -H 'Content-Type: application/json' \ + -X POST http://127.0.0.1:4010/api/v2/sessions \ + --data '{"login_name":"healthcheck","password":"healthcheck"}' > /tmp/prism-health-code.txt; then + code=$(cat /tmp/prism-health-code.txt) + if [ "$code" != "000" ]; then + echo "Prism is reachable on 127.0.0.1:4010 with HTTP ${code}" + prism_ready=1 + break + fi + fi + sleep 1 + done + if [ "$prism_ready" -ne 1 ]; then + echo "Prism did not become reachable in time." + exit 1 + fi + + proxy_ready=0 + for i in $(seq 1 30); do + if curl -sS -o /tmp/proxy-health-body.txt -w "%{http_code}" \ + -H 'Content-Type: application/json' \ + -X POST http://api.phplist.local/api/v2/sessions \ + --data '{"login_name":"healthcheck","password":"healthcheck"}' > /tmp/proxy-health-code.txt; then + code=$(cat /tmp/proxy-health-code.txt) + if [ "$code" != "000" ]; then + echo "Proxy is reachable on api.phplist.local with HTTP ${code}" + proxy_ready=1 + break + fi + fi + sleep 1 + done + if [ "$proxy_ready" -ne 1 ]; then + echo "Proxy did not become reachable in time." + exit 1 + fi + + - name: Run tests with phpunit + run: vendor/bin/phpunit tests + + - name: Upload Panther screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: panther-screenshots + path: var/screenshots diff --git a/README.md b/README.md index f7a88b2..185b65d 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,14 @@ Please install this package via Composer from within the which also has more detailed installation instructions in the README. +### When this module is installed as a dependency, publish bundle assets to the host application's `public/` directory: +```bash +php bin/console assets:install public --symlink --relative +``` + +This module serves its frontend files from `/`. + + ## Contributing to this package Please read the [contribution guide](.github/CONTRIBUTING.md) on how to diff --git a/apache/web-frontend.conf b/apache/web-frontend.conf new file mode 100644 index 0000000..775f35d --- /dev/null +++ b/apache/web-frontend.conf @@ -0,0 +1,26 @@ + + ServerName frontend.phplist.local + ServerAdmin webmaster@localhost + + DocumentRoot /{pathToTheProject}/web-frontend/public + + ErrorLog ${APACHE_LOG_DIR}/web-frontend-error.log + CustomLog ${APACHE_LOG_DIR}/web-frontend-access.log combined + + + Options FollowSymLinks + AllowOverride All + Require all granted + DirectoryIndex app.php + + + + Require all denied + + + + + SetHandler "proxy:unix:/run/php/php8.1-fpm.sock|fcgi://localhost" + + + diff --git a/assets/app.js b/assets/app.js index 0fdadbf..7d3fec5 100644 --- a/assets/app.js +++ b/assets/app.js @@ -1,9 +1,13 @@ +import './styles/app.css'; import { createApp } from 'vue'; import App from './vue/App.vue'; +import { router } from './router'; -// Mount the main app if the element exists const appElement = document.getElementById('vue-app'); + if (appElement) { - createApp(App).mount('#vue-app'); + const app = createApp(App); + app.use(router); + app.mount(appElement); } diff --git a/assets/images/avatar.jpg b/assets/images/avatar.jpg new file mode 100644 index 0000000..e051a3b Binary files /dev/null and b/assets/images/avatar.jpg differ diff --git a/assets/images/logo-48.png b/assets/images/logo-48.png new file mode 100644 index 0000000..215a9ef Binary files /dev/null and b/assets/images/logo-48.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..2f27bba Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/router/index.js b/assets/router/index.js new file mode 100644 index 0000000..e1bfe66 --- /dev/null +++ b/assets/router/index.js @@ -0,0 +1,32 @@ +import { createRouter, createWebHistory } from 'vue-router'; +import DashboardView from '../vue/views/DashboardView.vue' +import SubscribersView from '../vue/views/SubscribersView.vue' +import ListsView from '../vue/views/ListsView.vue' +import ListSubscribersView from '../vue/views/ListSubscribersView.vue' +import CampaignsView from '../vue/views/CampaignsView.vue' +import CampaignEditView from '../vue/views/CampaignEditView.vue' +import TemplatesView from '../vue/views/TemplatesView.vue' +import TemplateEditView from '../vue/views/TemplateEditView.vue' + +export const router = createRouter({ + history: createWebHistory(), + routes: [ + { path: '/', name: 'dashboard', component: DashboardView, meta: { title: 'Dashboard' } }, + { path: '/subscribers', name: 'subscribers', component: SubscribersView, meta: { title: 'Subscribers' } }, + { path: '/lists', name: 'lists', component: ListsView, meta: { title: 'Lists' } }, + { path: '/campaigns', name: 'campaigns', component: CampaignsView, meta: { title: 'Campaigns' } }, + { path: '/templates', name: 'templates', component: TemplatesView, meta: { title: 'Templates' } }, + { path: '/templates/create', name: 'template-create', component: TemplateEditView, meta: { title: 'Create Template' } }, + { path: '/templates/:templateId/edit', name: 'template-edit', component: TemplateEditView, meta: { title: 'Edit Template' } }, + { path: '/campaigns/create', name: 'campaign-create', component: CampaignEditView, meta: { title: 'Create Campaign' } }, + { path: '/campaigns/:campaignId/edit', name: 'campaign-edit', component: CampaignEditView, meta: { title: 'Edit Campaign' } }, + { path: '/lists/:listId/subscribers', name: 'list-subscribers', component: ListSubscribersView, meta: { title: 'List Subscribers' } }, + { path: '/:pathMatch(.*)*', redirect: '/' }, + ], +}); + +router.afterEach((to) => { + const defaultTitle = 'phpList'; + const pageTitle = to.meta.title; + document.title = pageTitle ? `${defaultTitle} - ${pageTitle}` : defaultTitle; +}); diff --git a/assets/styles/app.css b/assets/styles/app.css new file mode 100644 index 0000000..540b446 --- /dev/null +++ b/assets/styles/app.css @@ -0,0 +1,16 @@ +@import "tailwindcss"; +@tailwind utilities; + +@theme { + --color-primary: #2563eb; + --color-secondary: #6b7280; + --color-success: #16a34a; + --color-danger: #dc2626; + --color-info: #0891b2; + --color-ext-wf1: #543ff6; /** indigo-500 **/ + --color-ext-wf2: #eef2ff; /** indigo-50 **/ + --color-ext-wf3: #303F9F; /** indigo-700 **/ +} + +@source "../../templates/**/*.twig"; +@source "../**/*.vue"; diff --git a/assets/vue/App.vue b/assets/vue/App.vue index 8e47f0b..e38be10 100644 --- a/assets/vue/App.vue +++ b/assets/vue/App.vue @@ -1,26 +1,13 @@ - diff --git a/assets/vue/api.js b/assets/vue/api.js new file mode 100644 index 0000000..7cc9e77 --- /dev/null +++ b/assets/vue/api.js @@ -0,0 +1,58 @@ +import { + CampaignClient, + Client, + ListMessagesClient, + ListClient, + StatisticsClient, + SubscribersClient, + SubscriptionClient, + SubscriberAttributesClient, + TemplatesClient +} from '@tatevikgr/rest-api-client'; + +const appElement = document.getElementById('vue-app'); +const apiToken = appElement?.dataset.apiToken; +const apiBaseUrl = appElement?.dataset.apiBaseUrl; + +if (!apiBaseUrl) { + console.error('API Base URL is not configured.'); +} + +const client = new Client(apiBaseUrl || ''); + +if (apiToken) { + client.setSessionId(apiToken); +} + +export const subscribersClient = new SubscribersClient(client); +export const listClient = new ListClient(client); +export const campaignClient = new CampaignClient(client); +export const listMessagesClient = new ListMessagesClient(client); +export const statisticsClient = new StatisticsClient(client); +export const subscriptionClient = new SubscriptionClient(client); +export const subscriberAttributesClient = new SubscriberAttributesClient(client); +export const templateClient = new TemplatesClient(client); + +export const fetchAllLists = async ({ limit = 100, maxPages = 100 } = {}) => { + const lists = []; + let afterId = null; + + for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) { + const response = await listClient.getLists(afterId, limit); + const items = Array.isArray(response?.items) ? response.items : []; + lists.push(...items); + + const hasMore = response?.pagination?.hasMore === true; + const nextCursor = response?.pagination?.nextCursor; + + if (!hasMore || !Number.isFinite(nextCursor) || nextCursor === afterId) { + break; + } + + afterId = nextCursor; + } + + return lists; +}; + +export default client; diff --git a/assets/vue/components/base/BaseBadge.spec.js b/assets/vue/components/base/BaseBadge.spec.js new file mode 100644 index 0000000..df0507a --- /dev/null +++ b/assets/vue/components/base/BaseBadge.spec.js @@ -0,0 +1,33 @@ +import { mount } from '@vue/test-utils' +import BaseBadge from './BaseBadge.vue' + +describe('BaseBadge', () => { + it('renders neutral variant by default', () => { + const wrapper = mount(BaseBadge, { + slots: { + default: 'All', + }, + }) + + const classes = wrapper.get('span').classes() + expect(wrapper.text()).toContain('All') + expect(classes).toContain('bg-gray-100') + expect(classes).toContain('text-gray-800') + }) + + it('renders counter variant styles', () => { + const wrapper = mount(BaseBadge, { + props: { + variant: 'counter', + }, + slots: { + default: '10', + }, + }) + + const classes = wrapper.get('span').classes() + expect(classes).toContain('bg-indigo-50') + expect(classes).toContain('text-ext-wf3') + expect(wrapper.text()).toContain('10') + }) +}) diff --git a/assets/vue/components/base/BaseBadge.vue b/assets/vue/components/base/BaseBadge.vue new file mode 100644 index 0000000..217fd1b --- /dev/null +++ b/assets/vue/components/base/BaseBadge.vue @@ -0,0 +1,28 @@ + + + + diff --git a/assets/vue/components/base/BaseButton.spec.js b/assets/vue/components/base/BaseButton.spec.js new file mode 100644 index 0000000..5a0106b --- /dev/null +++ b/assets/vue/components/base/BaseButton.spec.js @@ -0,0 +1,47 @@ +import { mount } from '@vue/test-utils' +import BaseButton from './BaseButton.vue' + +describe('BaseButton', () => { + it('renders slot content', () => { + const wrapper = mount(BaseButton, { + slots: { + default: 'Save', + }, + }) + + expect(wrapper.text()).toContain('Save') + }) + + it('uses primary styles by default', () => { + const wrapper = mount(BaseButton) + const classes = wrapper.get('button').classes() + + expect(classes).toContain('text-white') + expect(classes).toContain('bg-blue-600') + }) + + it('uses secondary styles when variant is secondary', () => { + const wrapper = mount(BaseButton, { + props: { + variant: 'secondary', + }, + }) + + const classes = wrapper.get('button').classes() + expect(classes).toContain('text-gray-700') + expect(classes).toContain('bg-white') + }) + + it('forwards attributes to button', () => { + const wrapper = mount(BaseButton, { + attrs: { + disabled: true, + 'data-testid': 'submit-button', + }, + }) + + const button = wrapper.get('button') + expect(button.attributes('disabled')).toBeDefined() + expect(button.attributes('data-testid')).toBe('submit-button') + }) +}) diff --git a/assets/vue/components/base/BaseButton.vue b/assets/vue/components/base/BaseButton.vue new file mode 100644 index 0000000..aaeb2e8 --- /dev/null +++ b/assets/vue/components/base/BaseButton.vue @@ -0,0 +1,35 @@ + + + + + diff --git a/assets/vue/components/base/BaseCard.vue b/assets/vue/components/base/BaseCard.vue new file mode 100644 index 0000000..2278c74 --- /dev/null +++ b/assets/vue/components/base/BaseCard.vue @@ -0,0 +1,34 @@ + + + diff --git a/assets/vue/components/base/BaseIcon.spec.js b/assets/vue/components/base/BaseIcon.spec.js new file mode 100644 index 0000000..047d803 --- /dev/null +++ b/assets/vue/components/base/BaseIcon.spec.js @@ -0,0 +1,36 @@ +import { mount } from '@vue/test-utils' +import BaseIcon from './BaseIcon.vue' + +describe('BaseIcon', () => { + it('renders icon svg for known icon name', () => { + const wrapper = mount(BaseIcon, { + props: { + name: 'users', + }, + }) + + expect(wrapper.html()).toContain(' { + const wrapper = mount(BaseIcon, { + props: { + name: 'does-not-exist', + }, + }) + + expect(wrapper.html()).not.toContain(' { + const wrapper = mount(BaseIcon, { + props: { + name: 'users', + active: true, + }, + }) + + expect(wrapper.get('span').classes()).toContain('text-ext-wf3') + }) +}) diff --git a/assets/vue/components/base/BaseIcon.vue b/assets/vue/components/base/BaseIcon.vue new file mode 100644 index 0000000..2955106 --- /dev/null +++ b/assets/vue/components/base/BaseIcon.vue @@ -0,0 +1,118 @@ + + + diff --git a/assets/vue/components/base/BaseProgressBar.spec.js b/assets/vue/components/base/BaseProgressBar.spec.js new file mode 100644 index 0000000..bc4a6fd --- /dev/null +++ b/assets/vue/components/base/BaseProgressBar.spec.js @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils' +import BaseProgressBar from './BaseProgressBar.vue' + +describe('BaseProgressBar', () => { + it('applies default height and progress attributes', () => { + const wrapper = mount(BaseProgressBar, { + props: { + value: 35, + }, + }) + + const wrapperStyle = wrapper.get('.progress').attributes('style') + const bar = wrapper.get('[role="progressbar"]') + + expect(wrapperStyle).toContain('height: 6px;') + expect(bar.attributes('style')).toContain('width: 35%;') + expect(bar.attributes('aria-valuenow')).toBe('35') + expect(bar.attributes('aria-valuemin')).toBe('0') + expect(bar.attributes('aria-valuemax')).toBe('100') + }) + + it('uses custom height when provided', () => { + const wrapper = mount(BaseProgressBar, { + props: { + value: 80, + height: '10px', + }, + }) + + expect(wrapper.get('.progress').attributes('style')).toContain('height: 10px;') + }) +}) diff --git a/assets/vue/components/base/BaseProgressBar.vue b/assets/vue/components/base/BaseProgressBar.vue new file mode 100644 index 0000000..91ac7a7 --- /dev/null +++ b/assets/vue/components/base/BaseProgressBar.vue @@ -0,0 +1,30 @@ + + + + diff --git a/assets/vue/components/base/CkEditorField.vue b/assets/vue/components/base/CkEditorField.vue new file mode 100644 index 0000000..9a56ad6 --- /dev/null +++ b/assets/vue/components/base/CkEditorField.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/assets/vue/components/campaigns/CampaignDirectory.vue b/assets/vue/components/campaigns/CampaignDirectory.vue new file mode 100644 index 0000000..91bedec --- /dev/null +++ b/assets/vue/components/campaigns/CampaignDirectory.vue @@ -0,0 +1,974 @@ + + + diff --git a/assets/vue/components/campaigns/ViewCampaignModal.vue b/assets/vue/components/campaigns/ViewCampaignModal.vue new file mode 100644 index 0000000..f74cfb2 --- /dev/null +++ b/assets/vue/components/campaigns/ViewCampaignModal.vue @@ -0,0 +1,217 @@ + + + diff --git a/assets/vue/components/dashboard/CampaignsTable.vue b/assets/vue/components/dashboard/CampaignsTable.vue new file mode 100644 index 0000000..6688945 --- /dev/null +++ b/assets/vue/components/dashboard/CampaignsTable.vue @@ -0,0 +1,64 @@ + + + diff --git a/assets/vue/components/dashboard/KpiCard.vue b/assets/vue/components/dashboard/KpiCard.vue new file mode 100644 index 0000000..57df775 --- /dev/null +++ b/assets/vue/components/dashboard/KpiCard.vue @@ -0,0 +1,39 @@ + + + + diff --git a/assets/vue/components/dashboard/KpiGrid.vue b/assets/vue/components/dashboard/KpiGrid.vue new file mode 100644 index 0000000..4835163 --- /dev/null +++ b/assets/vue/components/dashboard/KpiGrid.vue @@ -0,0 +1,79 @@ + + + diff --git a/assets/vue/components/dashboard/PerformanceChartCard.vue b/assets/vue/components/dashboard/PerformanceChartCard.vue new file mode 100644 index 0000000..4dd02a2 --- /dev/null +++ b/assets/vue/components/dashboard/PerformanceChartCard.vue @@ -0,0 +1,128 @@ + + + + diff --git a/assets/vue/components/dashboard/QuickActionsCard.vue b/assets/vue/components/dashboard/QuickActionsCard.vue new file mode 100644 index 0000000..ba403f4 --- /dev/null +++ b/assets/vue/components/dashboard/QuickActionsCard.vue @@ -0,0 +1,89 @@ + + + diff --git a/assets/vue/components/dashboard/RecentCampaignsCard.vue b/assets/vue/components/dashboard/RecentCampaignsCard.vue new file mode 100644 index 0000000..0f6f010 --- /dev/null +++ b/assets/vue/components/dashboard/RecentCampaignsCard.vue @@ -0,0 +1,26 @@ + + + + diff --git a/assets/vue/components/lists/AddSubscribersModal.vue b/assets/vue/components/lists/AddSubscribersModal.vue new file mode 100644 index 0000000..b54f3f8 --- /dev/null +++ b/assets/vue/components/lists/AddSubscribersModal.vue @@ -0,0 +1,165 @@ + + + diff --git a/assets/vue/components/lists/CreateListModal.vue b/assets/vue/components/lists/CreateListModal.vue new file mode 100644 index 0000000..dcd58ca --- /dev/null +++ b/assets/vue/components/lists/CreateListModal.vue @@ -0,0 +1,177 @@ + + + diff --git a/assets/vue/components/lists/EditListModal.vue b/assets/vue/components/lists/EditListModal.vue new file mode 100644 index 0000000..41d5981 --- /dev/null +++ b/assets/vue/components/lists/EditListModal.vue @@ -0,0 +1,230 @@ + + + diff --git a/assets/vue/components/lists/ListDirectory.vue b/assets/vue/components/lists/ListDirectory.vue new file mode 100644 index 0000000..082f47b --- /dev/null +++ b/assets/vue/components/lists/ListDirectory.vue @@ -0,0 +1,349 @@ + + + diff --git a/assets/vue/components/lists/ListSubscribersExportPanel.vue b/assets/vue/components/lists/ListSubscribersExportPanel.vue new file mode 100644 index 0000000..bcc03ee --- /dev/null +++ b/assets/vue/components/lists/ListSubscribersExportPanel.vue @@ -0,0 +1,284 @@ + + + diff --git a/assets/vue/components/sidebar/AppSidebar.vue b/assets/vue/components/sidebar/AppSidebar.vue new file mode 100644 index 0000000..ca607e5 --- /dev/null +++ b/assets/vue/components/sidebar/AppSidebar.vue @@ -0,0 +1,81 @@ + + + diff --git a/assets/vue/components/sidebar/SidebarLogo.vue b/assets/vue/components/sidebar/SidebarLogo.vue new file mode 100644 index 0000000..ef1a1f9 --- /dev/null +++ b/assets/vue/components/sidebar/SidebarLogo.vue @@ -0,0 +1,18 @@ + + + diff --git a/assets/vue/components/sidebar/SidebarNavItem.vue b/assets/vue/components/sidebar/SidebarNavItem.vue new file mode 100644 index 0000000..3f26473 --- /dev/null +++ b/assets/vue/components/sidebar/SidebarNavItem.vue @@ -0,0 +1,49 @@ + + + + diff --git a/assets/vue/components/sidebar/SidebarNavSection.vue b/assets/vue/components/sidebar/SidebarNavSection.vue new file mode 100644 index 0000000..e9497f4 --- /dev/null +++ b/assets/vue/components/sidebar/SidebarNavSection.vue @@ -0,0 +1,20 @@ + + + diff --git a/assets/vue/components/subscribers/ImportResult.vue b/assets/vue/components/subscribers/ImportResult.vue new file mode 100644 index 0000000..f3eb82f --- /dev/null +++ b/assets/vue/components/subscribers/ImportResult.vue @@ -0,0 +1,83 @@ + + + diff --git a/assets/vue/components/subscribers/SubscriberDirectory.vue b/assets/vue/components/subscribers/SubscriberDirectory.vue new file mode 100644 index 0000000..a53da9e --- /dev/null +++ b/assets/vue/components/subscribers/SubscriberDirectory.vue @@ -0,0 +1,309 @@ + + + diff --git a/assets/vue/components/subscribers/SubscriberFilters.vue b/assets/vue/components/subscribers/SubscriberFilters.vue new file mode 100644 index 0000000..b29293c --- /dev/null +++ b/assets/vue/components/subscribers/SubscriberFilters.vue @@ -0,0 +1,38 @@ + + + diff --git a/assets/vue/components/subscribers/SubscriberModal.vue b/assets/vue/components/subscribers/SubscriberModal.vue new file mode 100644 index 0000000..a1745a3 --- /dev/null +++ b/assets/vue/components/subscribers/SubscriberModal.vue @@ -0,0 +1,205 @@ + + + diff --git a/assets/vue/components/subscribers/SubscriberTable.vue b/assets/vue/components/subscribers/SubscriberTable.vue new file mode 100644 index 0000000..b4919c4 --- /dev/null +++ b/assets/vue/components/subscribers/SubscriberTable.vue @@ -0,0 +1,144 @@ + + + diff --git a/assets/vue/components/subscribers/subscriberFilters.js b/assets/vue/components/subscribers/subscriberFilters.js new file mode 100644 index 0000000..d0b8ee4 --- /dev/null +++ b/assets/vue/components/subscribers/subscriberFilters.js @@ -0,0 +1,7 @@ +export const subscriberFilters = [ + { id: 'all', label: 'All' }, + { id: 'unconfirmed', label: 'Unconfirmed' }, + { id: 'blacklisted', label: 'Blacklisted' }, + { id: 'confirmed', label: 'Confirmed' }, + { id: 'non-blacklisted', label: 'Non-Blacklisted' } +] diff --git a/assets/vue/components/templates/TemplateLibrary.vue b/assets/vue/components/templates/TemplateLibrary.vue new file mode 100644 index 0000000..7f623e3 --- /dev/null +++ b/assets/vue/components/templates/TemplateLibrary.vue @@ -0,0 +1,333 @@ + + + diff --git a/assets/vue/composables/useSidebar.js b/assets/vue/composables/useSidebar.js new file mode 100644 index 0000000..2c1dad7 --- /dev/null +++ b/assets/vue/composables/useSidebar.js @@ -0,0 +1,24 @@ +import { ref } from 'vue' + +const isSidebarOpen = ref(false) + +export function useSidebar() { + const toggleSidebar = () => { + isSidebarOpen.value = !isSidebarOpen.value + } + + const closeSidebar = () => { + isSidebarOpen.value = false + } + + const openSidebar = () => { + isSidebarOpen.value = true + } + + return { + isSidebarOpen, + toggleSidebar, + closeSidebar, + openSidebar, + } +} diff --git a/assets/vue/layouts/AdminLayout.vue b/assets/vue/layouts/AdminLayout.vue new file mode 100644 index 0000000..f1d8aa1 --- /dev/null +++ b/assets/vue/layouts/AdminLayout.vue @@ -0,0 +1,227 @@ + + + + diff --git a/assets/vue/views/CampaignEditView.vue b/assets/vue/views/CampaignEditView.vue new file mode 100644 index 0000000..a12affc --- /dev/null +++ b/assets/vue/views/CampaignEditView.vue @@ -0,0 +1,1029 @@ + + + diff --git a/assets/vue/views/CampaignsView.vue b/assets/vue/views/CampaignsView.vue new file mode 100644 index 0000000..1f78a63 --- /dev/null +++ b/assets/vue/views/CampaignsView.vue @@ -0,0 +1,12 @@ + + + diff --git a/assets/vue/views/DashboardView.vue b/assets/vue/views/DashboardView.vue new file mode 100644 index 0000000..16dff3d --- /dev/null +++ b/assets/vue/views/DashboardView.vue @@ -0,0 +1,63 @@ + + + diff --git a/assets/vue/views/ListSubscribersView.vue b/assets/vue/views/ListSubscribersView.vue new file mode 100644 index 0000000..762483f --- /dev/null +++ b/assets/vue/views/ListSubscribersView.vue @@ -0,0 +1,568 @@ + + + diff --git a/assets/vue/views/ListsView.vue b/assets/vue/views/ListsView.vue new file mode 100644 index 0000000..dd66bc4 --- /dev/null +++ b/assets/vue/views/ListsView.vue @@ -0,0 +1,12 @@ + + + diff --git a/assets/vue/views/SubscribersView.vue b/assets/vue/views/SubscribersView.vue new file mode 100644 index 0000000..5a8fdea --- /dev/null +++ b/assets/vue/views/SubscribersView.vue @@ -0,0 +1,12 @@ + + + diff --git a/assets/vue/views/TemplateEditView.vue b/assets/vue/views/TemplateEditView.vue new file mode 100644 index 0000000..7e2b1ba --- /dev/null +++ b/assets/vue/views/TemplateEditView.vue @@ -0,0 +1,308 @@ +