diff --git a/.github/workflows/alert.yml b/.github/workflows/alert.yml
index a5f2bc58..7667504b 100644
--- a/.github/workflows/alert.yml
+++ b/.github/workflows/alert.yml
@@ -1,35 +1,39 @@
-name: Laravel
+name: Testing
on:
push:
- branches: [ "master" ]
+ branches: [ "master", "feature/laravel-10" ]
pull_request:
branches: [ "master" ]
+permissions:
+ contents: read
+
jobs:
- laravel-tests:
+ build:
runs-on: ubuntu-latest
steps:
- - uses: shivammathur/setup-php@15c43e89cdef867065b0213be354c2841860869e
- with:
- php-version: '8.0'
- uses: actions/checkout@v3
- - name: Copy .env
- run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- - name: Install Dependencies
- run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- - name: Generate key
- run: php artisan key:generate
- - name: Directory Permissions
- run: chmod -R 777 storage bootstrap/cache
- - name: Create Database
- run: |
- mkdir -p database
- touch database/database.sqlite
- - name: Execute tests (Unit and Feature tests) via PHPUnit
+
+ - name: Validate composer.json and composer.lock
+ run: composer validate --strict
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist --no-progress
+
+ - name: Execute tests
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
- run: vendor/bin/phpunit
+ run: php vendor/bin/testbench package:test
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..18c91471
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/README.md b/README.md
index a878083b..c649f1dd 100644
--- a/README.md
+++ b/README.md
@@ -1,206 +1,126 @@
-# Alert
+
+
+
-[](https://gitter.im/digitlimit/alert?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+> Version 2.0
-Alert is designed to make flashing messages in Laravel Applications a breeze.
-Tested and works in Laravel 5 or less
+Alert is Laravel package for displaying different types of messages in Laravel application views.
+It's designed to make flashing messages in Laravel Applications a breeze, with a lot of easy to use and fluent methods.
-### For this to work you need to include Twitter Bootstrap in your page
+## Quick Start
-## Installation
+1. Install Alert with composer:
-Add alert in your composer.json file:
-
-```php
-"require": {
- "digitlimit/alert": "v1.0"
-}
```
-
-Then run
-```command
-composer update
+composer require digitlimit/alert
```
-In Laravel 5 include Alert Service Provider within config/app.php or app/config/app.php in Laravel 4
+2. Somewhere in the blade template
-```php
-'providers' => [
- 'Digitlimit\Alert\AlertServiceProvider'
-];
+```
+
```
-You can also include alert facade in aliases array in same file above i.e config/app.php or app/config/app.php in Laravel 4
+Example:
-```php
-'aliases' => [
- 'Alert' => 'Digitlimit\Alert\Facades\Alert'
-];
```
+@extends('layouts.default')
+
+@section('content')
+
+
-## Usage
+
+ @include('form.profile')
+
+
-In your controller simply set your alert before redirection like so:
+ @include('partials.footer')
+@endsection
+```
-In laravel 5 controller for example:
+NB: At the moment the alert components are built with Twitter Bootstrap 5, and can be customized to use other CSS classes.
+Need to ensure bootstrap is included on the page.
-```php
-success()
- ->closable();
-
- return redirect()->route('users.getRegister');
- }
-}
+namespace App\Http\Controllers;
+use Alert;
-class LoginController extends Controller
+class DashboardController extends Controller
{
- public function postLogin(Request $request)
+ public function index()
{
- /*run validation on request parameters*/
- $validator = validator($request->only(['email','password']), [
- 'email' => 'required|email',
- 'password' => 'required|string',
- ]);
-
- /*return errors to view if any*/
- if($validator->fails()){
-
- Alert::form('Some errors occured','Opps')
- ->error();
-
- return redirect()
- ->back()
- ->withErrors($validator);
- }
+ Alert::message('Welcome! Please complete your profile')
+ ->info()
+ ->flash();
+
+ return view('home');
}
}
```
-Then in your view your can include the flash message to your view like so:
-
-```html
-
-
- @include('alert::form')
-
-
-
-```
-##### Form Alert Example
-
-
-
-## Features
-There are basically three types of alert messages you can flash
-
-#### Form - used mostly to display message in the header of your form
-
-Some where in the controller:
-```pph
-//This simply displays a success message
-Alert::form('Your account was successfully created','Congratulations')->success();
-```
-Some where in the view:
-```html
-@include('alert::form')
-```
+4. Result
-#### Notify - used mostly on the header of your page
+
-Some where in the controller:
-```pph
-//This simply displays a success message
-Alert::notify('Your account is going to expire today.','Info')->info();
-```
-Some where in the view layout where you want the alert to appear:
-```html
-@include('alert::notify')
-```
-#### Modal - used mostly to display an overlay message
+## Documentation
-Some where in the controller:
-```php
-//This simply displays a success message
-Alert::modal('Thanks for joining us.','Title')->info();
-```
-Some where in the view layout:
-```html
-@include('alert::modal')
-```
+Learn how to get started with Alert and then dive deeper into other and advanced topics:
-#### Customize alert views
-To customized various alert views run artisan command below
-```
- php artisan vendor:publish --provider="Digitlimit\Alert\AlertServiceProvider"
-```
+[Complete documentation](https://github.com/digitlimit/alert/wiki)
-This will publish `form.blade.php`, `modal.blade.php`, `notify.blade.php` and `sticky.blade.php`
-to publish views to `resources/views/vendor/alert` directory.
+## Change log
-The following helpers can be used in blade templates
+Coming soon
-**Alert Icon**
-This displays alert icon class e.g fa fa-cog
-Example:
-` `
+## Code of conduct
-**Alert Status**
-This displays alert status
-Example:
-``
+We will behave ourselves if you behave yourselves. For more details see our
+[CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
-**Alert Title**
-This displays alert title
-Example:
-`
{{Alert::title()}} `
+## Contributing
-**Alert Message**
-This displays alert message
-Example:
-`
{{Alert::message()}}
`
+Please read through our [contributing guidelines](./CONTRIBUTING.md). Included
+are directions for opening issues.
-**Form alert customization example**
+## Versioning
-```
-@if(Alert::has('form'))
-
+`
..`
- @if(Alert::icon()) @endif
+For more information on SemVer, please visit https://semver.org.
- @if(Alert::title()){{Alert::title()}} @endif {{Alert::message()}}
+* Any release may update the design, look-and-feel, or branding of an existing
+ icon
+* We will never intentionally release a `patch` version update that breaks
+ backward compatibility
+* A `minor` release **may include backward-incompatible changes** but we will
+ write clear upgrading instructions in UPGRADING.md
+* A `minor` or `patch` release will never remove icons
+* Bug fixes will be addressed as `patch` releases unless they include backward
+ incompatibility then they will be `minor` releases
-
- ×
-
-
-@endif
-```
+## License
+
+Alert Free is free, open source, and GPL friendly. You can use it for
+commercial projects, open source projects, or really almost whatever you want.
+
+- Code — MIT License
+ - In the Alert Free download, the MIT license applies all PHP files.
+We've kept attribution comments terse, so we ask that you do not actively work
+to remove them from files, especially code. They're a great way for folks to
+learn about Alert.
diff --git a/TODO b/TODO
new file mode 100644
index 00000000..ac169bc9
--- /dev/null
+++ b/TODO
@@ -0,0 +1,3 @@
+
+Todo:
+ ☐ composer require "digitlimit/alert":"dev-feature/laravel-10"
\ No newline at end of file
diff --git a/composer.json b/composer.json
index df6749c8..e198c81d 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,15 @@
{
"name": "digitlimit/alert",
- "description": "An easy way of flashing messages in Laravel Application ",
+ "description": "An easy way of flashing messages in Laravel Application",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Emeka Mbah",
+ "email": "frankemeks77@yahoo.com",
+ "homepage": "https://emekambah.medium.com"
+ }
+ ],
+ "homepage": "https://github.com/digitlimit/alert",
"keywords": [
"laravel",
"views",
@@ -8,25 +17,51 @@
"messages",
"withErrors"
],
- "license": "MIT",
- "authors": [
- {
- "name": "Mbah Emeka",
- "email": "frankemeks77@yahoo.com"
- }
- ],
"require": {
- "php": ">=5.4.0",
- "illuminate/support": "~5.0|^6.0"
+ "ext-json": "*",
+ "php": "^7.4|^8.0",
+ "illuminate/contracts": "^7.20|^8.19|^9.0|^10.0",
+ "illuminate/support": "^7.20|^8.19|^9.0|^10.0"
},
"require-dev": {
- "mockery/mockery": "dev-master",
- "phpunit/phpunit": "^9.0"
+ "guzzlehttp/guzzle": "^7.3",
+ "laravel/framework": "^7.20|^8.19|^9.0|^10.0",
+ "phpunit/phpunit": "^10.0",
+ "orchestra/testbench": "^8.5",
+ "nunomaduro/collision": "^7.5",
+ "pestphp/pest": "^2.5"
},
"autoload": {
- "psr-0": {
- "Digitlimit\\Alert": "src/"
+ "psr-4": {
+ "Digitlimit\\Alert\\": "src/"
+ },
+ "files": [
+ "src/Helpers.php"
+ ]
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Digitlimit\\Alert\\Tests\\": "tests"
}
},
- "minimum-stability": "stable"
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Digitlimit\\Alert\\AlertServiceProvider"
+ ],
+ "aliases": {
+ "Alert": "Digitlimit\\Alert\\Facades\\Alert"
+ }
+ }
+ },
+ "scripts": {
+ "post-autoload-dump": [
+ "@php vendor/bin/testbench package:discover --ansi"
+ ]
+ },
+ "config": {
+ "allow-plugins": {
+ "pestphp/pest-plugin": true
+ }
+ }
}
diff --git a/config/alert.php b/config/alert.php
new file mode 100644
index 00000000..03a89901
--- /dev/null
+++ b/config/alert.php
@@ -0,0 +1,40 @@
+ [
+ 'alert-normal' => [
+ 'alert' => \Digitlimit\Alert\Types\Normal::class,
+ 'component' => \Digitlimit\Alert\View\Components\Normal::class,
+ ],
+
+ 'alert-field' => [
+ 'alert' => \Digitlimit\Alert\Types\Field::class,
+ 'component' => \Digitlimit\Alert\View\Components\Field::class,
+ ],
+
+ 'alert-modal' => [
+ 'alert' => \Digitlimit\Alert\Types\Modal::class,
+ 'component' => \Digitlimit\Alert\View\Components\Modal::class,
+ ],
+
+ 'alert-notify' => [
+ 'alert' => \Digitlimit\Alert\Types\Notify::class,
+ 'component' => \Digitlimit\Alert\View\Components\Notify::class,
+ ],
+
+ 'alert-sticky' => [
+ 'alert' => \Digitlimit\Alert\Types\Sticky::class,
+ 'component' => \Digitlimit\Alert\View\Components\Sticky::class,
+ ],
+ ],
+];
diff --git a/phpunit.xml b/phpunit.xml
index 3347b75b..01bf269d 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,18 +1,21 @@
-
-
-
- ./tests/
-
-
+
+
+
+
+ ./tests/Unit
+
+
+ ./tests/Feature
+
+
+
+
+
+
+
+
+ src/
+
+
diff --git a/resources/views/components/field.blade.php b/resources/views/components/field.blade.php
new file mode 100644
index 00000000..1126fa64
--- /dev/null
+++ b/resources/views/components/field.blade.php
@@ -0,0 +1,35 @@
+@php
+ $name = $attributes->get('name', '');
+ $tag = $attributes->get('tag', $defaultTag);
+ $field = $alert->named('field', $name, $tag) ?? $alert->tagged('field', $tag);
+@endphp
+
+@if($slot->isNotEmpty())
+ {{ $slot }}
+@elseif($field)
+ @if(($name && $field->name == $name) || empty($name))
+ merge(['class' => 'form-text text-'.$field->level]) }}>
+ {{ $error ?? $field->message }}
+
+ @elseif($field->messageFor($name))
+ merge(['class' => 'form-text text-'.$field->level]) }}>
+ {{ $field->messageFor($name) }}
+
+ @endif
+@elseif(isset($errors))
+ @php
+ $level = $field->level ?? 'danger';
+ @endphp
+
+ @error($name, $tag)
+ merge(['class' => 'form-text text-'.$level ]) }}>
+ {{ $errors->$tag->first($name) }}
+
+ @enderror
+
+ @error($name)
+ merge(['class' => 'form-text text-'.$level ]) }}>
+ {{ $errors->first($name) }}
+
+ @enderror
+@endif
\ No newline at end of file
diff --git a/resources/views/components/modal.blade.php b/resources/views/components/modal.blade.php
new file mode 100644
index 00000000..424ba96b
--- /dev/null
+++ b/resources/views/components/modal.blade.php
@@ -0,0 +1,126 @@
+@php
+ $tag = $attributes->get('tag', $defaultTag);
+ $id = $attributes->get('id');
+ $modal = $alert->tagged('modal', $tag);
+
+ $cancel = $modal->cancel ?? '';
+ $action = $modal->action ?? '';
+ $view = $modal->view ?? '';
+@endphp
+
+@props([
+ 'header',
+ 'body',
+ 'footer'
+])
+
+@if($modal)
+ @php
+ $id = $id ?? $modal->id;
+ $hasBody = isset($body) && $body->isNotEmpty();
+ $hasHeader = isset($header) && $header->isNotEmpty();
+ $hasFooter = isset($footer) && $footer->isNotEmpty();
+ $hasTitle = $hasHeader || $modal->title;
+ @endphp
+ merge(['id' => $id]) }}
+ {{ $attributes->merge(['class' => 'modal']) }}
+ {{ $attributes->merge(['tabindex' => '-1']) }}
+ >
+
+
+
+@endif
\ No newline at end of file
diff --git a/resources/views/components/normal.blade.php b/resources/views/components/normal.blade.php
new file mode 100644
index 00000000..aeba864b
--- /dev/null
+++ b/resources/views/components/normal.blade.php
@@ -0,0 +1,18 @@
+@php
+ $id = $attributes->get('id');
+ $tag = $attributes->get('tag', $defaultTag);
+ $normal = $alert->tagged('normal', $tag);
+@endphp
+@if($normal)
+ @php
+ $id = $id ?? $normal->id;
+ @endphp
+ merge(['class' => 'alert alert-dismissible alert-'.$normal->level]) }} role="alert">
+ @if ($slot->isNotEmpty())
+ {{ $slot }}
+ @else
+ @if($normal->title){{ $normal->title }} @endif {{ $normal->message }}
+
+ @endif
+
+@endif
\ No newline at end of file
diff --git a/resources/views/components/notify.blade.php b/resources/views/components/notify.blade.php
new file mode 100644
index 00000000..a7e7bd5e
--- /dev/null
+++ b/resources/views/components/notify.blade.php
@@ -0,0 +1,71 @@
+@php
+ $tag = $attributes->get('tag', $defaultTag);
+ $id = $attributes->get('id');
+ $notify = $alert->tagged('notify', $tag);
+ $view = $notify->view ?? '';
+ $position = $notify->position ?? 'bottom-0 end-0';
+@endphp
+@if($notify)
+ @php
+ $id = $id ?? $notify->id;
+ $hasBody = isset($body) && $body->isNotEmpty();
+ $hasHeader = isset($header) && $header->isNotEmpty();
+ $hasTitle = $hasHeader || $notify->title;
+ @endphp
+ merge(['class' => 'position-fixed p-3 ' . $position]) }}
+ {{ $attributes->merge(['z-index' => '100']) }}
+ >
+
+
+ @if($hasTitle)
+
attributes->class(['toast-header']) : 'class=toast-header' }}>
+ @if ($hasHeader)
+ {{ $header }}
+ @elseif($notify->title)
+
+ {{ $notify->title }}
+
+ {{ $notify->subtitle ?? '' }}
+
+ @endif
+
+ @endif
+
+ @if($view)
+ {!! $view !!}
+ @elseif(!$hasTitle)
+
+
+
attributes->class(['toast-body']) : 'class=toast-body' }}>
+ @if($hasBody)
+ {{ $body }}
+ @elseif($notify->message)
+ {{ $notify->message }}
+ @endif
+
+
+ @if(!$hasBody)
+
+ @endif
+
+ @else
+
attributes->class(['toast-body']) : 'class=toast-body' }}>
+ @if($hasBody)
+ {{ $body }}
+ @elseif($notify->message)
+ {{ $notify->message }}
+ @endif
+
+ @endif
+
+
+
+
+@endif
\ No newline at end of file
diff --git a/resources/views/components/sticky.blade.php b/resources/views/components/sticky.blade.php
new file mode 100644
index 00000000..e90d7cb7
--- /dev/null
+++ b/resources/views/components/sticky.blade.php
@@ -0,0 +1,32 @@
+@php
+ $id = $attributes->get('id');
+ $tag = $attributes->get('tag', $defaultTag);
+ $sticky = $alert->tagged('sticky', $tag);
+ $action = $sticky->action ?? '';
+@endphp
+@if($sticky)
+ @php
+ $id = $id ?? $sticky->id;
+ @endphp
+
+ merge(['class' => 'alert alert-dismissible alert-'.$sticky->level]) }} role="alert">
+ @if ($slot->isNotEmpty())
+ {{ $slot }}
+ @else
+ @if($sticky->title)
{{ $sticky->title }} @endif {{ $sticky->message }}
+
+ @if($action->label)
+ @if($action->link)
+
attributes) !!}>
+ {{ $action->label }}
+
+ @else
+
attributes) !!}>
+ {{ $action->label }}
+
+ @endif
+ @endif
+
+ @endif
+
+@endif
\ No newline at end of file
diff --git a/src/Alert.php b/src/Alert.php
new file mode 100644
index 00000000..2275a155
--- /dev/null
+++ b/src/Alert.php
@@ -0,0 +1,147 @@
+session->get(
+ SessionKey::key($type, $tag)
+ );
+ }
+
+ /**
+ * Fetch an alert based on the tag name.
+ */
+ public function tagged(
+ string $type,
+ string $tag
+ ): MessageInterface|null {
+ if (!Type::exists($type)) {
+ throw new Exception("Invalid alert type '$type'. Check the alert config");
+ }
+
+ $tagged = $this->session->get(
+ SessionKey::key($type, $tag)
+ );
+
+ if (is_array($tagged)) {
+ return null;
+ }
+
+ return $tagged;
+ }
+
+ /**
+ * Fetch the normal alert.
+ */
+ public function normal(string $message = null): MessageInterface
+ {
+ return MessageFactory::make($this->session, 'normal', $message);
+ }
+
+ /**
+ * Fetch the field alert.
+ */
+ public function field(
+ string $message = null,
+ ?Validator $validator = null
+ ): MessageInterface {
+ return MessageFactory::make(
+ $this->session,
+ 'field',
+ $message,
+ $validator
+ );
+ }
+
+ /**
+ * Fetch the modal alert.
+ */
+ public function modal(string $message = null): MessageInterface
+ {
+ return MessageFactory::make($this->session, 'modal', $message);
+ }
+
+ /**
+ * Fetch the notify alert.
+ */
+ public function notify(string $message = null): MessageInterface
+ {
+ return MessageFactory::make($this->session, 'notify', $message);
+ }
+
+ /**
+ * Fetch the sticky alert.
+ */
+ public function sticky(string $message = null): MessageInterface
+ {
+ return MessageFactory::make($this->session, 'sticky', $message);
+ }
+
+ /**
+ * Fetch the default alert type, which is the normal alert.
+ */
+ public function message(string $message): MessageInterface
+ {
+ return $this->normal($message);
+ }
+
+ /**
+ * Fetch an alert from the given alert type.
+ */
+ public function from(
+ string $type,
+ string $message = null,
+ ...$args
+ ): MessageInterface {
+ if (!Type::exists($type)) {
+ throw new Exception("Invalid alert type '$type'. Check the alert config");
+ }
+
+ return MessageFactory::make($this->session, $type, $message, ...$args);
+ }
+}
diff --git a/src/AlertServiceProvider.php b/src/AlertServiceProvider.php
new file mode 100644
index 00000000..266094ae
--- /dev/null
+++ b/src/AlertServiceProvider.php
@@ -0,0 +1,75 @@
+loadViewsFrom(__DIR__.'/../resources/views', 'alert');
+
+ $this->registerComponents();
+
+ $this->bootForConsole();
+ }
+
+ /**
+ * Register any package services.
+ */
+ public function register(): void
+ {
+ $this->app->bind(SessionInterface::class, Session::class);
+ $this->app->bind(ConfigInterface::class, Config::class);
+
+ $this->app->singleton('alert', function ($app) {
+ return $app->make(Alert::class);
+ });
+
+ $this->mergeConfigFrom(
+ __DIR__.'/../config/alert.php',
+ 'alert'
+ );
+ }
+
+ /**
+ * Get the services provided by the provider.
+ */
+ public function provides(): array
+ {
+ return ['alert'];
+ }
+
+ /**
+ * Console-specific booting.
+ */
+ protected function bootForConsole(): void
+ {
+ $this->publishes([
+ __DIR__.'/../resources/views' => base_path('resources/views/vendor/digitlimit/alert'),
+ ], 'alert.views');
+
+ $this->publishes([
+ __DIR__.'/../config/alert.php' => config_path('alert.php'),
+ ], 'alert.config');
+ }
+
+ /**
+ * Register alert components.
+ */
+ protected function registerComponents(): void
+ {
+ Blade::componentNamespace('Digitlimit\\Alert\View\\Components', 'alert');
+
+ $types = config('alert.types');
+
+ foreach ($types as $name => $type) {
+ Blade::component($name, $type['component']);
+ }
+ }
+}
diff --git a/src/Component/Button.php b/src/Component/Button.php
new file mode 100644
index 00000000..7b3ea6c2
--- /dev/null
+++ b/src/Component/Button.php
@@ -0,0 +1,42 @@
+label = $label;
+ }
+
+ /**
+ * Set the button link.
+ */
+ public function link(string $link): void
+ {
+ $this->link = $link;
+ }
+
+ /**
+ * Set the button attributes.
+ */
+ public function attributes(array $attributes): void
+ {
+ $this->attributes = $attributes;
+ }
+}
diff --git a/src/Config.php b/src/Config.php
new file mode 100644
index 00000000..3a40a23e
--- /dev/null
+++ b/src/Config.php
@@ -0,0 +1,26 @@
+config = $config;
+ }
+
+ /**
+ * Fetch value from the config based on the given key.
+ */
+ public function get(string $key, mixed $default = null): mixed
+ {
+ return $this->config->get($key, $default);
+ }
+}
diff --git a/src/ConfigInterface.php b/src/ConfigInterface.php
new file mode 100644
index 00000000..472628a4
--- /dev/null
+++ b/src/ConfigInterface.php
@@ -0,0 +1,11 @@
+session = $session;
- }
-
- protected function flash($type = 'flash')
- {
- $this->alert = [
- $this->type => true,
- 'type' => $this->type,
- 'title' => $this->title,
- 'message' => $this->message,
- 'status' => $this->status,
- 'icon' => $this->icon,
-
- 'closable' => $this->closable,
- 'un_closable' => $this->un_closable,
- 'un_closable_strict' => $this->un_closable_strict,
- 'self_destroy' => $this->self_destroy,
- 'persist' => $this->persist,
-
- 'sticky_title' => $this->sticky_title,
- 'sticky_message' => $this->sticky_message,
-
- 'modal_size' => $this->modal_size,
- 'modal_view' => $this->modal_view,
-
- 'action_button_label' => $this->action_button_label,
- 'action_button_url' => $this->action_button_url,
- 'close_button_label' => $this->close_button_label,
- 'close_button_url' => $this->close_button_url,
- 'close_button_attributes' => $this->close_button_attributes,
-
- 'tag' => $this->tag,
- ];
-
- $this->session->$type('alert_message', $this->alert);
-
- return $this;
- }
-
- protected function alert()
- {
- return $this->session->get('alert_message');
- }
-
- public function __call($method, $param)
- {
- $alert = $this->alert();
- if (is_array($alert) && property_exists($this, $method) && isset($alert[$method])) {
- return $alert[$method];
- }
- }
-
- //Actions
- public function persist()
- {
- return $this->flash($type = 'put');
- }
-
- public function destroy()
- {
- return $this->flash($type = 'pull'); //retrieve an item and forget it
- }
-
- public function unClosable()
- {
- $this->closable = false;
- $this->un_closable = true;
-
- return $this->flash();
- }
-
- public function unClosableStrict()
- {
- $this->closable = false;
- $this->un_closable = true;
- $this->un_closable_strict = true;
-
- return $this->flash();
- }
-
- public function selfDestroy()
- {
- $this->self_destroy = true;
-
- return $this->flash();
- }
-
- public function setIcon($icon = '')
- {
- $this->icon = $icon;
-
- return $this->flash();
- }
-
- public function setActionButton($label = '', $url = '')
- {
- $this->action_button_label = $label;
- $this->action_button_url = $url;
-
- return $this->flash();
- }
-
- public function setCloseButton($label = '', $url = '', $attributes = [])
- {
- $this->close_button_label = $label;
- $this->close_button_url = $url;
- $this->close_button_attributes = $attributes;
-
- return $this->flash();
- }
-
- public function large()
- {
- $this->modal_size = 'modal-lg';
-
- return $this->flash();
- }
-
- public function small()
- {
- $this->modal_size = 'modal-sm';
-
- return $this->flash();
- }
-
- //Alert status
- public function success($class_name = 'success')
- {
- $this->status = $class_name;
- $this->icon = 'fa fa-check-circle';
-
- return $this->flash();
- }
-
- public function info($class_name = 'info')
- {
- $this->status = $class_name;
- $this->icon = 'fa fa-info';
-
- return $this->flash();
- }
-
- public function warning($class_name = 'warning')
- {
- $this->status = $class_name;
- $this->icon = 'fa fa-warning';
-
- return $this->flash();
- }
-
- public function error($class_name = 'danger')
- {
- $this->status = $class_name;
- $this->icon = 'fa fa-times-circle';
-
- return $this->flash();
- }
-
- public function royal($class_name = 'royal')
- {
- $this->status = $class_name;
- $this->icon = 'fa fa-bullhorn';
-
- return $this->flash();
- }
-
- public function primary($class_name = 'primary')
- {
- $this->status = $class_name;
- $this->icon = 'fa fa-comments-o';
-
- return $this->flash();
- }
-
- //Alert Types
- public function modal($message, $title = '', $view = '')
- {
- $this->type = 'alert_modal_message';
- $this->title = $title;
- $this->message = $message;
- $this->modal_view = $view;
-
- return $this->flash();
- }
-
- public function form($message, $title = '')
- {
- $this->type = 'alert_form_message';
- $this->title = $title;
- $this->message = $message;
-
- return $this->flash();
- }
-
- public function notify($message, $title = '')
- {
- $this->type = 'alert_notify_message';
- $this->title = $title;
- $this->message = $message;
-
- return $this->flash();
- }
-
- public function sticky($message, $title = '')
- {
- $this->type = 'alert_sticky_message';
- $this->sticky_title = $title;
- $this->sticky_message = $message;
-
- return $this->flash();
- }
-
- /**
- * Check if given alert exists.
- *
- * @param $alert_type
- *
- * @return bool
- */
- public function has($alert_type)
- {
- $alert = $this->alert();
-
- $alert_type = "alert_{$alert_type}_message";
-
- return is_array($alert) ? isset($alert[$alert_type]) : false;
- }
-
- public function tag($name)
- {
- $this->tag = $name;
-
- return $this->flash();
- }
-
- /**
- * Tag an alert.
- *
- * @param $name
- *
- * @return bool
- */
- public function tagged($name)
- {
- $alert = $this->alert();
-
- return $alert['tag'] == $name;
- }
-
- /**
- * Check if alert for success.
- *
- * @return bool
- */
- public function hasSuccess()
- {
- return $this->message && $this->status == 'success';
- }
-
- /**
- * Check if alert for error.
- *
- * @return bool
- */
- public function hasError()
- {
- return $this->message && $this->status == 'error';
- }
-
- /**
- * Check if alert for info.
- *
- * @return bool
- */
- public function hasInfo()
- {
- return $this->message && $this->status == 'info';
- }
-}
diff --git a/src/Digitlimit/Alert/AlertServiceProvider.php b/src/Digitlimit/Alert/AlertServiceProvider.php
deleted file mode 100644
index 6ca3d138..00000000
--- a/src/Digitlimit/Alert/AlertServiceProvider.php
+++ /dev/null
@@ -1,73 +0,0 @@
-app->singleton('digitlimit.alert', function ($app) {
- return new Alert($app['session.store']);
- });
- }
-
- /**
- * Bootstrap the application events.
- *
- * @return void
- */
- public function boot()
- {
- $this->loadViewsFrom(__DIR__.'/views', 'alert');
- $this->publishes([
- __DIR__.'/views' => base_path('resources/views/vendor/alert'),
- ]);
-
- Blade::directive('alertHasSuccess', function () {
- return "";
- });
-
- Blade::directive('endAlertHasSuccess', function () {
- return '';
- });
-
- Blade::directive('alertHasNoSuccess', function () {
- return "";
- });
-
- Blade::directive('endAlertHasNoSuccess', function () {
- return '';
- });
-
- Blade::directive('alertHasError', function () {
- return "";
- });
-
- Blade::directive('endAlertHasError', function () {
- return '';
- });
-
- Blade::directive('alertHasNoError', function () {
- return "";
- });
-
- Blade::directive('endAlertHasNoError', function () {
- return '';
- });
- }
-}
diff --git a/src/Digitlimit/Alert/views/field.blade.php b/src/Digitlimit/Alert/views/field.blade.php
deleted file mode 100644
index 396c8241..00000000
--- a/src/Digitlimit/Alert/views/field.blade.php
+++ /dev/null
@@ -1,9 +0,0 @@
-@if($errors->has($field))
-
- @if(isset($tag) && $tag)
- {{ $errors->${$tag}->first($field) }}
- @else
- {{ $errors->first($field) }}
- @endif
-
-@endif
\ No newline at end of file
diff --git a/src/Digitlimit/Alert/views/form.blade.php b/src/Digitlimit/Alert/views/form.blade.php
deleted file mode 100644
index 7b682311..00000000
--- a/src/Digitlimit/Alert/views/form.blade.php
+++ /dev/null
@@ -1,14 +0,0 @@
-@if(Alert::has('form'))
-
-
- @if(Alert::icon()) @endif
-
- @if(Alert::title()){{Alert::title()}} @endif {{Alert::message()}}
-
-
- ×
-
-
-@endif
diff --git a/src/Digitlimit/Alert/views/modal.blade.php b/src/Digitlimit/Alert/views/modal.blade.php
deleted file mode 100644
index a9465e9c..00000000
--- a/src/Digitlimit/Alert/views/modal.blade.php
+++ /dev/null
@@ -1,44 +0,0 @@
-@if ( Alert::has('modal') )
-
-
-
-
-
-
-
- {{Alert::message()}}
-
-
-
-
-
-
-
-
-@endif
\ No newline at end of file
diff --git a/src/Digitlimit/Alert/views/notify.blade.php b/src/Digitlimit/Alert/views/notify.blade.php
deleted file mode 100644
index ccebecc1..00000000
--- a/src/Digitlimit/Alert/views/notify.blade.php
+++ /dev/null
@@ -1,11 +0,0 @@
-@if(Alert::has('notify'))
-
-
- @if(Alert::title()){{Alert::title()}} @endif {{Alert::message()}}
-
-
- ×
-
-
-@endif
\ No newline at end of file
diff --git a/src/Digitlimit/Alert/views/sticky.blade.php b/src/Digitlimit/Alert/views/sticky.blade.php
deleted file mode 100644
index 85076de7..00000000
--- a/src/Digitlimit/Alert/views/sticky.blade.php
+++ /dev/null
@@ -1,18 +0,0 @@
-@if (Alert::has('sticky'))
-
-
- @if(Alert::closable())
- ×
- @endif
-
- @if(Alert::title())
-
- @if(Alert::icon()) @endif
- {{Alert::title()}}
-
- @endif
-
- {!!Alert::sticky_message()!!}
-
-
-@endif
\ No newline at end of file
diff --git a/src/Digitlimit/Alert/Facades/Alert.php b/src/Facades/Alert.php
similarity index 56%
rename from src/Digitlimit/Alert/Facades/Alert.php
rename to src/Facades/Alert.php
index 9c5b1c0d..bd65eb84 100644
--- a/src/Digitlimit/Alert/Facades/Alert.php
+++ b/src/Facades/Alert.php
@@ -7,12 +7,12 @@
class Alert extends Facade
{
/**
- * Get the binding in the IoC container.
+ * Get the registered name of the component.
*
* @return string
*/
- protected static function getFacadeAccessor()
+ protected static function getFacadeAccessor(): string
{
- return 'digitlimit.alert';
+ return 'alert';
}
}
diff --git a/src/Helpers.php b/src/Helpers.php
new file mode 100644
index 00000000..84b57926
--- /dev/null
+++ b/src/Helpers.php
@@ -0,0 +1,20 @@
+message($message)
+ ->title($title)
+ ->flash();
+ }
+
+ return $alert;
+ }
+}
diff --git a/src/Helpers/Attribute.php b/src/Helpers/Attribute.php
new file mode 100644
index 00000000..29d85d0c
--- /dev/null
+++ b/src/Helpers/Attribute.php
@@ -0,0 +1,21 @@
+get('alert.types');
+ }
+
+ /**
+ * Get the prefixed type for an alert type.
+ */
+ public static function prefixed(string $type): string
+ {
+ return self::PREFIX.$type;
+ }
+
+ /**
+ * Check if an alert type exists.
+ */
+ public static function exists(string $type): bool
+ {
+ $types = self::types();
+
+ return isset($types[$type]) || isset($types[self::prefixed($type)]);
+ }
+
+ /**
+ * Get an alert type.
+ */
+ public static function type(string $type): array
+ {
+ $types = self::types();
+
+ return $types[$type] ?? $types[self::prefixed($type)];
+ }
+
+ /**
+ * Get alert class name.
+ */
+ public static function clasname(string $type): string
+ {
+ if (!self::exists($type)) {
+ throw new Exception("The alert type '$type' does not exist in config");
+ }
+
+ return self::type($type)['alert'];
+ }
+}
diff --git a/src/Message/AbstractMessage.php b/src/Message/AbstractMessage.php
new file mode 100644
index 00000000..ae2b7e1f
--- /dev/null
+++ b/src/Message/AbstractMessage.php
@@ -0,0 +1,122 @@
+id = $id;
+
+ return $this;
+ }
+
+ /**
+ * Set alert level.
+ */
+ public function level(string $level): self
+ {
+ $this->level = $level;
+
+ return $this;
+ }
+
+ /**
+ * Set alert message.
+ */
+ public function message(string $message): self
+ {
+ $this->message = $message;
+
+ return $this;
+ }
+
+ /**
+ * Set the alert title.
+ */
+ public function title(string $title): self
+ {
+ $this->title = $title;
+
+ return $this;
+ }
+
+ /**
+ * Set the alert tag.
+ */
+ public function tag(string $tag): self
+ {
+ $this->tag = $tag;
+
+ return $this;
+ }
+
+ /**
+ * Fetch the alert tag.
+ */
+ public function getTag(): string
+ {
+ return $this->tag;
+ }
+
+ /**
+ * Flash alert to store.
+ * Its a temporal store that is deleted once pulled/fetched.
+ */
+ public function flash(string $message = null, string $level = null): void
+ {
+ $this->message = $message ?? $this->message;
+ $this->level = $level ?? $this->level;
+
+ $sessionKey = SessionKey::key($this->key(), $this->getTag());
+ $this->session->flash($sessionKey, $this);
+ }
+}
diff --git a/src/Message/MessageFactory.php b/src/Message/MessageFactory.php
new file mode 100644
index 00000000..17123115
--- /dev/null
+++ b/src/Message/MessageFactory.php
@@ -0,0 +1,24 @@
+store = $store;
+ }
+
+ /**
+ * Flash alert to store.
+ */
+ public function flash(string $key, mixed $value): void
+ {
+ $this->store->flash($key, $value);
+ }
+
+ /**
+ * Fetch alert from the store.
+ */
+ public function get(string $key, mixed $default = null): mixed
+ {
+ return $this->store->get($key, $default);
+ }
+}
diff --git a/src/SessionInterface.php b/src/SessionInterface.php
new file mode 100644
index 00000000..2c84cb06
--- /dev/null
+++ b/src/SessionInterface.php
@@ -0,0 +1,16 @@
+level = 'primary';
+
+ return $this;
+ }
+
+ /**
+ * Secondary alert level.
+ */
+ public function secondary(): self
+ {
+ $this->level = 'secondary';
+
+ return $this;
+ }
+
+ /**
+ * Success alert level.
+ */
+ public function success(): self
+ {
+ $this->level = 'success';
+
+ return $this;
+ }
+
+ /**
+ * Info alert level.
+ */
+ public function info(): self
+ {
+ $this->level = 'info';
+
+ return $this;
+ }
+
+ /**
+ * Error alert level.
+ */
+ public function error(): self
+ {
+ $this->level = 'danger';
+
+ return $this;
+ }
+
+ /**
+ * Warning alert level.
+ */
+ public function warning(): self
+ {
+ $this->level = 'warning';
+
+ return $this;
+ }
+
+ /**
+ * Light alert level.
+ */
+ public function light(): self
+ {
+ $this->level = 'light';
+
+ return $this;
+ }
+
+ /**
+ * Dark alert level.
+ */
+ public function dark(): self
+ {
+ $this->level = 'dark';
+
+ return $this;
+ }
+}
diff --git a/src/Types/Field.php b/src/Types/Field.php
new file mode 100644
index 00000000..fc0ee84e
--- /dev/null
+++ b/src/Types/Field.php
@@ -0,0 +1,115 @@
+id(Helper::randomString());
+ $this->messages = new MessageBag();
+ }
+
+ /**
+ * Message store key for the field alert.
+ */
+ public function key(): string
+ {
+ return 'field';
+ }
+
+ /**
+ * Set field name.
+ */
+ public function name(string $name): self
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * Set messages.
+ */
+ public function messages(MessageBag $messages): self
+ {
+ $this->messages = $messages;
+
+ return $this;
+ }
+
+ /**
+ * Set errors.
+ */
+ public function errors(Validator $validator): self
+ {
+ $this->messages = $validator->errors();
+
+ return $this;
+ }
+
+ /**
+ * Fetch message for a given field name.
+ */
+ public function messageFor(string $name): string
+ {
+ return $this->messages->first($name);
+ }
+
+ /**
+ * Get field store tag.
+ */
+ public function getTag(): string
+ {
+ if ($this->name) {
+ //e.g default.firstname
+ return $this->tag.'.'.$this->name;
+ }
+
+ return $this->tag;
+ }
+
+ /**
+ * Flash field instance to store.
+ */
+ public function flash(string $message = null, string $level = null): void
+ {
+ $this->message = $message ?? $this->message;
+ $this->level = $level ?? $this->level;
+
+ if ($this->messages->isEmpty() && empty($this->name)) {
+ throw new Exception(
+ 'Messages or field name is required. Hint: messages($validator) or name($name)'
+ );
+ }
+
+ $sessionKey = SessionKey::key($this->key(), $this->getTag());
+ $this->session->flash($sessionKey, $this);
+ }
+}
diff --git a/src/Types/Modal.php b/src/Types/Modal.php
new file mode 100644
index 00000000..f4757a0b
--- /dev/null
+++ b/src/Types/Modal.php
@@ -0,0 +1,166 @@
+id(Helper::randomString());
+ $this->action = new Button();
+ $this->cancel = new Button();
+ }
+
+ /**
+ * Message store key for the modal alert.
+ */
+ public function key(): string
+ {
+ return 'modal';
+ }
+
+ /**
+ * Set the action button.
+ */
+ public function action(string $label, string $link = null, array $attributes = []): self
+ {
+ $this->action = new Button($label, $link, $attributes);
+
+ return $this;
+ }
+
+ /**
+ * Set the cancel button.
+ */
+ public function cancel(string $label, string $link = null, array $attributes = []): self
+ {
+ $this->cancel = new Button($label, $link, $attributes);
+
+ return $this;
+ }
+
+ /**
+ * Set modal to scrollable.
+ */
+ public function scrollable(
+ string $class = 'modal-dialog-scrollable'
+ ): self {
+ $this->scrollable = $class;
+
+ return $this;
+ }
+
+ /**
+ * Set modal size to small.
+ */
+ public function small(string $class = 'modal-sm'): self
+ {
+ $this->size = $class;
+
+ return $this;
+ }
+
+ /**
+ * Set modal size to large.
+ */
+ public function large(string $class = 'modal-lg'): self
+ {
+ $this->size = $class;
+
+ return $this;
+ }
+
+ /**
+ * Set modal size to extra-large.
+ */
+ public function extraLarge(string $class = 'modal-xl'): self
+ {
+ $this->size = $class;
+
+ return $this;
+ }
+
+ /**
+ * Set modal size to fullscreen.
+ */
+ public function fullscreen(string $class = 'modal-fullscreen'): self
+ {
+ $this->size = $class;
+
+ return $this;
+ }
+
+ /**
+ * Set modal position to center.
+ */
+ public function centered(string $class = 'modal-dialog-centered'): self
+ {
+ $this->position = $class;
+
+ return $this;
+ }
+
+ /**
+ * Set a view for the modal.
+ */
+ public function view(View $view): self
+ {
+ $this->view = $view->render();
+
+ return $this;
+ }
+
+ /**
+ * Set HTML string for the modal.
+ */
+ public function html(string $html): self
+ {
+ $this->view = $html;
+
+ return $this;
+ }
+}
diff --git a/src/Types/Normal.php b/src/Types/Normal.php
new file mode 100644
index 00000000..1be09a54
--- /dev/null
+++ b/src/Types/Normal.php
@@ -0,0 +1,31 @@
+id(Helper::randomString());
+ }
+
+ /**
+ * Message store key for the normal alert.
+ */
+ public function key(): string
+ {
+ return 'normal';
+ }
+}
diff --git a/src/Types/Notify.php b/src/Types/Notify.php
new file mode 100644
index 00000000..1b61a23f
--- /dev/null
+++ b/src/Types/Notify.php
@@ -0,0 +1,107 @@
+id(Helper::randomString());
+ $this->bottomRight();
+ }
+
+ /**
+ * Message store key for the notify alert.
+ */
+ public function key(): string
+ {
+ return 'notify';
+ }
+
+ /**
+ * Position notify on center of the screen.
+ */
+ public function centered(string $class = 'top-50 start-50 translate-middle'): self
+ {
+ $this->position = $class;
+
+ return $this;
+ }
+
+ /**
+ * Position notify on the center of the screen.
+ */
+ public function topLeft(string $class = 'top-0 start-0'): self
+ {
+ $this->position = $class;
+
+ return $this;
+ }
+
+ /**
+ * Position notify on the top right of the screen.
+ */
+ public function topRight(string $class = 'top-0 end-0'): self
+ {
+ $this->position = $class;
+
+ return $this;
+ }
+
+ /**
+ * Position notify on the top center of the screen.
+ */
+ public function topCenter(string $class = 'top-0 start-50 translate-middle-x'): self
+ {
+ $this->position = $class;
+
+ return $this;
+ }
+
+ /**
+ * Position notify on the bottom left of the screen.
+ */
+ public function bottomLeft(string $class = 'bottom-0 start-0'): self
+ {
+ $this->position = $class;
+
+ return $this;
+ }
+
+ /**
+ * Position notify on the bottom right of the screen.
+ */
+ public function bottomRight(string $class = 'bottom-0 end-0'): self
+ {
+ $this->position = $class;
+
+ return $this;
+ }
+
+ /**
+ * Position notify on the bottom center of the screen.
+ */
+ public function bottomCenter(string $class = 'bottom-0 start-50 translate-middle-x'): self
+ {
+ $this->position = $class;
+
+ return $this;
+ }
+}
diff --git a/src/Types/Sticky.php b/src/Types/Sticky.php
new file mode 100644
index 00000000..78cfbbbd
--- /dev/null
+++ b/src/Types/Sticky.php
@@ -0,0 +1,48 @@
+id(Helper::randomString());
+ $this->action = new Button();
+ }
+
+ /**
+ * Message store key for the sticky alert.
+ */
+ public function key(): string
+ {
+ return 'sticky';
+ }
+
+ /**
+ * Set the action button.
+ */
+ public function action(string $label, string $link = null, array $attributes = []): self
+ {
+ $this->action = new Button($label, $link, $attributes);
+
+ return $this;
+ }
+}
diff --git a/src/View/Components/Field.php b/src/View/Components/Field.php
new file mode 100644
index 00000000..e8b8c43d
--- /dev/null
+++ b/src/View/Components/Field.php
@@ -0,0 +1,37 @@
+alert = $alert;
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ */
+ public function render(): View|Closure|string
+ {
+ return view('alert::components.field');
+ }
+}
diff --git a/src/View/Components/Modal.php b/src/View/Components/Modal.php
new file mode 100644
index 00000000..c7bb8222
--- /dev/null
+++ b/src/View/Components/Modal.php
@@ -0,0 +1,81 @@
+ 'button',
+ 'class' => 'btn btn-primary',
+ ];
+
+ /**
+ * Default cancel button attributes.
+ */
+ public array $cancelAttributes = [
+ 'type' => 'button',
+ 'class' => 'btn btn-secondary',
+ 'data-bs-dismiss' => 'modal',
+ ];
+
+ /**
+ * Create a new component instance.
+ */
+ public function __construct(Alert $alert)
+ {
+ $this->alert = $alert;
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ */
+ public function render(): View|Closure|string
+ {
+ return view('alert::components.modal');
+ }
+
+ /**
+ * Merge and convert array attributes to HTML string attributes.
+ */
+ public function actionAttributes(array $attributes): string
+ {
+ $newAttributes = array_merge(
+ $this->actionAttributes,
+ $attributes
+ );
+
+ return Attribute::toString($newAttributes);
+ }
+
+ /**
+ * Merge and convert array attributes to HTML string attributes.
+ */
+ public function cancelAttributes(array $attributes): string
+ {
+ $newAttributes = array_merge(
+ $this->cancelAttributes,
+ $attributes
+ );
+
+ return Attribute::toString($newAttributes);
+ }
+}
diff --git a/src/View/Components/Normal.php b/src/View/Components/Normal.php
new file mode 100644
index 00000000..3baffde5
--- /dev/null
+++ b/src/View/Components/Normal.php
@@ -0,0 +1,37 @@
+alert = $alert;
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ */
+ public function render(): View|Closure|string
+ {
+ return view('alert::components.normal');
+ }
+}
diff --git a/src/View/Components/Notify.php b/src/View/Components/Notify.php
new file mode 100644
index 00000000..c552ae48
--- /dev/null
+++ b/src/View/Components/Notify.php
@@ -0,0 +1,37 @@
+alert = $alert;
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ */
+ public function render(): View|Closure|string
+ {
+ return view('alert::components.notify');
+ }
+}
diff --git a/src/View/Components/Sticky.php b/src/View/Components/Sticky.php
new file mode 100644
index 00000000..b1349f79
--- /dev/null
+++ b/src/View/Components/Sticky.php
@@ -0,0 +1,59 @@
+ 'button',
+ 'class' => 'btn btn-sm btn-primary float-end',
+ ];
+
+ /**
+ * Create a new component instance.
+ */
+ public function __construct(Alert $alert)
+ {
+ $this->alert = $alert;
+ }
+
+ /**
+ * Get the view / contents that represent the component.
+ */
+ public function render(): View|Closure|string
+ {
+ return view('alert::components.sticky');
+ }
+
+ /**
+ * Merge and convert array attributes to HTML string attributes.
+ */
+ public function actionAttributes(array $attributes): string
+ {
+ $newAttributes = array_merge(
+ $this->actionAttributes,
+ $attributes
+ );
+
+ return Attribute::toString($newAttributes);
+ }
+}
diff --git a/tests/.gitkeep b/tests/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/AlertTest.php b/tests/AlertTest.php
deleted file mode 100644
index 24a47025..00000000
--- a/tests/AlertTest.php
+++ /dev/null
@@ -1,12 +0,0 @@
- 'btn-sm']);
+
+ expect($button->label)->toEqual('Submit');
+ expect($button->link)->toEqual('/about');
+ expect($button->attributes)->toEqual(['class' => 'btn-sm']);
+
+ $button->label('Register');
+ $button->link('/register');
+ $button->attributes(['id' => 'register']);
+
+ expect($button->label)->toEqual('Register');
+ expect($button->link)->toEqual('/register');
+ expect($button->attributes)->toEqual(['id' => 'register']);
+})->name('component', 'component-button');
diff --git a/tests/Feature/Components/FieldTest.php b/tests/Feature/Components/FieldTest.php
new file mode 100644
index 00000000..1cfa7e81
--- /dev/null
+++ b/tests/Feature/Components/FieldTest.php
@@ -0,0 +1,80 @@
+success()
+ ->name('username')
+ ->flash();
+
+ $view = $this
+ ->blade(' ');
+
+ $view
+ ->assertSee('class="form-text text-success"', false)
+ ->assertSee('Username is available');
+})->name('view-component', 'view-component-field-default');
+
+it('can render a tagged field view component', function () {
+ Alert::field('Please select a country')
+ ->tag('contact')
+ ->name('country')
+ ->warning()
+ ->flash();
+
+ $view = $this
+ ->blade(' ');
+
+ $view
+ ->assertSee('class="form-text text-warning"', false)
+ ->assertSee('Please select a country');
+})->name('view-component', 'view-component-field-tagged');
+
+it('can render a named field view component', function () {
+ Alert::field('Good, you chose a valid country')
+ ->tag('contact')
+ ->name('country')
+ ->success()
+ ->flash();
+
+ Alert::field('Good, you chose a valid state')
+ ->tag('contact')
+ ->name('state')
+ ->success()
+ ->flash();
+
+ $view = $this
+ ->blade(' ');
+
+ $view
+ ->assertSee('class="form-text text-success"', false)
+ ->assertSee('Good, you chose a valid country')
+ ->assertDontSee('Good, you chose a valid state');
+})->name('view-component', 'view-component-field-tagged');
+
+it('can render validation errors for the field view component', function () {
+ $validator = validator(
+ ['firstname' => '', 'lastname' => ''],
+ [
+ 'firstname' => 'required',
+ 'lastname' => 'required',
+ ]
+ );
+
+ Alert::field()
+ ->tag('contact')
+ ->errors($validator)
+ ->error()
+ ->flash();
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="form-text text-danger"', false)
+ ->assertSee('The firstname field is required');
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="form-text text-danger"', false)
+ ->assertSee('The lastname field is required');
+})->name('view-component', 'view-component-field-tagged');
diff --git a/tests/Feature/Components/ModalTest.php b/tests/Feature/Components/ModalTest.php
new file mode 100644
index 00000000..4629c878
--- /dev/null
+++ b/tests/Feature/Components/ModalTest.php
@@ -0,0 +1,79 @@
+flash();
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="modal"', false)
+ ->assertSee('Than you for joining us');
+})->name('view-component', 'view-component-modal-default');
+
+it('can render a default modal with buttons and title', function () {
+ Alert::modal('Your message has been recieved, you will hear from us soon')
+ ->action('Yes')
+ ->cancel('Cancel')
+ ->centered()
+ ->title('Please login')
+ ->error()
+ ->flash();
+
+ $view = $this
+ ->blade(' ');
+
+ $view
+ ->assertSee('class="modal"', false)
+ ->assertSee('Yes')
+ ->assertSee('Cancel')
+ ->assertSee('Please login')
+ ->assertSee('Your message has been recieved, you will hear from us soon');
+})->name('view-component', 'view-component-modal-buttons-title');
+
+it('can render a default modal a the right position', function () {
+ Alert::modal()
+ ->centered('centered')
+ ->flash();
+
+ $view = $this
+ ->blade(' ');
+
+ $view
+ ->assertSee('class="modal-dialog centered "', false);
+})->name('view-component', 'view-component-modal-position');
+
+it('can render a default modal a the right size', function () {
+ Alert::modal()
+ ->small()
+ ->flash();
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="modal-dialog modal-sm "', false);
+
+ Alert::modal()
+ ->large()
+ ->flash();
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="modal-dialog modal-lg "', false);
+
+ Alert::modal()
+ ->extraLarge()
+ ->flash();
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="modal-dialog modal-xl "', false);
+
+ Alert::modal()
+ ->fullscreen()
+ ->flash();
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="modal-dialog modal-fullscreen "', false);
+})->name('view-component', 'view-component-modal-size');
diff --git a/tests/Feature/Components/NormalTest.php b/tests/Feature/Components/NormalTest.php
new file mode 100644
index 00000000..be884498
--- /dev/null
+++ b/tests/Feature/Components/NormalTest.php
@@ -0,0 +1,13 @@
+flash();
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="alert alert-dismissible alert-"', false)
+ ->assertSee('Thank you for joining us');
+})->name('view-component', 'view-component-normal-default');
diff --git a/tests/Feature/Components/NotifyTest.php b/tests/Feature/Components/NotifyTest.php
new file mode 100644
index 00000000..a8143662
--- /dev/null
+++ b/tests/Feature/Components/NotifyTest.php
@@ -0,0 +1 @@
+flash();
+
+ $this
+ ->blade(' ')
+ ->assertSee('class="alert alert-"', false)
+ ->assertSee('Thank you for joining us');
+})->name('view-component', 'view-component-sticky-default');
diff --git a/tests/Feature/Message/MessageInterfaceTest.php b/tests/Feature/Message/MessageInterfaceTest.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/tests/Feature/Message/MessageInterfaceTest.php
@@ -0,0 +1 @@
+primary();
+ expect($alert->level)->toEqual('primary');
+
+ $alert = Alert::message('Thank you!')->secondary();
+ expect($alert->level)->toEqual('secondary');
+
+ $alert = Alert::message('Thank you!')->success();
+ expect($alert->level)->toEqual('success');
+
+ $alert = Alert::message('Thank you!')->info();
+ expect($alert->level)->toEqual('info');
+
+ $alert = Alert::message('Thank you!')->error();
+ expect($alert->level)->toEqual('danger');
+
+ $alert = Alert::message('Thank you!')->warning();
+ expect($alert->level)->toEqual('warning');
+
+ $alert = Alert::message('Thank you!')->light();
+ expect($alert->level)->toEqual('light');
+
+ $alert = Alert::message('Thank you!')->dark();
+ expect($alert->level)->toEqual('dark');
+})->name('traits', 'traits-levelable');
diff --git a/tests/Feature/Types/FieldTest.php b/tests/Feature/Types/FieldTest.php
new file mode 100644
index 00000000..8eea10b0
--- /dev/null
+++ b/tests/Feature/Types/FieldTest.php
@@ -0,0 +1,17 @@
+name('firstname')
+ ->flash();
+
+ $default = Alert::named('field', 'firstname');
+
+ expect($default)->toBeInstanceOf(MessageInterface::class);
+ expect($default)->toBeInstanceOf(Field::class);
+ expect($default->message)->toEqual('Invalid firstname');
+})->name('types', 'types-field', 'types-field');
diff --git a/tests/Feature/Types/ModalTest.php b/tests/Feature/Types/ModalTest.php
new file mode 100644
index 00000000..249809da
--- /dev/null
+++ b/tests/Feature/Types/ModalTest.php
@@ -0,0 +1,15 @@
+flash();
+
+ $default = Alert::default('modal');
+
+ expect($default)->toBeInstanceOf(MessageInterface::class);
+ expect($default)->toBeInstanceOf(Modal::class);
+ expect($default->message)->toEqual('Thank you!');
+})->name('types', 'types-modal', 'types-modal');
diff --git a/tests/Feature/Types/NormalTest.php b/tests/Feature/Types/NormalTest.php
new file mode 100644
index 00000000..d4cd807c
--- /dev/null
+++ b/tests/Feature/Types/NormalTest.php
@@ -0,0 +1,15 @@
+flash();
+
+ $default = Alert::default('normal');
+
+ expect($default)->toBeInstanceOf(MessageInterface::class);
+ expect($default)->toBeInstanceOf(Normal::class);
+ expect($default->message)->toEqual('Thank you!');
+})->name('types', 'types-default', 'types-normal');
diff --git a/tests/Feature/Types/NotifyTest.php b/tests/Feature/Types/NotifyTest.php
new file mode 100644
index 00000000..66e658bc
--- /dev/null
+++ b/tests/Feature/Types/NotifyTest.php
@@ -0,0 +1,15 @@
+flash();
+
+ $default = Alert::default('notify');
+
+ expect($default)->toBeInstanceOf(MessageInterface::class);
+ expect($default)->toBeInstanceOf(Notify::class);
+ expect($default->message)->toEqual('Thank you!');
+})->name('types', 'types-notify', 'types-notify');
diff --git a/tests/Feature/Types/StickyTest.php b/tests/Feature/Types/StickyTest.php
new file mode 100644
index 00000000..0e6b452a
--- /dev/null
+++ b/tests/Feature/Types/StickyTest.php
@@ -0,0 +1,15 @@
+flash();
+
+ $default = Alert::default('sticky');
+
+ expect($default)->toBeInstanceOf(MessageInterface::class);
+ expect($default)->toBeInstanceOf(Sticky::class);
+ expect($default->message)->toEqual('Thank you!');
+})->name('types', 'types-sticky', 'types-sticky');
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 00000000..baff53e7
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,42 @@
+in('Feature');
+
+/*
+|--------------------------------------------------------------------------
+| Expectations
+|--------------------------------------------------------------------------
+|
+| When you're writing tests, you often need to check that values meet certain conditions. The
+| "expect()" function gives you access to a set of "expectations" methods that you can use
+| to assert different things. Of course, you may extend the Expectation API at any time.
+|
+*/
+
+expect()->extend('toBeOne', function () {
+ return $this->toBe(1);
+});
+
+/*
+|--------------------------------------------------------------------------
+| Functions
+|--------------------------------------------------------------------------
+|
+| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
+| project that you don't want to repeat in every file. Here you can also expose helpers as
+| global functions to help you to reduce the number of lines of code in your test files.
+|
+*/
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 00000000..9ae3ca2c
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,43 @@
+>
+ */
+ protected function getPackageAliases($app)
+ {
+ return [
+
+ ];
+ }
+
+ protected function getEnvironmentSetUp($app)
+ {
+ // perform environment setup
+ }
+
+ public function packagePath(string $path = '')
+ {
+ return __DIR__.'/../'.$path;
+ }
+}
diff --git a/tests/Unit/Helpers/AttributeTest.php b/tests/Unit/Helpers/AttributeTest.php
new file mode 100644
index 00000000..be43bc79
--- /dev/null
+++ b/tests/Unit/Helpers/AttributeTest.php
@@ -0,0 +1,13 @@
+ 'alert-id',
+ 'class' => 'alert alert-success',
+ ]);
+
+ expect($attributesString)
+ ->toEqual('id="alert-id" class="alert alert-success"');
+})->name('helpers', 'helpers-to-string');
diff --git a/tests/Unit/Helpers/SessionKeyTest.php b/tests/Unit/Helpers/SessionKeyTest.php
new file mode 100644
index 00000000..38740bf1
--- /dev/null
+++ b/tests/Unit/Helpers/SessionKeyTest.php
@@ -0,0 +1,10 @@
+toEqual(SessionKey::MAIN_KEY.'.notify.contact-form-1');
+})->name('helpers', 'helpers-session-key');
diff --git a/tests/Unit/Helpers/TypeTest.php b/tests/Unit/Helpers/TypeTest.php
new file mode 100644
index 00000000..b3d9bbc7
--- /dev/null
+++ b/tests/Unit/Helpers/TypeTest.php
@@ -0,0 +1 @@
+