├── .github └── workflows │ └── main.yml ├── .gitignore ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── art ├── Laravel Smart Ads.png ├── ad-auto-placement.png ├── smart-ads-create-new.png └── smart-ads-dashboard.png ├── composer.json ├── config └── smart-ads.php ├── database ├── factories │ ├── SmartAdFactory.php │ └── SmartAdTrackingFactory.php └── migrations │ ├── create_smart_ads_table.php.stub │ └── create_smart_ads_tracking_table.php.stub ├── main.js ├── mix-manifest.json ├── package-lock.json ├── package.json ├── phpunit.xml.dist ├── resources ├── assets │ ├── css │ │ └── app.css │ └── js │ │ ├── app.js │ │ ├── init-alpine.js │ │ └── prism.js ├── dist │ ├── css │ │ ├── app.css │ │ └── prism.css │ ├── js │ │ ├── banner-manager.js │ │ ├── smart-banner.js │ │ └── smart-banner.min.js │ └── mix-manifest.json └── views │ ├── components │ └── smart-ad-component.blade.php │ ├── layouts │ ├── app.blade.php │ └── partials │ │ ├── desktop-sidebar.blade.php │ │ ├── header.blade.php │ │ ├── mobile-sidebar.blade.php │ │ └── nav.blade.php │ ├── livewire │ └── ad-report-component.blade.php │ └── smart-ad-manager │ ├── create.blade.php │ ├── dashboard.blade.php │ ├── edit.blade.php │ ├── index.blade.php │ └── show.blade.php ├── routes └── web.php ├── src ├── Components │ └── SmartAdComponent.php ├── Http │ ├── Controllers │ │ └── SmartAdManagerController.php │ ├── Livewire │ │ └── AdReportComponent.php │ └── Requests │ │ └── StoreSmartAdRequest.php ├── LaravelSmartAds.php ├── LaravelSmartAdsFacade.php ├── LaravelSmartAdsServiceProvider.php └── Models │ ├── SmartAd.php │ └── SmartAdTracking.php ├── tailwind.config.js ├── tests ├── CreateSmartAdTest.php ├── LaravelSmartAdsTest.php ├── LaravelSmartAdsTestCase.php ├── SmartAdAdminTest.php ├── SmartAdComponentTest.php ├── SmartAdDashboardTest.php └── SmartAdTrackingTest.php └── webpack.mix.js /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | types: [ready_for_review, synchronize, opened] 10 | 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | php: [8.0, 8.1] 20 | laravel: [9.*] 21 | 22 | name: PHP:${{ matrix.php }} / Laravel:${{ matrix.laravel }} 23 | 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | 28 | - name: Setup PHP, with composer and extensions 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.php }} 32 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 33 | tools: composer:v2 34 | coverage: none 35 | 36 | - name: Get composer cache directory 37 | id: composer-cache 38 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 39 | 40 | - name: Cache composer dependencies 41 | uses: actions/cache@v2 42 | with: 43 | path: ${{ steps.composer-cache.outputs.dir }} 44 | key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 45 | restore-keys: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer- 46 | 47 | - name: Install Composer dependencies 48 | run: | 49 | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update 50 | composer update --no-interaction --no-suggest 51 | - name: Run PHPUnit tests 52 | run: vendor/bin/phpunit 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .phpunit.result.cache 3 | build 4 | composer.lock 5 | coverage 6 | docs 7 | phpunit.xml 8 | phpstan.neon 9 | testbench.yaml 10 | vendor 11 | node_modules -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-smart-ads` will be documented in this file 4 | 5 | ## 1.0.0 - 201X-XX-XX 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tushar Gugnani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ad Manager for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/5balloons/laravel-smart-ads.svg?style=flat-square)](https://packagist.org/packages/5balloons/laravel-smart-ads) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/5balloons/laravel-smart-ads.svg?style=flat-square)](https://packagist.org/packages/5balloons/laravel-smart-ads) 5 | ![GitHub Actions](https://github.com/5balloons/laravel-smart-ads/actions/workflows/main.yml/badge.svg) 6 | 7 | ![alt text](/art/Laravel%20Smart%20Ads.png?raw=true "Larvel Smart Ads Dashboard") 8 | 9 | Simple Ad, Banner, Callouts Manager for Laravel. 10 | 11 | ## Video Demo 12 | 13 | https://www.youtube.com/watch?v=Hy_qZTljupQ 14 | 15 | 16 | ## Installation 17 | 18 | You can install the package via composer: 19 | 20 | ```bash 21 | composer require 5balloons/laravel-smart-ads 22 | ``` 23 | 24 | The package will automatically register itself. 25 | 26 | Publishing Migrations (Required) 27 | 28 | ```bash 29 | php artisan vendor:publish --provider="_5balloons\LaravelSmartAds\LaravelSmartAdsServiceProvider" --tag="smart-ads-migrations" 30 | ``` 31 | 32 | and then run migrate command to run the migrations 33 | 34 | ```bash 35 | php artisan migrate 36 | ``` 37 | 38 | Publishing Assets (Required) 39 | 40 | ```bash 41 | php artisan vendor:publish --provider="_5balloons\LaravelSmartAds\LaravelSmartAdsServiceProvider" --tag="smart-ads-assets" 42 | ``` 43 | This command will copy the necessary css and js files required to run the ad manager dashboard. 44 | 45 | Publishing Config File (Optional) 46 | 47 | ```bash 48 | php artisan vendor:publish --provider="_5balloons\LaravelSmartAds\LaravelSmartAdsServiceProvider" --tag="smart-ads-config" 49 | ``` 50 | 51 | ## Usage 52 | 53 | The ad manager dashboard can now be accessed at `/smart-ad-manager` 54 | 55 | ![alt text](https://raw.githubusercontent.com/5balloons/laravel-smart-ads/main/art/smart-ads-dashboard.png) 56 | 57 | ### Creating Ads 58 | 59 | You can create a new ad by navigating to `smart-ad-manager/ads/create` page and then providing a valid name and HTML body of the Ad. 60 | 61 | ![alt text](https://raw.githubusercontent.com/5balloons/laravel-smart-ads/main/art/smart-ads-create-new.png) 62 | 63 | 64 | ### Placing Ads 65 | 66 | In order to place the ads and track clicks you must place the following JS in your header (typically this would go in your blade layout file) 67 | 68 | ```html 69 | 70 | ``` 71 | 72 | There are two ways in which you can place ads / banners in your application 73 | 74 | #### (Manual Placement) 75 | To manually place an ad you can copy the blade component code from the view ad page and place it at desired location in your blade file. For example an ad with the slug of your-example-ad can be placed with the following code. 76 | 77 | ```html 78 | 79 | ``` 80 | 81 | #### (Auto Placement) 82 | 83 | You can choose to auto place at the ads at the desired locations on the website by providing the CSS selector where you are looking to place the ad and choosing the position (Before selector, After selector, inside selector etc. to place the ad) 84 | 85 | ![alt text](https://raw.githubusercontent.com/5balloons/laravel-smart-ads/main/art/ad-auto-placement.png) 86 | 87 | 88 | ### Tracking Clicks 89 | 90 | Tracking clicks is enabled by default and in order for it to work you must include a global meta csrf token in your blade template file, inside the head element of your HTML. 91 | 92 | ```html 93 | 94 | ``` 95 | 96 | 97 | ### Testing 98 | 99 | ```bash 100 | composer test 101 | ``` 102 | 103 | ### Changelog 104 | 105 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 106 | 107 | ## Contributing 108 | 109 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 110 | 111 | ### Security 112 | 113 | If you discover any security related issues, please email tushar@5balloons.info instead of using the issue tracker. 114 | 115 | ## Credits 116 | 117 | - [Tushar Gugnani](https://github.com/tushargugnani) 118 | - [All Contributors](../../contributors) 119 | 120 | ## License 121 | 122 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 123 | 124 | -------------------------------------------------------------------------------- /art/Laravel Smart Ads.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5balloons/laravel-smart-ads/d3c66a922ae40521b07d9010f29f0adc02a6e549/art/Laravel Smart Ads.png -------------------------------------------------------------------------------- /art/ad-auto-placement.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5balloons/laravel-smart-ads/d3c66a922ae40521b07d9010f29f0adc02a6e549/art/ad-auto-placement.png -------------------------------------------------------------------------------- /art/smart-ads-create-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5balloons/laravel-smart-ads/d3c66a922ae40521b07d9010f29f0adc02a6e549/art/smart-ads-create-new.png -------------------------------------------------------------------------------- /art/smart-ads-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5balloons/laravel-smart-ads/d3c66a922ae40521b07d9010f29f0adc02a6e549/art/smart-ads-dashboard.png -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "5balloons/laravel-smart-ads", 3 | "description": "Ad, Banners, Callouts Manager for Laravel", 4 | "keywords": [ 5 | "5balloons", 6 | "laravel-smart-ads" 7 | ], 8 | "homepage": "https://github.com/5balloons/laravel-smart-ads", 9 | "license": "MIT", 10 | "type": "library", 11 | "authors": [ 12 | { 13 | "name": "Tushar Gugnani", 14 | "email": "tushar@5balloons.info", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1|^8.2", 20 | "livewire/livewire": "^3.0", 21 | "spatie/laravel-package-tools": "^1.0.0" 22 | }, 23 | "require-dev": { 24 | "orchestra/testbench": "^7.6", 25 | "phpunit/phpunit": "^9.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "_5balloons\\LaravelSmartAds\\": "src", 30 | "_5balloons\\LaravelSmartAds\\Database\\Factories\\": "database/factories" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "_5balloons\\LaravelSmartAds\\Tests\\": "tests" 36 | } 37 | }, 38 | "scripts": { 39 | "test": "vendor/bin/phpunit", 40 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 41 | 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "_5balloons\\LaravelSmartAds\\LaravelSmartAdsServiceProvider" 50 | ], 51 | "aliases": { 52 | "LaravelSmartAds": "_5balloons\\LaravelSmartAds\\LaravelSmartAdsFacade" 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /config/smart-ads.php: -------------------------------------------------------------------------------- 1 | [ 8 | 'prefix' => '', 9 | 'middleware' => ['web'], 10 | ] 11 | ]; -------------------------------------------------------------------------------- /database/factories/SmartAdFactory.php: -------------------------------------------------------------------------------- 1 | word.' '.fake()->word; 15 | return [ 16 | 'name' => $adname, 17 | 'body' => fake()->randomHtml(), 18 | 'adType' => 'HTML', 19 | 'slug' => implode('-', explode(' ', $adname)) 20 | ]; 21 | } 22 | } -------------------------------------------------------------------------------- /database/factories/SmartAdTrackingFactory.php: -------------------------------------------------------------------------------- 1 | json_encode(['test' => "6"]), 17 | 'totalClicks' => 5, 18 | ]; 19 | } 20 | } -------------------------------------------------------------------------------- /database/migrations/create_smart_ads_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('name')->unique(); 19 | $table->string('slug'); 20 | $table->text('body')->nullable(); 21 | $table->string('adType'); 22 | $table->string('image')->nullable(); 23 | $table->string('imageUrl')->nullable(); 24 | $table->string('imageAlt')->nullable(); 25 | $table->integer('views')->default(0); 26 | $table->integer('clicks')->default(0); 27 | $table->boolean('enabled')->default(1); 28 | $table->json('placements')->nullable(); 29 | $table->timestamps(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists('smart_ads'); 41 | } 42 | } -------------------------------------------------------------------------------- /database/migrations/create_smart_ads_tracking_table.php.stub: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->json('ad_clicks'); 19 | $table->integer('totalClicks'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('smart_ads_tracking'); 32 | } 33 | } -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /******/ (() => { // webpackBootstrap 2 | /******/ "use strict"; 3 | /******/ 4 | /******/ 5 | /******/ })() 6 | ; -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/resources/dist/css/app.css": "/resources/dist/css/app.css", 3 | "/resources/dist/js/banner-manager.js": "/resources/dist/js/banner-manager.js", 4 | "/resources/dist/js/smart-banner.min.js": "/resources/dist/js/smart-banner.min.js" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-smart-ads", 3 | "version": "1.0.0", 4 | "description": "[![Latest Version on Packagist](https://img.shields.io/packagist/v/5balloons/laravel-smart-ads.svg?style=flat-square)](https://packagist.org/packages/5balloons/laravel-smart-ads) [![Total Downloads](https://img.shields.io/packagist/dt/5balloons/laravel-smart-ads.svg?style=flat-square)](https://packagist.org/packages/5balloons/laravel-smart-ads) ![GitHub Actions](https://github.com/5balloons/laravel-smart-ads/actions/workflows/main.yml/badge.svg)", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/5balloons/laravel-smart-ads.git" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/5balloons/laravel-smart-ads/issues" 21 | }, 22 | "homepage": "https://github.com/5balloons/laravel-smart-ads#readme", 23 | "devDependencies": { 24 | "@tailwindcss/forms": "^0.5.3", 25 | "laravel-mix": "^6.0.49", 26 | "mix-tailwindcss": "^1.3.0", 27 | "resolve-url-loader": "^5.0.0", 28 | "sass": "^1.57.1", 29 | "sass-loader": "^12.6.0", 30 | "tailwindcss": "^3.2.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | tests 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /resources/assets/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /resources/assets/js/app.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5balloons/laravel-smart-ads/d3c66a922ae40521b07d9010f29f0adc02a6e549/resources/assets/js/app.js -------------------------------------------------------------------------------- /resources/assets/js/init-alpine.js: -------------------------------------------------------------------------------- 1 | function data() { 2 | function getThemeFromLocalStorage() { 3 | // if user already changed the theme, use it 4 | if (window.localStorage.getItem('dark')) { 5 | return JSON.parse(window.localStorage.getItem('dark')) 6 | } 7 | 8 | // else return their preferences 9 | return ( 10 | !!window.matchMedia && 11 | window.matchMedia('(prefers-color-scheme: dark)').matches 12 | ) 13 | } 14 | 15 | function setThemeToLocalStorage(value) { 16 | window.localStorage.setItem('dark', value) 17 | } 18 | 19 | return { 20 | dark: getThemeFromLocalStorage(), 21 | toggleTheme() { 22 | this.dark = !this.dark 23 | setThemeToLocalStorage(this.dark) 24 | }, 25 | isSideMenuOpen: false, 26 | toggleSideMenu() { 27 | this.isSideMenuOpen = !this.isSideMenuOpen 28 | }, 29 | closeSideMenu() { 30 | this.isSideMenuOpen = false 31 | }, 32 | isNotificationsMenuOpen: false, 33 | toggleNotificationsMenu() { 34 | this.isNotificationsMenuOpen = !this.isNotificationsMenuOpen 35 | }, 36 | closeNotificationsMenu() { 37 | this.isNotificationsMenuOpen = false 38 | }, 39 | isProfileMenuOpen: false, 40 | toggleProfileMenu() { 41 | this.isProfileMenuOpen = !this.isProfileMenuOpen 42 | }, 43 | closeProfileMenu() { 44 | this.isProfileMenuOpen = false 45 | }, 46 | isPagesMenuOpen: false, 47 | togglePagesMenu() { 48 | this.isPagesMenuOpen = !this.isPagesMenuOpen 49 | }, 50 | // Modal 51 | isModalOpen: false, 52 | trapCleanup: null, 53 | openModal() { 54 | this.isModalOpen = true 55 | this.trapCleanup = focusTrap(document.querySelector('#modal')) 56 | }, 57 | closeModal() { 58 | this.isModalOpen = false 59 | this.trapCleanup() 60 | }, 61 | } 62 | } -------------------------------------------------------------------------------- /resources/assets/js/prism.js: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript&plugins=line-numbers+normalize-whitespace */ 3 | var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(jg.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a"+i.content+""},!e.document)return e.addEventListener?(a.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),r=t.language,i=t.code,l=t.immediateClose;e.postMessage(a.highlight(i,a.languages[r],r)),l&&e.close()}),!1),a):a;var g=a.util.currentScript();function f(){a.manual||a.highlightAll()}if(g&&(a.filename=g.src,g.hasAttribute("data-manual")&&(a.manual=!0)),!a.manual){var h=document.readyState;"loading"===h||"interactive"===h&&g&&g.defer?document.addEventListener("DOMContentLoaded",f):window.requestAnimationFrame?window.requestAnimationFrame(f):window.setTimeout(f,16)}return a}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); 4 | Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^$/i;var t={"included-cdata":{pattern://i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; 5 | !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); 6 | Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; 7 | Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; 8 | !function(){if("undefined"!=typeof Prism&&"undefined"!=typeof document){var e="line-numbers",n=/\n(?!$)/g,t=Prism.plugins.lineNumbers={getLine:function(n,t){if("PRE"===n.tagName&&n.classList.contains(e)){var i=n.querySelector(".line-numbers-rows");if(i){var r=parseInt(n.getAttribute("data-start"),10)||1,s=r+(i.children.length-1);ts&&(t=s);var l=t-r;return i.children[l]}}},resize:function(e){r([e])},assumeViewportIndependence:!0},i=void 0;window.addEventListener("resize",(function(){t.assumeViewportIndependence&&i===window.innerWidth||(i=window.innerWidth,r(Array.prototype.slice.call(document.querySelectorAll("pre.line-numbers"))))})),Prism.hooks.add("complete",(function(t){if(t.code){var i=t.element,s=i.parentNode;if(s&&/pre/i.test(s.nodeName)&&!i.querySelector(".line-numbers-rows")&&Prism.util.isActive(i,e)){i.classList.remove(e),s.classList.add(e);var l,o=t.code.match(n),a=o?o.length+1:1,u=new Array(a+1).join("");(l=document.createElement("span")).setAttribute("aria-hidden","true"),l.className="line-numbers-rows",l.innerHTML=u,s.hasAttribute("data-start")&&(s.style.counterReset="linenumber "+(parseInt(s.getAttribute("data-start"),10)-1)),t.element.appendChild(l),r([s]),Prism.hooks.run("line-numbers",t)}}})),Prism.hooks.add("line-numbers",(function(e){e.plugins=e.plugins||{},e.plugins.lineNumbers=!0}))}function r(e){if(0!=(e=e.filter((function(e){var n,t=(n=e,n?window.getComputedStyle?getComputedStyle(n):n.currentStyle||null:null)["white-space"];return"pre-wrap"===t||"pre-line"===t}))).length){var t=e.map((function(e){var t=e.querySelector("code"),i=e.querySelector(".line-numbers-rows");if(t&&i){var r=e.querySelector(".line-numbers-sizer"),s=t.textContent.split(n);r||((r=document.createElement("span")).className="line-numbers-sizer",t.appendChild(r)),r.innerHTML="0",r.style.display="block";var l=r.getBoundingClientRect().height;return r.innerHTML="",{element:e,lines:s,lineHeights:[],oneLinerHeight:l,sizer:r}}})).filter(Boolean);t.forEach((function(e){var n=e.sizer,t=e.lines,i=e.lineHeights,r=e.oneLinerHeight;i[t.length-1]=void 0,t.forEach((function(e,t){if(e&&e.length>1){var s=n.appendChild(document.createElement("span"));s.style.display="block",s.textContent=e}else i[t]=r}))})),t.forEach((function(e){for(var n=e.sizer,t=e.lineHeights,i=0,r=0;rt&&(o[l]="\n"+o[l],a=s)}n[i]=o.join("")}return n.join("\n")}},"undefined"!=typeof module&&module.exports&&(module.exports=n),Prism.plugins.NormalizeWhitespace=new n({"remove-trailing":!0,"remove-indent":!0,"left-trim":!0,"right-trim":!0}),Prism.hooks.add("before-sanity-check",(function(e){var n=Prism.plugins.NormalizeWhitespace;if((!e.settings||!1!==e.settings["whitespace-normalization"])&&Prism.util.isActive(e.element,"whitespace-normalization",!0))if(e.element&&e.element.parentNode||!e.code){var r=e.element.parentNode;if(e.code&&r&&"pre"===r.nodeName.toLowerCase()){for(var i in null==e.settings&&(e.settings={}),t)if(Object.hasOwnProperty.call(t,i)){var o=t[i];if(r.hasAttribute("data-"+i))try{var a=JSON.parse(r.getAttribute("data-"+i)||"true");typeof a===o&&(e.settings[i]=a)}catch(e){}}for(var l=r.childNodes,s="",c="",u=!1,m=0;m:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(.5rem*var(--tw-space-x-reverse))}.space-x-6>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-left:calc(1.5rem*(1 - var(--tw-space-x-reverse)));margin-right:calc(1.5rem*var(--tw-space-x-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-300>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(209 213 219/var(--tw-divide-opacity))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-full{border-radius:9999px}.rounded-l-md{border-bottom-left-radius:.375rem;border-top-left-radius:.375rem}.rounded-r-md{border-bottom-right-radius:.375rem;border-top-right-radius:.375rem}.rounded-tr-lg{border-top-right-radius:.5rem}.rounded-br-lg{border-bottom-right-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-solid{border-style:solid}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-gray-400{--tw-border-opacity:1;border-color:rgb(156 163 175/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(220 252 231/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(254 226 226/var(--tw-bg-opacity))}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-indigo-300{--tw-bg-opacity:1;background-color:rgb(165 180 252/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.bg-clip-padding{background-clip:padding-box}.p-5{padding:1.25rem}.p-3{padding:.75rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-1{padding:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3\.5{padding-bottom:.875rem;padding-top:.875rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1\.5{padding-bottom:.375rem;padding-top:.375rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.pl-4{padding-left:1rem}.pr-3{padding-right:.75rem}.text-left{text-align:left}.align-middle{vertical-align:middle}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-xs{font-size:.75rem;line-height:1rem}.text-base{font-size:1rem;line-height:1.5rem}.font-semibold{font-weight:600}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.tracking-wide{letter-spacing:.025em}.text-purple-700{--tw-text-opacity:1;color:rgb(126 34 206/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(220 38 38/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(30 64 175/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity))}.text-red-700{--tw-text-opacity:1;color:rgb(185 28 28/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-purple-600{--tw-text-opacity:1;color:rgb(147 51 234/var(--tw-text-opacity))}.opacity-100{opacity:1}.opacity-0{opacity:0}.shadow{--tw-shadow:0 1px 3px 0 rgba(0,0,0,.1),0 1px 2px -1px rgba(0,0,0,.1);--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-1{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-black{--tw-ring-opacity:1;--tw-ring-color:rgb(0 0 0/var(--tw-ring-opacity))}.ring-gray-300{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.ring-opacity-5{--tw-ring-opacity:0.05}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-300{transition-duration:.3s}.duration-150{transition-duration:.15s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.hover\:bg-purple-700:hover{--tw-bg-opacity:1;background-color:rgb(126 34 206/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.hover\:text-gray-400:hover{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.focus\:z-10:focus{z-index:10}.focus\:border:focus{border-width:1px}.focus\:border-purple-400:focus{--tw-border-opacity:1;border-color:rgb(192 132 252/var(--tw-border-opacity))}.focus\:border-blue-600:focus{--tw-border-opacity:1;border-color:rgb(37 99 235/var(--tw-border-opacity))}.focus\:border-blue-200:focus{--tw-border-opacity:1;border-color:rgb(191 219 254/var(--tw-border-opacity))}.focus\:border-blue-300:focus{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity))}.focus\:bg-white:focus{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.focus\:text-gray-700:focus{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-purple-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(168 85 247/var(--tw-ring-opacity))}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}.active\:bg-purple-600:active{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity))}.active\:bg-gray-100:active{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.active\:text-gray-700:active{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.active\:text-gray-500:active{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.dark .dark\:divide-gray-700>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(55 65 81/var(--tw-divide-opacity))}.dark .dark\:border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}.dark .dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}.dark .dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.dark .dark\:bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}.dark .dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark .dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.dark .dark\:text-purple-800{--tw-text-opacity:1;color:rgb(107 33 168/var(--tw-text-opacity))}.dark .dark\:text-gray-100{--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}.dark .dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.dark .dark\:text-gray-200{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}.dark .dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.dark .dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}.dark .dark\:text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity))}.dark .dark\:text-purple-300{--tw-text-opacity:1;color:rgb(216 180 254/var(--tw-text-opacity))}.dark .dark\:hover\:bg-gray-800:hover{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.dark .dark\:hover\:text-gray-200:hover{--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}@media (min-width:640px){.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:flex{display:flex}.sm\:hidden{display:none}.sm\:flex-auto{flex:1 1 auto}.sm\:flex-1{flex:1 1 0%}.sm\:grid-cols-9{grid-template-columns:repeat(9,minmax(0,1fr))}.sm\:items-center{align-items:center}.sm\:justify-center{justify-content:center}.sm\:justify-between{justify-content:space-between}.sm\:px-6{padding-right:1.5rem}.sm\:pl-6,.sm\:px-6{padding-left:1.5rem}}@media (min-width:768px){.md\:block{display:block}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:rounded-lg{border-radius:.5rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1024px){.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} 2 | -------------------------------------------------------------------------------- /resources/dist/css/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.29.0 2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript&plugins=line-numbers+normalize-whitespace */ 3 | code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help} 4 | pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right} 5 | -------------------------------------------------------------------------------- /resources/dist/js/banner-manager.js: -------------------------------------------------------------------------------- 1 | function data(){return{dark:window.localStorage.getItem("dark")?JSON.parse(window.localStorage.getItem("dark")):!!window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches,toggleTheme(){var e;this.dark=!this.dark,e=this.dark,window.localStorage.setItem("dark",e)},isSideMenuOpen:!1,toggleSideMenu(){this.isSideMenuOpen=!this.isSideMenuOpen},closeSideMenu(){this.isSideMenuOpen=!1},isNotificationsMenuOpen:!1,toggleNotificationsMenu(){this.isNotificationsMenuOpen=!this.isNotificationsMenuOpen},closeNotificationsMenu(){this.isNotificationsMenuOpen=!1},isProfileMenuOpen:!1,toggleProfileMenu(){this.isProfileMenuOpen=!this.isProfileMenuOpen},closeProfileMenu(){this.isProfileMenuOpen=!1},isPagesMenuOpen:!1,togglePagesMenu(){this.isPagesMenuOpen=!this.isPagesMenuOpen},isModalOpen:!1,trapCleanup:null,openModal(){this.isModalOpen=!0,this.trapCleanup=focusTrap(document.querySelector("#modal"))},closeModal(){this.isModalOpen=!1,this.trapCleanup()}}}var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,a={},r={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&").replace(/=g.reach);x+=F.value.length,F=F.next){var P=F.value;if(n.length>e.length)return;if(!(P instanceof i)){var A,S=1;if(b){if(!(A=s(k,x,e,v))||A.index>=e.length)break;var $=A.index,M=A.index+A[0].length,O=x;for(O+=F.value.length;$>=O;)O+=(F=F.next).value.length;if(x=O-=F.value.length,F.value instanceof i)continue;for(var E=F;E!==n.tail&&(Og.reach&&(g.reach=N);var _=F.prev;if(C&&(_=u(n,_,C),x+=C.length),c(n,_,S),F=u(n,_,new i(d,h?r.tokenize(z,h):z,y,z)),j&&u(n,F,j),S>1){var L={cause:d+","+m,reach:N};o(e,n,t,F.prev,x,L),g&&L.reach>g.reach&&(g.reach=L.reach)}}}}}}function l(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var a=n.next,r={value:t,prev:n,next:a};return n.next=r,a.prev=r,e.length++,r}function c(e,n,t){for(var a=n.next,r=0;r"+i.content+""},!e.document)return e.addEventListener?(r.disableWorkerMessageHandler||e.addEventListener("message",(function(n){var t=JSON.parse(n.data),a=t.language,i=t.code,s=t.immediateClose;e.postMessage(r.highlight(i,r.languages[a],a)),s&&e.close()}),!1),r):r;var g=r.util.currentScript();function d(){r.manual||r.highlightAll()}if(g&&(r.filename=g.src,g.hasAttribute("data-manual")&&(r.manual=!0)),!r.manual){var p=document.readyState;"loading"===p||"interactive"===p&&g&&g.defer?document.addEventListener("DOMContentLoaded",d):window.requestAnimationFrame?window.requestAnimationFrame(d):window.setTimeout(d,16)}return r}(_self);"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism),Prism.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(e,n){var t={};t["language-"+n]={pattern:/(^$)/i,lookbehind:!0,inside:Prism.languages[n]},t.cdata=/^$/i;var a={"included-cdata":{pattern://i,inside:t}};a["language-"+n]={pattern:/[\s\S]+/,inside:Prism.languages[n]};var r={};r[e]={pattern:RegExp("(<__[^>]*>)(?:))*\\]\\]>|(?!)".replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:a},Prism.languages.insertBefore("markup","cdata",r)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(e,n){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+e+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[n,"language-"+n],inside:Prism.languages[n]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml,function(e){var n=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+n.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+n.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+n.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+n.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:n,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var t=e.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism),Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript,function(){if(void 0!==Prism&&"undefined"!=typeof document){var e="line-numbers",n=/\n(?!$)/g,t=Prism.plugins.lineNumbers={getLine:function(n,t){if("PRE"===n.tagName&&n.classList.contains(e)){var a=n.querySelector(".line-numbers-rows");if(a){var r=parseInt(n.getAttribute("data-start"),10)||1,i=r+(a.children.length-1);ti&&(t=i);var s=t-r;return a.children[s]}}},resize:function(e){r([e])},assumeViewportIndependence:!0},a=void 0;window.addEventListener("resize",(function(){t.assumeViewportIndependence&&a===window.innerWidth||(a=window.innerWidth,r(Array.prototype.slice.call(document.querySelectorAll("pre.line-numbers"))))})),Prism.hooks.add("complete",(function(t){if(t.code){var a=t.element,i=a.parentNode;if(i&&/pre/i.test(i.nodeName)&&!a.querySelector(".line-numbers-rows")&&Prism.util.isActive(a,e)){a.classList.remove(e),i.classList.add(e);var s,o=t.code.match(n),l=o?o.length+1:1,u=new Array(l+1).join("");(s=document.createElement("span")).setAttribute("aria-hidden","true"),s.className="line-numbers-rows",s.innerHTML=u,i.hasAttribute("data-start")&&(i.style.counterReset="linenumber "+(parseInt(i.getAttribute("data-start"),10)-1)),t.element.appendChild(s),r([i]),Prism.hooks.run("line-numbers",t)}}})),Prism.hooks.add("line-numbers",(function(e){e.plugins=e.plugins||{},e.plugins.lineNumbers=!0}))}function r(e){if(0!=(e=e.filter((function(e){var n,t=(n=e,n?window.getComputedStyle?getComputedStyle(n):n.currentStyle||null:null)["white-space"];return"pre-wrap"===t||"pre-line"===t}))).length){var t=e.map((function(e){var t=e.querySelector("code"),a=e.querySelector(".line-numbers-rows");if(t&&a){var r=e.querySelector(".line-numbers-sizer"),i=t.textContent.split(n);r||((r=document.createElement("span")).className="line-numbers-sizer",t.appendChild(r)),r.innerHTML="0",r.style.display="block";var s=r.getBoundingClientRect().height;return r.innerHTML="",{element:e,lines:i,lineHeights:[],oneLinerHeight:s,sizer:r}}})).filter(Boolean);t.forEach((function(e){var n=e.sizer,t=e.lines,a=e.lineHeights,r=e.oneLinerHeight;a[t.length-1]=void 0,t.forEach((function(e,t){if(e&&e.length>1){var i=n.appendChild(document.createElement("span"));i.style.display="block",i.textContent=e}else a[t]=r}))})),t.forEach((function(e){for(var n=e.sizer,t=e.lineHeights,a=0,r=0;rn&&(i[o]="\n"+i[o],s=l)}t[r]=i.join("")}return t.join("\n")}},"undefined"!=typeof module&&module.exports&&(module.exports=t),Prism.plugins.NormalizeWhitespace=new t({"remove-trailing":!0,"remove-indent":!0,"left-trim":!0,"right-trim":!0}),Prism.hooks.add("before-sanity-check",(function(e){var t=Prism.plugins.NormalizeWhitespace;if((!e.settings||!1!==e.settings["whitespace-normalization"])&&Prism.util.isActive(e.element,"whitespace-normalization",!0))if(e.element&&e.element.parentNode||!e.code){var a=e.element.parentNode;if(e.code&&a&&"pre"===a.nodeName.toLowerCase()){for(var r in null==e.settings&&(e.settings={}),n)if(Object.hasOwnProperty.call(n,r)){var i=n[r];if(a.hasAttribute("data-"+r))try{var s=JSON.parse(a.getAttribute("data-"+r)||"true");typeof s===i&&(e.settings[r]=s)}catch(e){}}for(var o=a.childNodes,l="",u="",c=!1,g=0;g response.json()) 3 | .then((ads) => { 4 | ads.forEach(function(ad){ 5 | //Check if ad is enabled 6 | if(ad.enabled){ 7 | //Loop through the ads and auto place them using insertAdjacentHTML 8 | let placements = JSON.parse(ad.placements); 9 | placements.forEach(function(placement){ 10 | if(placement.selector){ 11 | var adSelector = document.querySelector(placement.selector); 12 | if(adSelector){ 13 | let adBody = ''; 14 | if(ad.adType == 'HTML'){ 15 | adBody = "
"+ad.body+"
" 16 | }else if(ad.adType == 'IMAGE'){ 17 | adBody = "
\ 18 | \ 19 | \""+ad.imageAlt+"\"\ 20 | \ 21 |
" 22 | } 23 | adSelector.insertAdjacentHTML(placement.position, adBody); 24 | } 25 | } 26 | }); 27 | } 28 | }); 29 | 30 | //Remove the parent temp element (smart-ad-temp), since it messes with the CSS Design for some ads 31 | var smartAds = document.querySelectorAll('.smart-banner-temp'); 32 | //console.log(smartAds); 33 | smartAds.forEach(function(smartAd){ 34 | let adSlug = smartAd.getAttribute('banner-slug'); 35 | let adStyles = smartAd.getAttribute('style'); 36 | smartAd.firstElementChild.setAttribute("banner-slug", adSlug); 37 | smartAd.firstElementChild.setAttribute("style", adStyles); 38 | smartAd.firstElementChild.classList.add("smart-banner"); 39 | smartAd.replaceWith(...smartAd.childNodes); 40 | }); 41 | 42 | var smartAds = document.querySelectorAll('.smart-banner'); 43 | smartAds.forEach(function(smartAd){ 44 | smartAd.addEventListener('click', updateClick); 45 | }); 46 | 47 | }); 48 | 49 | //Event listener function for updating clicks 50 | function updateClick(e){ 51 | let token = document.querySelector('meta[name="csrf-token"]').getAttribute('content'); 52 | let slug = e.target.closest('.smart-banner').getAttribute('banner-slug'); 53 | fetch('/smart-banner-update-clicks', { 54 | headers: { 55 | "Content-Type": "application/json", 56 | "Accept": "application/json, text-plain, */*", 57 | "X-Requested-With": "XMLHttpRequest", 58 | "X-CSRF-TOKEN": token 59 | }, 60 | method: 'post', 61 | credentials: "same-origin", 62 | body: JSON.stringify({ 63 | slug: slug, 64 | }) 65 | }).catch(function(error) { 66 | console.log(error); 67 | }); 68 | } 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /resources/dist/js/smart-banner.min.js: -------------------------------------------------------------------------------- 1 | function updateClick(e){let t=document.querySelector('meta[name="csrf-token"]').getAttribute("content"),n=e.target.closest(".smart-banner").getAttribute("banner-slug");fetch("/smart-banner-update-clicks",{headers:{"Content-Type":"application/json",Accept:"application/json, text-plain, */*","X-Requested-With":"XMLHttpRequest","X-CSRF-TOKEN":t},method:"post",credentials:"same-origin",body:JSON.stringify({slug:n})}).catch((function(e){console.log(e)}))}fetch("/smart-banner-auto-placements").then((e=>e.json())).then((e=>{e.forEach((function(e){if(e.enabled){JSON.parse(e.placements).forEach((function(t){if(t.selector){var n=document.querySelector(t.selector);if(n){let a="";"HTML"==e.adType?a='
'+e.body+"
":"IMAGE"==e.adType&&(a=''),n.insertAdjacentHTML(t.position,a)}}}))}})),document.querySelectorAll(".smart-banner-temp").forEach((function(e){let t=e.getAttribute("banner-slug"),n=e.getAttribute("style");e.firstElementChild.setAttribute("banner-slug",t),e.firstElementChild.setAttribute("style",n),e.firstElementChild.classList.add("smart-banner"),e.replaceWith(...e.childNodes)})),document.querySelectorAll(".smart-banner").forEach((function(e){e.addEventListener("click",updateClick)}))})); 2 | -------------------------------------------------------------------------------- /resources/dist/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/css/app.css": "/css/app.css" 3 | } 4 | -------------------------------------------------------------------------------- /resources/views/components/smart-ad-component.blade.php: -------------------------------------------------------------------------------- 1 | @isset($smartAd) 2 |
3 | @if($smartAd->adType == 'HTML') 4 | {!! $smartAd->body !!} 5 | @elseif($smartAd->adType == 'IMAGE') 6 | 7 | {{$smartAd->imageAlt}} 8 | 9 | @endif 10 |
11 | @endisset 12 | -------------------------------------------------------------------------------- /resources/views/layouts/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Laravel Smart Ads Dashboard 7 | 11 | 12 | 13 | @yield('styles') 14 | 18 | 19 | 23 | @livewireStyles 24 | 25 | 26 |
30 | 31 | @include('smart-ads::layouts.partials.desktop-sidebar') 32 | 33 | 34 | @include('smart-ads::layouts.partials.mobile-sidebar') 35 |
36 | @include('smart-ads::layouts.partials.header') 37 |
38 | @yield('content') 39 |
40 |
41 |
42 | @yield('scripts') 43 | @livewireScripts 44 | 45 | -------------------------------------------------------------------------------- /resources/views/layouts/partials/desktop-sidebar.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/layouts/partials/header.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
5 | 6 | 24 |
    25 | 26 |
  • 27 | 59 |
  • 60 | 61 |
  • 62 | 71 | 105 |
  • 106 |
107 |
108 |
-------------------------------------------------------------------------------- /resources/views/layouts/partials/mobile-sidebar.blade.php: -------------------------------------------------------------------------------- 1 |
11 | -------------------------------------------------------------------------------- /resources/views/layouts/partials/nav.blade.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/views/livewire/ad-report-component.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
Clicks Summary
6 |
7 |
8 |
Today so far
9 |
{{$totalClicksToday}}
10 |
11 |
12 |
Yesterday
13 |
{{$totalClicksYesterday}}
14 |
15 |
16 |
Last 7 days
17 |
{{$totalClicks7Days}}
18 |
19 |
20 |
This month
21 |
{{$totalClicksThisMonth}}
22 |
23 |
24 |
25 |
26 |
27 |
Click Report By Date
28 | 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |

Ad Clicks

50 |

List of Ads with clicks count for the selected period.

51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | @foreach($clicksPerAd as $adClick) 66 | 67 | 68 | 69 | 70 | @endforeach 71 | 72 | 73 | 74 |
AdClicks
{{$adClick['name']}}{{$adClick['clicks']}}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 | 86 | 87 | 154 |
-------------------------------------------------------------------------------- /resources/views/smart-ad-manager/create.blade.php: -------------------------------------------------------------------------------- 1 | @extends('smart-ads::layouts.app') 2 | @section('content') 3 |
4 |

5 | Create New Ad 6 |

7 | 8 |
9 | @csrf 10 |
13 | 25 | 26 |
29 |
30 | 34 | 38 |
39 | 40 |
41 | 54 |
55 | 56 |
57 |
58 | 68 | 69 | 82 | 83 | 96 |
97 | 98 | 99 |
100 |
101 | 102 |
116 |
117 |
118 | 126 |
127 |
128 | 129 |
146 | 177 |
Add More placement
178 | 179 |
180 | 181 | 182 |
183 |
184 |
185 | 186 |
187 | 188 |
189 | 190 |
191 | 192 |
193 | 194 | 195 | 196 |
197 | @endsection -------------------------------------------------------------------------------- /resources/views/smart-ad-manager/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | @extends('smart-ads::layouts.app') 2 | @section('content') 3 |
4 |

5 | Dashboard 6 |

7 | 8 | @livewire('ad-report-component') 9 |
10 | @endsection 11 | @section('styles') 12 | 13 | @endsection -------------------------------------------------------------------------------- /resources/views/smart-ad-manager/edit.blade.php: -------------------------------------------------------------------------------- 1 | @extends('smart-ads::layouts.app') 2 | @section('content') 3 |
4 |

5 | Edit Ad 6 |

7 | 8 |
9 | @csrf 10 | @method('PUT') 11 |
14 | 26 | 27 |
30 |
31 | 35 | 39 |
40 | 41 |
42 | 55 |
56 | 57 |
58 |
59 | {{$smartAd->imageAlt}} 60 | 70 | 71 | 84 | 85 | 98 |
99 | 100 | 101 |
102 |
103 | 104 |
118 |
119 |
120 | 128 |
129 |
130 | 131 |
158 | 159 | 190 |
Add More placement
191 | 192 |
193 | 194 | 195 |
196 |
197 |
198 | 199 |
200 | 201 |
202 | 203 |
204 | 205 |
206 | 207 | 208 | 209 |
210 | @endsection -------------------------------------------------------------------------------- /resources/views/smart-ad-manager/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('smart-ads::layouts.app') 2 | @section('content') 3 |
4 |

7 | Ads 8 |

9 | 10 | 11 | @if(session()->has('message')) 12 |
13 | {{session('message')}} 14 |
15 | 16 | @endif 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 36 | @forelse($smartAds as $ad) 37 | 38 | 47 | 50 | 85 | 86 | @empty 87 | 88 | 91 | 92 | @endforelse 93 | 94 |
Ad NameClicksActions
39 |
40 | 45 |
46 |
48 | {{$ad->clicks}} 49 | 51 | @if($ad->enabled) 52 |
53 | @csrf 54 | 59 |
60 | @else 61 |
62 | @csrf 63 | 68 |
69 | @endif 70 | 71 | 72 | 73 | 74 | 75 |
76 | @csrf 77 | @method('DELETE') 78 | 83 |
84 |
89 | No Ads in the database 90 |
95 |
96 |
99 | 100 | Showing {{$smartAds->firstItem()}}-{{$smartAds->lastItem()}} of {{$smartAds->total()}} 101 | 102 | 103 | 104 |
105 | {{$smartAds->onEachSide(2)->links()}} 106 |
107 |
108 |
109 | 110 |
111 | @endsection -------------------------------------------------------------------------------- /resources/views/smart-ad-manager/show.blade.php: -------------------------------------------------------------------------------- 1 | @extends('smart-ads::layouts.app') 2 | @section('content') 3 |
4 |

5 | Ad : {{$smartAd->name}} 6 |

7 | 8 | 9 | @if(session()->has('message')) 10 |
11 | {{session('message')}} 12 |
13 | @endif 14 | 15 |
16 |
Ad Slug
17 |

18 | {{$smartAd->slug}} 19 |

20 | 21 | 22 | @if($smartAd->adType == "HTML") 23 |
Ad HTML Body
24 |

25 |


26 |                     {{ $smartAd->body }}
27 |                 
28 |

29 | @elseif($smartAd->adType == "IMAGE") 30 |
Ad Image
31 |

32 | {{$smartAd->imageAlt}} 33 |

34 | 35 |
Image URL
36 |

37 |

{{$smartAd->imageUrl}}

38 |

39 | @endif 40 | 41 |
42 |
Usage (Manual Placement)
43 |
44 | <x-smart-ad-component slug="{{$smartAd->slug}}"/> 45 |
46 |
47 | 48 |
49 |
Auto Placement
50 | @isset($smartAd->placements) 51 | @foreach(json_decode($smartAd->placements) as $placement) 52 | @if(!empty($placement->selector)) 53 | @switch($placement->position) 54 | @case('beforebegin') 55 | Before HTML Element 56 | @break 57 | @case('afterend') 58 | Before HTML Element 59 | @break 60 | @case('afterbegin') 61 | Inside HTML Selector (At Beginning) 62 | @break 63 | @case('beforeend') 64 | Inside HTML Selector (At End) 65 | @break 66 | @endswitch 67 | -> {{$placement->selector}}
68 | @endif 69 | @endforeach 70 | @else 71 |

No Auto placements set for this Ad

72 | @endisset 73 | 74 | 75 |
76 | 77 |
78 | 79 | 80 | 81 | 82 | 83 |
84 | @endsection -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | $prefix, 'middleware' => $middleware], function () { 11 | Route::get('/smart-ad-manager', [SmartAdManagerController::class, 'dashboard']); 12 | Route::get('/smart-ad-manager/ads', [SmartAdManagerController::class, 'index']); 13 | Route::get('/smart-ad-manager/ads/create', [SmartAdManagerController::class, 'create']); 14 | Route::get('/smart-ad-manager/ads/{smartAd}', [SmartAdManagerController::class, 'show']); 15 | Route::post('/smart-ad-manager/ads/store', [SmartAdManagerController::class, 'store']); 16 | Route::get('/smart-ad-manager/ads/edit/{smartAd}', [SmartAdManagerController::class, 'edit']); 17 | Route::put('/smart-ad-manager/ads/update/{smartAd}', [SmartAdManagerController::class, 'update']); 18 | Route::delete('/smart-ad-manager/ads/delete/{smartAd}', [SmartAdManagerController::class, 'delete']); 19 | Route::post('/smart-ad-manager/ads/disable/{smartAd}', [SmartAdManagerController::class, 'disable']); 20 | Route::post('/smart-ad-manager/ads/enable/{smartAd}', [SmartAdManagerController::class, 'enable']); 21 | }); 22 | 23 | Route::get('/smart-banner-auto-placements', [SmartAdManagerController::class, 'autoAds']); 24 | Route::post('/smart-banner-update-clicks', [SmartAdManagerController::class, 'updateClicks']); -------------------------------------------------------------------------------- /src/Components/SmartAdComponent.php: -------------------------------------------------------------------------------- 1 | slug = $slug; 25 | } 26 | 27 | /** 28 | * Get the view / contents that represent the component. 29 | * 30 | * @return \Illuminate\Contracts\View\View|\Closure|string 31 | */ 32 | public function render() 33 | { 34 | $smartAd = SmartAd::where('slug', $this->slug)->where('enabled', true)->first(); 35 | return view('smart-ads::components.smart-ad-component', compact('smartAd')); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Controllers/SmartAdManagerController.php: -------------------------------------------------------------------------------- 1 | orderBy('name')->paginate(10); 19 | $totalClicks = SmartAd::sum('clicks'); 20 | return view('smart-ads::smart-ad-manager.index', compact('smartAds', 'totalClicks')); 21 | } 22 | 23 | public function show(SmartAd $smartAd){ 24 | return view('smart-ads::smart-ad-manager.show', compact('smartAd')); 25 | } 26 | 27 | public function create(){ 28 | return view('smart-ads::smart-ad-manager.create'); 29 | } 30 | 31 | public function store(StoreSmartAdRequest $request){ 32 | 33 | if(isset($request->image)){ 34 | $imagePath = $request->file('image')->store('image', 'public'); 35 | } 36 | 37 | $smartAd = SmartAd::create([ 38 | 'name' => $request->name, 39 | 'slug' => $this->slug($request->name), 40 | 'adType' => $request->adType, 41 | 'body' => isset($request->body) ? $request->body : null, 42 | 'image' => isset($imagePath) ? $imagePath : null, 43 | 'imageUrl' => $request->imageUrl, 44 | 'imageAlt' => $request->imageAlt, 45 | 'enabled' => true, 46 | 'placements' => !empty(json_decode($request->placements)[0]->selector) ? $request->placements : null, 47 | ]); 48 | return redirect("/smart-ad-manager/ads/{$smartAd->id}")->with(['message' => 'Ad Created', 'color' => 'green']); 49 | } 50 | 51 | public function edit(SmartAd $smartAd){ 52 | return view('smart-ads::smart-ad-manager.edit', compact('smartAd')); 53 | } 54 | 55 | public function update(StoreSmartAdRequest $request, SmartAd $smartAd){ 56 | 57 | $smartAd->name = $request->name; 58 | $smartAd->placements = $request->placements; 59 | if($request->adType == 'HTML'){ 60 | $smartAd->image = null; 61 | $smartAd->imageUrl = null; 62 | $smartAd->imageAlt = null; 63 | $smartAd->body = $request->body; 64 | }elseif($request->adType == 'IMAGE'){ 65 | if(isset($request->image)){ 66 | $imagePath = $request->file('image')->store('image', 'public'); 67 | $smartAd->image = $imagePath; 68 | } 69 | 70 | $smartAd->imageUrl = isset($request->imageUrl) ? $request->imageUrl : null; 71 | $smartAd->imageAlt = isset($request->imageAlt) ? $request->imageAlt : null; 72 | } 73 | $smartAd->adType = $request->adType; 74 | 75 | $smartAd->save(); 76 | return redirect("/smart-ad-manager/ads/{$smartAd->id}")->with(['message' => 'Ad Edited', 'color' => 'green']); 77 | } 78 | 79 | public function delete(SmartAd $smartAd){ 80 | $smartAd->delete(); 81 | return redirect('/smart-ad-manager/ads')->with(['message' => 'Ad Deleted', 'color' => 'green']); 82 | } 83 | 84 | public function autoAds(){ 85 | $ads = SmartAd::whereNotNull('placements')->get(); 86 | return $ads; 87 | } 88 | 89 | /** 90 | * Adds click count to the add 91 | */ 92 | public function updateClicks(Request $request){ 93 | $slug = $request->get('slug'); 94 | LaravelSmartAdsFacade::updateClicks($slug); 95 | } 96 | 97 | //*Enable the Ad */ 98 | public function enable(SmartAd $smartAd){ 99 | $smartAd->enabled = true; 100 | $smartAd->save(); 101 | return redirect('/smart-ad-manager/ads')->with(['message' => 'Ad Enabled', 'color' => 'green']); 102 | } 103 | 104 | //*Disable the Ad */ 105 | public function disable(SmartAd $smartAd){ 106 | $smartAd->enabled = false; 107 | $smartAd->save(); 108 | return redirect('/smart-ad-manager/ads')->with(['message' => 'Ad Disabled', 'color' => 'green']); 109 | } 110 | 111 | protected function slug($data) 112 | { 113 | $ex = explode(' ', $data); 114 | return implode('-', $ex); 115 | } 116 | } 117 | 118 | -------------------------------------------------------------------------------- /src/Http/Livewire/AdReportComponent.php: -------------------------------------------------------------------------------- 1 | totalClicksToday = SmartAdTracking::whereDate('created_at', Carbon::today())->sum('totalClicks'); 33 | $this->totalClicksYesterday = SmartAdTracking::whereDate('created_at', Carbon::yesterday())->sum('totalClicks'); 34 | $this->totalClicks7Days = SmartAdTracking::whereBetween(DB::raw('DATE(created_at)'), [Carbon::today()->subDays(7), Carbon::today()])->sum('totalClicks'); 35 | $this->totalClicksThisMonth = SmartAdTracking::whereMonth('created_at', Carbon::now()->month)->sum('totalClicks'); 36 | return view('smart-ads::livewire.ad-report-component'); 37 | } 38 | 39 | public function calculateClicksReport(){ 40 | $date_from = Carbon::parse($this->reportStartDate)->startOfDay(); 41 | $date_to = Carbon::parse($this->reportEndDate)->endOfDay(); 42 | $smartAdTracking = SmartAdTracking:: 43 | where('created_at', '>=', $date_from) 44 | ->where('created_at', '<', $date_to) 45 | ->get(); 46 | 47 | //Calculate clicks per date 48 | $dateClicksCollection = $smartAdTracking->mapWithKeys(function ($item, $key) { 49 | return [$item->created_at->format('Y-m-d') => $item->totalClicks]; 50 | }); 51 | $period = CarbonPeriod::create($this->reportStartDate, $this->reportEndDate); 52 | $result = collect(); 53 | foreach($period as $p){ 54 | if($dateClicksCollection->has($p->format('Y-m-d'))){ 55 | $result[$p->format('Y-m-d')] = $dateClicksCollection->get($p->format('Y-m-d')); 56 | }else{ 57 | $result[$p->format('Y-m-d')] = 0; 58 | } 59 | } 60 | $this->clicksPerDate = $result->toArray(); 61 | 62 | //Calculate clicks per ad in the given period 63 | $adClicksCollection = $smartAdTracking->map(function($item, $key){ 64 | return json_decode($item['ad_clicks'], true); 65 | }); 66 | 67 | 68 | $ads = SmartAd::all(); 69 | foreach($ads as $ad){ 70 | array_push($this->clicksPerAd, ['name' => $ad->name, 'clicks' => $adClicksCollection->sum($ad->slug)]); 71 | } 72 | 73 | $this->clicksPerAd = collect($this->clicksPerAd)->sortByDesc('clicks')->values()->all(); 74 | 75 | $this->dispatch('renderChart'); 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/Http/Requests/StoreSmartAdRequest.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | public function rules() 26 | { 27 | if($this->form_type == 'edit'){ 28 | $image_rule = "nullable|image|mimes:jpg,png,jpeg,gif,svg|max:2048"; 29 | }else{ 30 | $image_rule = "required_if:adType,IMAGE|image|mimes:jpg,png,jpeg,gif,svg|max:2048"; 31 | } 32 | return [ 33 | 'name' => [ 34 | 'required', 35 | Rule::unique('smart_ads', 'name')->ignore($this->smartAd) 36 | ], 37 | "body" => "required_if:adType,HTML", 38 | "image" => $image_rule, 39 | "imageUrl" => "nullable|url" 40 | ]; 41 | } 42 | } -------------------------------------------------------------------------------- /src/LaravelSmartAds.php: -------------------------------------------------------------------------------- 1 | firstOrFail(); 13 | $smartAd->clicks++; 14 | $smartAd->save(); 15 | if(SmartAdTracking::whereDate('created_at', Carbon::today())->exists()){ 16 | $smartAdTracking = SmartAdTracking::whereDate('created_at', Carbon::today())->first(); 17 | $ad_clicks = json_decode($smartAdTracking->ad_clicks); 18 | if(isset($ad_clicks->{$smartAd->slug})){ 19 | $ad_clicks->{$smartAd->slug}++; //Increase clicks if already exists 20 | }else{ 21 | $ad_clicks->{$smartAd->slug} = 1; //First click of the day 22 | } 23 | $totalClicks = $smartAdTracking->totalClicks + 1; 24 | $smartAdTracking->update([ 25 | 'ad_clicks' => json_encode($ad_clicks), 26 | 'totalClicks' => $totalClicks 27 | ]); 28 | }else{ 29 | SmartAdTracking::create([ 30 | 'totalClicks' => 1, 31 | 'ad_clicks' => json_encode([$smartAd->slug => 1]) 32 | ]); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LaravelSmartAdsFacade.php: -------------------------------------------------------------------------------- 1 | name('laravel-smart-ads') 19 | ->hasConfigFile() 20 | ->hasViews() 21 | ->hasRoute('web') 22 | ->hasAssets() 23 | ->hasViewComponents('', SmartAdComponent::class) 24 | ->hasMigrations(['create_smart_ads_table','create_smart_ads_tracking_table']); 25 | 26 | } 27 | 28 | public function bootingPackage() 29 | { 30 | $this->registerLivewireComponents(); 31 | } 32 | 33 | public function registerLivewireComponents(){ 34 | Livewire::component('ad-report-component', AdReportComponent::class); 35 | } 36 | 37 | /** 38 | * Register the application services. 39 | */ 40 | public function registeringPackage() 41 | { 42 | // Register the main class to use with the facade 43 | $this->app->singleton('laravel-smart-ads', function () { 44 | return new LaravelSmartAds; 45 | }); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Models/SmartAd.php: -------------------------------------------------------------------------------- 1 | get('/smart-ad-manager/ads/create') 16 | ->assertSee('Create New Ad'); 17 | } 18 | 19 | /** @test */ 20 | public function it_asserts_ad_name_is_required_to_create_new_ad(){ 21 | $laravelAd = SmartAd::factory()->make(['name' => '']); 22 | $this->post('/smart-ad-manager/ads/store', $laravelAd->toArray()) 23 | ->assertSessionHasErrors(['name']); 24 | } 25 | 26 | /** @test */ 27 | public function it_asserts_ad_name_is_unique(){ 28 | $laravelAdExisting = SmartAd::factory()->create(['name' => 'ad-name']); 29 | $newLaravelAd = SmartAd::factory()->make(['name' => 'ad-name']); 30 | $this->post('/smart-ad-manager/ads/store', $newLaravelAd->toArray()) 31 | ->assertSessionHasErrors(['name']); 32 | } 33 | 34 | /** @test */ 35 | public function it_asserts_ad_body_is_required_to_create_new_ad_if_ad_type_is_html(){ 36 | $laravelAd = SmartAd::factory()->make(['body' => '', 'adType' => "HTML"]); 37 | $this->post('/smart-ad-manager/ads/store', $laravelAd->toArray()) 38 | ->assertSessionHasErrors(['body']); 39 | } 40 | 41 | /** @test */ 42 | public function it_asserts_ad_image_is_required_to_create_new_ad_if_ad_type_is_image(){ 43 | $laravelAd = SmartAd::factory()->make(['image' => '', 'adType' => "IMAGE"]); 44 | $this->post('/smart-ad-manager/ads/store', $laravelAd->toArray()) 45 | ->assertSessionHasErrors(['image']); 46 | } 47 | 48 | /** @test */ 49 | public function it_asserts_user_can_create_html_ad(){ 50 | $laravelAd = SmartAd::factory()->make(); 51 | 52 | $this->post('/smart-ad-manager/ads/store', $laravelAd->toArray()); 53 | 54 | $this->assertEquals(1,SmartAd::all()->count()); 55 | } 56 | 57 | /** @test */ 58 | public function it_asserts_user_can_create_image_ad(){ 59 | $image = UploadedFile::fake()->create('test.png', $kilobytes = 0); 60 | $laravelAd = SmartAd::factory()->make([ 61 | 'body'=> '', 62 | 'adType' => "IMAGE", 63 | 'image' => $image, 64 | 'imageAlt' => 'Alt Text', 65 | 'imageUrl' => 'http://www.google.com', 66 | ]); 67 | $this->post('/smart-ad-manager/ads/store', $laravelAd->toArray()); 68 | 69 | $this->assertDatabaseHas('smart_ads', [ 70 | 'imageAlt' => $laravelAd->imageAlt, 71 | 'imageUrl' => $laravelAd->imageUrl, 72 | ]); 73 | } 74 | 75 | /** @test */ 76 | public function it_asserts_image_url_is_a_valid_URL(){ 77 | $image = UploadedFile::fake()->create('test.png', $kilobytes = 0); 78 | $laravelAd = SmartAd::factory()->make([ 79 | 'body'=> '', 80 | 'adType' => "IMAGE", 81 | 'image' => $image, 82 | 'imageAlt' => 'Alt Text', 83 | 'imageUrl' => 'non-valid-url', 84 | ]); 85 | $this->post('/smart-ad-manager/ads/store', $laravelAd->toArray()) 86 | ->assertSessionHasErrors(['imageUrl']); 87 | } 88 | 89 | 90 | } 91 | -------------------------------------------------------------------------------- /tests/LaravelSmartAdsTest.php: -------------------------------------------------------------------------------- 1 | create(['name' => 'Alright']); 17 | $this->assertEquals('Alright', $smartAd->name); 18 | } 19 | 20 | /** @test */ 21 | public function it_asserts_a_laravel_ad_has_a_body(){ 22 | $smartAd = SmartAd::factory()->create(['body' => 'Hello']); 23 | $this->assertEquals('Hello', $smartAd->body); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /tests/LaravelSmartAdsTestCase.php: -------------------------------------------------------------------------------- 1 | set('app.key', '6rE9Nz59bGRbeMATftriyQjrpF7DcOQm'); 22 | 23 | } 24 | 25 | protected function getPackageProviders($app){ 26 | return[ 27 | LivewireServiceProvider::class, 28 | LaravelSmartAdsServiceProvider::class, 29 | ]; 30 | } 31 | 32 | protected function getPackageAliases($app) 33 | { 34 | return[ 35 | 'LaravelSmartAds' => LaravelSmartAdsFacade::class 36 | ]; 37 | } 38 | 39 | public function getEnvironmentSetUp($app) 40 | { 41 | $app['config']->set('view.paths', [ 42 | __DIR__.'/../resources/views', 43 | resource_path('views'), 44 | ]); 45 | 46 | // import the CreatePostsTable class from the migration 47 | require_once __DIR__ . '/../database/migrations/create_smart_ads_table.php.stub'; 48 | require_once __DIR__ . '/../database/migrations/create_smart_ads_tracking_table.php.stub'; 49 | 50 | // run the up() method of that migration class 51 | (new \CreateSmartAdsTable)->up(); 52 | (new \CreateSmartAdsTrackingTable)->up(); 53 | } 54 | } -------------------------------------------------------------------------------- /tests/SmartAdAdminTest.php: -------------------------------------------------------------------------------- 1 | count(5)->create(); 18 | $this->get('/smart-ad-manager/ads') 19 | ->assertSee($laravelAds->random()->name); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tests/SmartAdComponentTest.php: -------------------------------------------------------------------------------- 1 | create(['name'=> 'test-ad', 'body' => 'Hello']); 22 | 23 | $view = $this->blade( 24 | '', 25 | ); 26 | 27 | $view->assertSee($smartAd->body); 28 | 29 | } 30 | 31 | 32 | } -------------------------------------------------------------------------------- /tests/SmartAdDashboardTest.php: -------------------------------------------------------------------------------- 1 | create(['totalClicks' => 5]); 23 | SmartAdTracking::factory()->create(['totalClicks' => 10, 'created_at' => Carbon::yesterday()]); 24 | SmartAdTracking::factory()->create(['totalClicks' => 6, 'created_at' => Carbon::today()->subDays(2)]); 25 | 26 | $component = Livewire::test(AdReportComponent::class); 27 | $component->assertSet('totalClicksToday', 5); 28 | $component->assertSet('totalClicksYesterday', 10); 29 | $component->assertSet('totalClicks7Days', 21); 30 | 31 | } 32 | 33 | /** @test */ 34 | public function it_asserts_dashboard_has_correct_monthly_clicks_data(){ 35 | SmartAdTracking::factory()->create(['totalClicks' => '10', 'created_at' => '01-'.Carbon::now()->format('m').'-'.Carbon::now()->year]); 36 | SmartAdTracking::factory()->create(['totalClicks' => '5', 'created_at' => '03-'.Carbon::now()->format('m').'-'.Carbon::now()->year]); 37 | SmartAdTracking::factory()->create(['totalClicks' => '18', 'created_at' => '10-'.Carbon::now()->format('m').'-'.Carbon::now()->year]); 38 | SmartAdTracking::factory()->create(['totalClicks' => '4', 'created_at' => '15-'.Carbon::now()->format('m').'-'.Carbon::now()->year]); 39 | 40 | $component = Livewire::test(AdReportComponent::class); 41 | $component->assertSet('totalClicksThisMonth', 37); 42 | } 43 | 44 | /** @test */ 45 | public function it_asserts_correct_data_is_sent_to_graph(){ 46 | 47 | //Given that we have ad tracking for the last 7 days 48 | SmartAdTracking::factory()->create(['totalClicks' => 5]); 49 | SmartAdTracking::factory()->create(['totalClicks' => 10, 'created_at' => Carbon::yesterday()]); 50 | SmartAdTracking::factory()->create(['totalClicks' => 7, 'created_at' => Carbon::today()->subDays(2)]); 51 | SmartAdTracking::factory()->create(['totalClicks' => 1, 'created_at' => Carbon::today()->subDays(3)]); 52 | SmartAdTracking::factory()->create(['totalClicks' => 3, 'created_at' => Carbon::today()->subDays(5)]); 53 | SmartAdTracking::factory()->create(['totalClicks' => 4, 'created_at' => Carbon::today()->subDays(6)]); 54 | SmartAdTracking::factory()->create(['totalClicks' => 5, 'created_at' => Carbon::today()->subDays(7)]); 55 | 56 | $component = Livewire::test(AdReportComponent::class) 57 | ->set('reportStartDate', Carbon::today()->subDays(7)->format('Y-m-d')) 58 | ->set('reportEndDate', Carbon::today()->format('Y-m-d')) 59 | ->call('calculateClicksReport'); 60 | 61 | 62 | $component->assertSet('clicksPerDate', [ 63 | Carbon::today()->subDays(7)->format('Y-m-d') => 5, 64 | Carbon::today()->subDays(6)->format('Y-m-d') => 4, 65 | Carbon::today()->subDays(5)->format('Y-m-d') => 3, 66 | Carbon::today()->subDays(4)->format('Y-m-d') => 0, 67 | Carbon::today()->subDays(3)->format('Y-m-d') => 1, 68 | Carbon::today()->subDays(2)->format('Y-m-d') => 7, 69 | Carbon::today()->subDays(1)->format('Y-m-d') => 10, 70 | Carbon::today()->subDays(0)->format('Y-m-d') => 5 71 | ]); 72 | 73 | } 74 | 75 | /** @test */ 76 | public function it_asserts_correct_ad_clicks_are_shown_in_report(){ 77 | //Given that we have an ad 78 | $smartAd = SmartAd::factory()->create(); 79 | //That has been clicked over the span of few days 80 | SmartAdTracking::factory()->create(['ad_clicks' => json_encode([$smartAd->slug => '5'])]); 81 | SmartAdTracking::factory()->create(['ad_clicks' => json_encode([$smartAd->slug => '5']), 'created_at' => Carbon::yesterday()]); 82 | SmartAdTracking::factory()->create(['ad_clicks' => json_encode([$smartAd->slug => '7']), 'created_at' => Carbon::today()->subDays(2)]); 83 | SmartAdTracking::factory()->create(['ad_clicks' => json_encode([$smartAd->slug => '10']), 'created_at' => Carbon::today()->subDays(3)]); 84 | 85 | $component = Livewire::test(AdReportComponent::class) 86 | ->set('reportStartDate', Carbon::today()->subDays(7)->format('Y-m-d')) 87 | ->set('reportEndDate', Carbon::today()->format('Y-m-d')) 88 | ->call('calculateClicksReport'); 89 | 90 | $component->assertSet('clicksPerAd', [ 91 | [ 92 | 'name' => $smartAd->name, 93 | 'clicks' => 27 94 | ] 95 | ]); 96 | 97 | } 98 | 99 | /** @test */ 100 | public function it_asserts_correct_ad_clicks_for_multiple_ads_are_shown_in_report(){ 101 | //Given that we have two ads 102 | $smartAd1 = SmartAd::factory()->create(); 103 | $smartAd2 = SmartAd::factory()->create(); 104 | //That has been clicked over the span of few days 105 | SmartAdTracking::factory()->create(['ad_clicks' => json_encode([$smartAd1->slug => '5'])]); 106 | SmartAdTracking::factory()->create(['ad_clicks' => json_encode([$smartAd1->slug => '5', $smartAd2->slug => '6']), 'created_at' => Carbon::yesterday()]); 107 | SmartAdTracking::factory()->create(['ad_clicks' => json_encode([$smartAd1->slug => '7']), 'created_at' => Carbon::today()->subDays(2)]); 108 | SmartAdTracking::factory()->create(['ad_clicks' => json_encode([$smartAd1->slug => '10', $smartAd2->slug => '5']), 'created_at' => Carbon::today()->subDays(3)]); 109 | 110 | $component = Livewire::test(AdReportComponent::class) 111 | ->set('reportStartDate', Carbon::today()->subDays(7)->format('Y-m-d')) 112 | ->set('reportEndDate', Carbon::today()->format('Y-m-d')) 113 | ->call('calculateClicksReport'); 114 | 115 | $component->assertSet('clicksPerAd', [ 116 | [ 117 | 'name' => $smartAd1->name, 118 | 'clicks' => 27 119 | ], 120 | [ 121 | 'name' => $smartAd2->name, 122 | 'clicks' => 11 123 | ] 124 | ]); 125 | 126 | } 127 | 128 | } -------------------------------------------------------------------------------- /tests/SmartAdTrackingTest.php: -------------------------------------------------------------------------------- 1 | create(); 21 | //User clicks on the ad 22 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd->slug]); 23 | //Ad clicks count is updated in smart_ads_table 24 | $smartAd->refresh(); 25 | $this->assertEquals(1, $smartAd->clicks); 26 | //Ad Clicks gets updated in smart_ads_tracking_table 27 | $smartAdTracking = SmartAdTracking::whereDate('created_at', Carbon::today())->first(); 28 | $this->assertEquals(1, json_decode($smartAdTracking->ad_clicks)->{$smartAd->slug}); 29 | $this->assertEquals(1, $smartAdTracking->totalClicks); 30 | } 31 | 32 | 33 | /** @test */ 34 | public function it_asserts_click_tracking_works_with_multiple_clicks(){ 35 | //Given that we have an Ad in the database 36 | $smartAd = SmartAd::factory()->create(); 37 | //User clicks on the ad multiple times 38 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd->slug]); 39 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd->slug]); 40 | //Ad Clicks gets updated in smart_ads_tracking_table 41 | $smartAdTracking = SmartAdTracking::whereDate('created_at', Carbon::today())->first(); 42 | $this->assertEquals(2, json_decode($smartAdTracking->ad_clicks)->{$smartAd->slug}); 43 | $this->assertEquals(2, $smartAdTracking->totalClicks); 44 | } 45 | 46 | /** @test */ 47 | public function it_asserts_click_tracking_works_with_multiple_ads(){ 48 | //Given that we have multiple Ad in the database 49 | $smartAd1 = SmartAd::factory()->create(); 50 | $smartAd2 = SmartAd::factory()->create(); 51 | //User clicks on the ad multiple times 52 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd1->slug]); 53 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd2->slug]); 54 | //Ad Clicks gets updated in smart_ads_tracking_table 55 | $smartAdTracking = SmartAdTracking::whereDate('created_at', Carbon::today())->first(); 56 | $this->assertEquals(1, json_decode($smartAdTracking->ad_clicks)->{$smartAd1->slug}); 57 | $this->assertEquals(1, json_decode($smartAdTracking->ad_clicks)->{$smartAd2->slug}); 58 | $this->assertEquals(2, $smartAdTracking->totalClicks); 59 | } 60 | 61 | /** @test */ 62 | public function it_asserts_click_tracking_works_with_multiple_ads_and_multiple_clicks(){ 63 | //Given that we have multiple Ad in the database 64 | $smartAd1 = SmartAd::factory()->create(); 65 | $smartAd2 = SmartAd::factory()->create(); 66 | //User clicks on the ad multiple times 67 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd1->slug]); 68 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd1->slug]); 69 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd1->slug]); 70 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd2->slug]); 71 | $this->post('/smart-banner-update-clicks', ['slug' => $smartAd2->slug]); 72 | //Ad Clicks gets updated in smart_ads_tracking_table 73 | $smartAdTracking = SmartAdTracking::whereDate('created_at', Carbon::today())->first(); 74 | $this->assertEquals(3, json_decode($smartAdTracking->ad_clicks)->{$smartAd1->slug}); 75 | $this->assertEquals(2, json_decode($smartAdTracking->ad_clicks)->{$smartAd2->slug}); 76 | $this->assertEquals(5, $smartAdTracking->totalClicks); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix'); 2 | 3 | require('mix-tailwindcss'); 4 | 5 | mix.postCss('resources/assets/css/app.css', 'resources/dist/css') 6 | .tailwind() 7 | 8 | mix.combine(['resources/assets/js/init-alpine.js', 'resources/assets/js/prism.js'], 'resources/dist/js/banner-manager.js'); 9 | mix.minify('resources/dist/js/smart-banner.js'); 10 | --------------------------------------------------------------------------------