├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── preview-classic.png ├── preview-dark-mode.png └── src ├── Controllers ├── TodoBarController.php ├── TodoBarProjects.php └── TodoBarTasks.php ├── Facade └── TodoBar.php ├── Middleware └── TodoBarMiddleware.php ├── Storage ├── DataStorageInterface.php └── JSONStorage.php ├── TodoBarMiddleware.php ├── TodoBarServiceProvider.php ├── assets ├── loader.gif ├── todobar-dark.css ├── todobar.css └── todobar.js ├── config └── todobar.php ├── resources └── views │ ├── partials │ ├── form.blade.php │ ├── handle.blade.php │ ├── projects.blade.php │ └── tasks.blade.php │ └── todobar.blade.php └── routes └── web.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | composer.lock 3 | phpunit.xml 4 | node_modules/ 5 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - "7.2" 4 | before_script: 5 | - composer self-update 6 | - composer install --prefer-source 7 | 8 | script: 9 | - ./vendor/bin/phpcs --standard=PSR2 src 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jefferson Ochoa and contributors 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 | # Laravel TodoBar 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/tpaksu/laravel-todobar/version)](https://packagist.org/packages/tpaksu/laravel-todobar) [![Total Downloads](https://poser.pugx.org/tpaksu/laravel-todobar/downloads)](https://packagist.org/packages/tpaksu/laravel-todobar) [![License](https://poser.pugx.org/tpaksu/laravel-todobar/license)](https://packagist.org/packages/tpaksu/laravel-todobar) 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/tpaksu/laravel-todobar.svg?style=flat-square)](https://packagist.org/packages/tpaksu/laravel-todobar) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tpaksu/laravel-todobar/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tpaksu/laravel-todobar/?branch=master) 5 | [![Code Intelligence Status](https://scrutinizer-ci.com/g/tpaksu/laravel-todobar/badges/code-intelligence.svg?b=master)](https://scrutinizer-ci.com/code-intelligence) 6 | [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/tpaksu/laravel-todobar.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/tpaksu/laravel-todobar/context:javascript) 7 | 8 | TodoBar creates an overlay right sidebar to ease your Laravel projects task management. 9 | 10 | - It stores the tasks in a JSON file which is located in `/resources/todobar` folder, which is shared among your code in your repository, so you can get the tasks on every platform that you are developing your project. 11 | 12 | - It supports multiple groups, which you can use to track different aspects of your project in a single file, and you can easily switch between groups with a single dropdown. 13 | 14 | - It uses Bootstrap components as the frontend library, and for the "Edit Task" modal. 15 |

16 | 17 | ## Preview 18 | *Classic Mode* 19 | 20 | ![Classic Mode](preview-classic.png) 21 |

22 | *Dark Mode* 23 | 24 | ![Dark Mode](preview-dark-mode.png) 25 |

26 | 27 | 28 | 29 | ## Installation 30 | You can install the package via composer with: 31 | 32 | ``` 33 | composer require tpaksu/laravel-todobar --dev 34 | ``` 35 | 36 | The sidebar will be enabled by default, but you can disable it by adding 37 | 38 | TODOBAR_ENABLED=false 39 | 40 | to your environment variables and run `php artisan config:cache` to update the configuration cache. 41 |

42 | 43 | 44 | ## Package Contents 45 | 46 | This package publishes the views used in the package and a configuration file which consists of three settings: 47 | 48 | - **enabled**: Enables the sidebar by setting the environment variable (`TODOBAR_ENABLED`) in your `.env` file, or by changing the default value when the environment value is missing. 49 | 50 | - **start_visible**: Defines the visibility on page load, if you set it to true, the sidebar will be shown on page load. 51 | 52 | - **overlay**: Defines if the sidebar will cover some part of the web page (overlay), or shrink the page and display the whole page besides itself. 53 | 54 | The views used and published by this package: 55 | 56 | + resources/views/vendor/tpaksu/todobar 57 | +-- partials 58 | | +-- form.blade.php 59 | | |-- handle.blade.php 60 | | |-- projects.blade.php 61 | | +-- tasks blade.php 62 | +-- todobar.blade.php 63 |

64 | 65 | ## Extending 66 | 67 | The package contains a `Storage` folder which contains an interface `DataStorageInterface` defining the data storage provider repository, and an example `JSONStorage` class which handles the data persisting in JSON file. If you want to use something different than a JSON file to store your tasks, you can create a class implementing `DataStorageInterface` and make the package use that instead by changing the configuration like this: 68 | 69 | ```php 70 | "storage" => [ 71 | "engine" => \App\TodoBar\CustomStorage::class, 72 | "params" => [ 73 | "param" => "[ Passed to your class ]", 74 | ], 75 | ], 76 | ``` 77 | 78 |

79 | 80 | ## Contributing 81 | 82 | You are always welcome to send pull requests to this package. Please describe why and what changed in the code, so I can approve them quickly. 83 |

84 | 85 | ## Security 86 | If you discover any security related issues, please email tpaksu@gmail.com directly instead of using the issue tracker. 87 |

88 | 89 | ## License 90 | The MIT License (MIT). 91 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tpaksu/laravel-todobar", 3 | "description": "A simple todo list drawer for development tasks listing", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Taha Paksu", 9 | "email": "tpaksu@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "TPaksu\\TodoBar\\": "src/" 15 | } 16 | }, 17 | "autoload-dev": { 18 | "psr-4": { 19 | "TPaksu\\TodoBar\\Tests\\": "tests/" 20 | } 21 | }, 22 | "require": {}, 23 | "extra": { 24 | "laravel": { 25 | "providers": [ 26 | "TPaksu\\TodoBar\\TodoBarServiceProvider" 27 | ], 28 | "aliases": { 29 | "TodoBar": "TPaksu\\TodoBar\\Facade" 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /preview-classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaksu/laravel-todobar/3ad955ac047c3cc6ed1e2fd400dd960f6de85016/preview-classic.png -------------------------------------------------------------------------------- /preview-dark-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaksu/laravel-todobar/3ad955ac047c3cc6ed1e2fd400dd960f6de85016/preview-dark-mode.png -------------------------------------------------------------------------------- /src/Controllers/TodoBarController.php: -------------------------------------------------------------------------------- 1 | " . file_get_contents($this->assets_path("todobar.js")) . ""; 13 | } 14 | 15 | public function getDrawer() { 16 | return View::make("laravel-todobar::todobar"); 17 | } 18 | 19 | public function getStyles() { 20 | $dark_mode = config("todobar.dark_mode", false); 21 | $file = "todobar.css"; 22 | if ($dark_mode) { 23 | $file = "todobar-dark.css"; 24 | } 25 | $path = $this->assets_path($file); 26 | return ""; 27 | } 28 | 29 | public function getInjection() 30 | { 31 | return $this->getStyles() . $this->getDrawer() . $this->getScripts(); 32 | } 33 | 34 | public function inject(Response $response) { 35 | $response->setContent(str_replace("", "" . $this->getInjection(), $response->getContent())); 36 | } 37 | 38 | public function assets_path($file) 39 | { 40 | return implode(DIRECTORY_SEPARATOR, [__DIR__, "..", "assets", $file]); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Controllers/TodoBarProjects.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 17 | } 18 | /** 19 | * Display a listing of the resource. 20 | * 21 | * @return \Illuminate\Http\Response 22 | */ 23 | public function index() 24 | { 25 | $data = collect($this->storage->all())->pluck("name"); 26 | return response()->json(["status" => "success", "data" => $data], 200); 27 | } 28 | 29 | /** 30 | * Store a newly created resource in storage. 31 | * 32 | * @param \Illuminate\Http\Request $request 33 | * @return \Illuminate\Http\Response 34 | */ 35 | public function store(Request $request) 36 | { 37 | $new_id = $this->storage->insert([ 38 | "name" => $request->name, 39 | "tasks" => [] 40 | ]); 41 | return response()->json(["status" => "success", "id" => $new_id, "name" => $request->name], 200); 42 | 43 | } 44 | 45 | /** 46 | * Display the specified resource. 47 | * 48 | * @param int $id 49 | * @return \Illuminate\Http\Response 50 | */ 51 | public function show($id) 52 | { 53 | $tasks = $this->storage->find($id) ?? []; 54 | return response()->json(["status" => "success", "tasks" => $tasks], 200); 55 | } 56 | 57 | /** 58 | * Update the specified resource in storage. 59 | * 60 | * @param \Illuminate\Http\Request $request 61 | * @param int $id 62 | * @return \Illuminate\Http\Response 63 | */ 64 | public function update(Request $request, $id) 65 | { 66 | $this->storage->update($id, [ 67 | "name" => $request->name, 68 | ]); 69 | return response()->json(["status" => "success", "id" => $id, "name" => $request->name], 200); 70 | } 71 | 72 | /** 73 | * Remove the specified resource from storage. 74 | * 75 | * @param int $id 76 | * @return \Illuminate\Http\Response 77 | */ 78 | public function destroy($id) 79 | { 80 | $this->storage->delete($id); 81 | return response()->json(["status" => "success"], 200); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Controllers/TodoBarTasks.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 16 | } 17 | /** 18 | * Display a listing of the resource. 19 | * 20 | * @return \Illuminate\Http\Response 21 | */ 22 | public function index(Request $request, $project_id) 23 | { 24 | if ($project_id >= 0) { 25 | $project = $this->storage->find($project_id) ?? null; 26 | if ($project) { 27 | return response()->json([ 28 | "status" => "success", 29 | "project" => $project, 30 | "html" => view()->make('laravel-todobar::partials.tasks', ["tasks" => $project->tasks, "project_id" => $project_id])->render(), 31 | ], 200); 32 | } 33 | return response()->json(["status" => "error", "error" => "Sorry, we can't find the project with that ID."]); 34 | } else { 35 | return response()->json([ 36 | "status" => "success", 37 | "project" => null, 38 | "html" => view()->make('laravel-todobar::partials.tasks')->render(), 39 | ], 200); 40 | } 41 | } 42 | 43 | /** 44 | * Store a newly created resource in storage. 45 | * 46 | * @param \Illuminate\Http\Request $request 47 | * @return \Illuminate\Http\Response 48 | */ 49 | public function store(Request $request, $project_id) 50 | { 51 | $request->validate([ 52 | "content" => "required|string|filled", 53 | ]); 54 | 55 | $project = $this->storage->find($project_id); 56 | 57 | if ($project) { 58 | $project->tasks[] = (object)[ 59 | "content" => $request->content, 60 | "completed" => 0, 61 | ]; 62 | $this->storage->update($project_id, $project); 63 | return response()->json(["status" => "success"], 200); 64 | } else { 65 | return response()->json(["status" => "error", "error" => "We couldn't find the project that you wanted to add that task."], 200); 66 | } 67 | } 68 | 69 | /** 70 | * Update the specified resource in storage. 71 | * 72 | * @param \Illuminate\Http\Request $request 73 | * @param int $id 74 | * @return \Illuminate\Http\Response 75 | */ 76 | public function update(Request $request, $project_id, $task_id) 77 | { 78 | if ($request->has("status")) { 79 | $request->validate([ 80 | "status" => "required|boolean" 81 | ]); 82 | $update = "completed"; 83 | $update_key = "status"; 84 | } else { 85 | $request->validate([ 86 | "content" => "required|string|filled", 87 | ]); 88 | $update = "content"; 89 | $update_key = "content"; 90 | } 91 | 92 | $project = $this->storage->find($project_id); 93 | 94 | if ($project) { 95 | if (isset($project->tasks[$task_id])) { 96 | 97 | $project->tasks[$task_id]->{$update} = $request->get($update_key); 98 | 99 | $this->storage->update($project_id, $project); 100 | return response()->json(["status" => "success"], 200); 101 | } else { 102 | return response()->json(["status" => "error", "error" => "We couldn't find the task that you wanted to update."], 200); 103 | } 104 | } else { 105 | return response()->json(["status" => "error", "error" => "We couldn't find the project that you wanted to add that task."], 200); 106 | } 107 | } 108 | 109 | /** 110 | * Remove the specified resource from storage. 111 | * 112 | * @param int $id 113 | * @return \Illuminate\Http\Response 114 | */ 115 | public function destroy($project_id, $task_id) 116 | { 117 | $project = $this->storage->find($project_id); 118 | 119 | if ($project) { 120 | if (isset($project->tasks[$task_id])) { 121 | unset($project->tasks[$task_id]); 122 | $project->tasks = array_values($project->tasks); 123 | $this->storage->update($project_id, $project); 124 | return response()->json(["status" => "success"], 200); 125 | } else { 126 | return response()->json(["status" => "error", "error" => "We couldn't find the task that you wanted to update."], 200); 127 | } 128 | } else { 129 | return response()->json(["status" => "error", "error" => "We couldn't find the project that you wanted to add that task."], 200); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Facade/TodoBar.php: -------------------------------------------------------------------------------- 1 | container = $container; 21 | $this->todobar = $todobar; 22 | } 23 | 24 | /** 25 | * Handle an incoming request. 26 | * 27 | * @param \Illuminate\Http\Request $request 28 | * @param \Closure $next 29 | * @return mixed 30 | */ 31 | public function handle($request, Closure $next) 32 | { 33 | $response = $next($request); 34 | 35 | if ($response instanceof Response) { 36 | $this->todobar->inject($response); 37 | } 38 | 39 | return $response; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Storage/DataStorageInterface.php: -------------------------------------------------------------------------------- 1 | file = $params["file"]; 16 | $this->client = Storage::createLocalDriver(['root' => resource_path("/todobar")]); 17 | 18 | if ($this->client->exists($this->file) === false) { 19 | $this->client->put($this->file, "{\"projects\": []}"); 20 | } 21 | 22 | $this->json = json_decode($this->client->get($this->file)); 23 | } 24 | 25 | public function all() 26 | { 27 | return $this->json->projects; 28 | } 29 | 30 | public function find($id) 31 | { 32 | return $this->json->projects[$id] ?? null; 33 | } 34 | 35 | public function insert($values) 36 | { 37 | // reorder indexes 38 | $this->json->projects = array_values($this->json->projects); 39 | 40 | // get the new index 41 | $id = count($this->json->projects); 42 | 43 | // store new item 44 | $this->json->projects[$id] = new \stdClass(); 45 | 46 | foreach ($values as $key => $value) { 47 | $this->json->projects[$id]->{$key} = $value; 48 | } 49 | // update the file 50 | $this->updateStorage(); 51 | 52 | // return newly added id 53 | return $id; 54 | } 55 | 56 | public function update($id, $values) 57 | { 58 | if (isset($this->json->projects[$id])) { 59 | foreach ($values as $key => $value) { 60 | $this->json->projects[$id]->{$key} = $value; 61 | } 62 | $this->updateStorage(); 63 | } 64 | } 65 | 66 | public function delete($id) 67 | { 68 | unset($this->json->projects[$id]); 69 | 70 | // reorder indexes 71 | $this->json->projects = array_values($this->json->projects); 72 | 73 | // update the file 74 | $this->updateStorage(); 75 | } 76 | 77 | protected function updateStorage() 78 | { 79 | $this->client->put($this->file, json_encode($this->json)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/TodoBarMiddleware.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaksu/laravel-todobar/3ad955ac047c3cc6ed1e2fd400dd960f6de85016/src/TodoBarMiddleware.php -------------------------------------------------------------------------------- /src/TodoBarServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__ . '/routes/web.php'); 19 | $this->loadViewsFrom(__DIR__ . '/resources/views', 'laravel-todobar'); 20 | $this->mergeConfigFrom(__DIR__ . '/config/todobar.php', "todobar"); 21 | $this->publishes([ 22 | __DIR__ . '/config/todobar.php' => config_path('todobar.php'), 23 | __DIR__ . '/resources/views' => base_path('resources/views/tpaksu/todobar'), 24 | ]); 25 | } 26 | 27 | /** 28 | * Register the application services. 29 | * 30 | * @return void 31 | */ 32 | public function register() 33 | { 34 | if (config('todobar.enabled', false) === true) { 35 | $this->app->make('TPaksu\TodoBar\Controllers\TodoBarController'); 36 | $this->app["router"]->pushMiddlewareToGroup("web", "\TPaksu\TodoBar\Middleware\TodoBarMiddleware"); 37 | } 38 | 39 | $this->app->singleton(DataStorageInterface::class, function() { 40 | $storage = config("todobar.storage.engine", Storage\JSONStorage::class); 41 | $config = config("todobar.storage.params", ["file" => "items.json"]); 42 | if (class_exists($storage)) { 43 | return new $storage($config); 44 | } 45 | return new Storage\JSONStorage(["file" => "items.json"]); 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/assets/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tpaksu/laravel-todobar/3ad955ac047c3cc6ed1e2fd400dd960f6de85016/src/assets/loader.gif -------------------------------------------------------------------------------- /src/assets/todobar-dark.css: -------------------------------------------------------------------------------- 1 | .laravel-todobar { 2 | position: fixed; 3 | right: 0; 4 | top: 0; 5 | bottom: 0; 6 | width: 0; 7 | height: 100vh; 8 | z-index: 100; 9 | } 10 | 11 | .laravel-todobar .laravel-todobar-container { 12 | display: none; 13 | height: 100vh; 14 | flex-direction: column; 15 | } 16 | 17 | html.todobar-active .laravel-todobar .laravel-todobar-container { 18 | display: flex; 19 | } 20 | 21 | .laravel-todobar-handle { 22 | cursor: pointer; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | position: absolute; 27 | top: 57px; 28 | background: #ff2d20; 29 | font-weight: bold; 30 | left: -36px; 31 | padding: 9px; 32 | border-radius: 5px 0 0 5px; 33 | box-shadow: -2px 0 2px -1px rgba(0, 0, 0, 0.4); 34 | z-index: -1; 35 | } 36 | 37 | html.todobar-active .laravel-todobar { 38 | position: fixed; 39 | right: 0; 40 | top: 0; 41 | bottom: 0; 42 | width: 20vw; 43 | padding: 0px; 44 | background: white; 45 | border: 1px solid #ddd; 46 | border-color: #343a40; 47 | box-shadow: -2px 0 2px -1px rgba(0, 0, 0, 0.1); 48 | } 49 | 50 | @media screen and (max-width: 1440px) { 51 | html.todobar-active .laravel-todobar { 52 | width: 30vw; 53 | } 54 | } 55 | 56 | @media screen and (max-width: 1200px) { 57 | html.todobar-active .laravel-todobar { 58 | width: 40vw; 59 | } 60 | } 61 | 62 | @media screen and (max-width: 992px) { 63 | html.todobar-active .laravel-todobar { 64 | width: 40vw; 65 | } 66 | } 67 | 68 | @media screen and (max-width: 767px) { 69 | html.todobar-active .laravel-todobar { 70 | width: 60vw; 71 | } 72 | } 73 | 74 | @media screen and (max-width: 576px) { 75 | html.todobar-active .laravel-todobar { 76 | width: 87vw; 77 | } 78 | } 79 | 80 | html.todobar-active body.laravel-todobar-split { 81 | width: 80%; 82 | } 83 | 84 | .laravel-todobar .todobar-projects-container { 85 | width: 100%; 86 | display: flex; 87 | align-items: center; 88 | justify-content: center; 89 | padding: 15px; 90 | background: #fcfcfd; 91 | background: #343a40; 92 | } 93 | 94 | .laravel-todobar .todobar-projects-select { 95 | padding: 5px; 96 | margin-left: 4px; 97 | flex: 1; 98 | } 99 | 100 | .laravel-todobar .todobar-projects-container button { 101 | margin-left: 2px; 102 | } 103 | 104 | .laravel-todobar .laravel-todobar-content { 105 | padding: 0px; 106 | display: flex; 107 | flex-direction: column; 108 | height: calc(100vh - 56.8px); 109 | } 110 | 111 | .laravel-todobar .laravel-todobar-form { 112 | flex: 0; 113 | padding: 15px; 114 | background: #fcfcfd; 115 | background: #343a40; 116 | border-bottom: 1px solid #ddd; 117 | border-color: #343a40; 118 | } 119 | 120 | .laravel-todobar .laravel-todobar-content h3 { 121 | background: #eee; 122 | padding: 15px; 123 | border-top: 1px solid #dde; 124 | border-bottom: 1px solid #dde; 125 | margin-bottom: 0; 126 | flex: 0; 127 | background: #464b51; 128 | border-color: #343a40; 129 | 130 | color: white; 131 | border-color: #343a40; 132 | } 133 | 134 | .laravel-todobar ul.laravel-todobar-list { 135 | list-style-type: none; 136 | padding: 0; 137 | line-height: 1.5; 138 | overflow-y: scroll; 139 | flex: 1; 140 | margin-bottom: 0; 141 | background: #343a40; 142 | color: #eee; 143 | } 144 | 145 | .laravel-todobar ul.laravel-todobar-list li { 146 | padding: 15px; 147 | border-bottom: 1px solid #dde; 148 | border-color: #343a40; 149 | background: #343a40; 150 | color: #eee; 151 | } 152 | 153 | .laravel-todobar ul.laravel-todobar-list li input:checked + label .label-content { 154 | color: slategray; 155 | text-decoration: line-through; 156 | text-decoration-color: slategray; 157 | } 158 | 159 | .laravel-todobar ul.laravel-todobar-list li.custom-checkbox-odd { 160 | background: #f9f9f9; 161 | background: #464b51; 162 | } 163 | 164 | .laravel-todobar .laravel-todobar-loader { 165 | background: rgba(0, 0, 0, 0.7) no-repeat 50% 50%; 166 | display: flex; 167 | align-items: center; 168 | justify-content: center; 169 | position: absolute; 170 | top: 0; 171 | left: 0; 172 | right: 0; 173 | bottom: 0; 174 | z-index: 10; 175 | } 176 | 177 | .laravel-todobar ul.laravel-todobar-list li .form-group, 178 | .laravel-todobar ul.laravel-todobar-list li .form-group label { 179 | margin-bottom: 0; 180 | } 181 | 182 | .laravel-todobar ul.laravel-todobar-list li .form-group input[type="checkbox"] { 183 | display: none; 184 | } 185 | 186 | .laravel-todobar ul.laravel-todobar-list li .form-group .checkbox-dummy { 187 | background: white; 188 | color: white; 189 | border: 1px solid #aaa; 190 | width: 20px; 191 | height: 20px; 192 | transform: translateY(100%) translateX(-30px); 193 | border-radius: 5px; 194 | display: flex; 195 | align-items: center; 196 | justify-content: center; 197 | border-color: #565b61; 198 | } 199 | 200 | .laravel-todobar ul.laravel-todobar-list li input:checked + label .checkbox-dummy { 201 | background: green; 202 | } 203 | 204 | .laravel-todobar ul.laravel-todobar-list li label { 205 | display: block; 206 | padding-left: 30px; 207 | margin-top: -20px; 208 | } 209 | 210 | .laravel-todobar-edit-modal .modal-content { 211 | border-radius: calc(0.3rem + 2.6px); 212 | } 213 | 214 | .laravel-todobar-edit-modal .modal-body { 215 | background: #40464b; 216 | } 217 | 218 | .laravel-todobar-edit-modal .modal-header { 219 | background: #2d3032; 220 | border-bottom: 1px solid #2a2c2e; 221 | } 222 | 223 | .laravel-todobar-edit-modal .modal-header button.close span { 224 | text-shadow: 0 1px 0 #000; 225 | color: #eee; 226 | } 227 | 228 | .laravel-todobar-edit-modal .modal-title { 229 | color: #ccc; 230 | } 231 | 232 | .laravel-todobar-edit-modal .modal-footer { 233 | background: #2d3032; 234 | border-top: 1px solid #2a2c2e; 235 | -webkit-box-shadow: none; 236 | -moz-box-shadow: none; 237 | box-shadow: none; 238 | } 239 | -------------------------------------------------------------------------------- /src/assets/todobar.css: -------------------------------------------------------------------------------- 1 | .laravel-todobar { 2 | position: fixed; 3 | right: 0; 4 | top: 0; 5 | bottom: 0; 6 | width: 0; 7 | height: 100vh; 8 | z-index: 100; 9 | } 10 | 11 | .laravel-todobar .laravel-todobar-container { 12 | display: none; 13 | height: 100vh; 14 | flex-direction: column; 15 | } 16 | 17 | html.todobar-active .laravel-todobar .laravel-todobar-container { 18 | display: flex; 19 | } 20 | 21 | .laravel-todobar-handle { 22 | cursor: pointer; 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | position: absolute; 27 | top: 57px; 28 | background: #ff2d20; 29 | font-weight: bold; 30 | left: -37px; 31 | padding: 9px; 32 | border-radius: 5px 0 0 5px; 33 | box-shadow: -2px 0 2px -1px rgba(0, 0, 0, 0.4); 34 | z-index: -1; 35 | } 36 | 37 | html.todobar-active .laravel-todobar { 38 | position: fixed; 39 | right: 0; 40 | top: 0; 41 | bottom: 0; 42 | width: 20vw; 43 | padding: 0px; 44 | background: white; 45 | border: 1px solid #ddd; 46 | box-shadow: -2px 0 2px -1px rgba(0, 0, 0, 0.1); 47 | } 48 | 49 | html.todobar-active body.laravel-todobar-split { 50 | width: calc(100% - 20vw); 51 | } 52 | 53 | @media screen and (max-width: 1440px) { 54 | html.todobar-active .laravel-todobar { 55 | width: 30vw; 56 | } 57 | html.todobar-active body.laravel-todobar-split { 58 | width: calc(100% - 30vw); 59 | } 60 | } 61 | 62 | @media screen and (max-width: 1200px) { 63 | html.todobar-active .laravel-todobar { 64 | width: 40vw; 65 | } 66 | html.todobar-active body.laravel-todobar-split { 67 | width: calc(100% - 40vw); 68 | } 69 | } 70 | 71 | @media screen and (max-width: 992px) { 72 | html.todobar-active .laravel-todobar { 73 | width: 40vw; 74 | } 75 | html.todobar-active body.laravel-todobar-split { 76 | width: calc(100% - 40vw); 77 | } 78 | } 79 | 80 | @media screen and (max-width: 767px) { 81 | html.todobar-active .laravel-todobar { 82 | width: 60vw; 83 | } 84 | html.todobar-active body.laravel-todobar-split { 85 | width: 100%; 86 | } 87 | } 88 | 89 | @media screen and (max-width: 576px) { 90 | html.todobar-active .laravel-todobar { 91 | width: 87vw; 92 | } 93 | html.todobar-active body.laravel-todobar-split { 94 | width: 100%; 95 | } 96 | } 97 | 98 | .laravel-todobar .todobar-projects-container { 99 | width: 100%; 100 | display: flex; 101 | align-items: center; 102 | justify-content: center; 103 | padding: 15px; 104 | background: #fcfcfd; 105 | } 106 | 107 | .laravel-todobar .todobar-projects-select { 108 | padding: 5px; 109 | margin-left: 4px; 110 | flex: 1; 111 | } 112 | 113 | .laravel-todobar .todobar-projects-container button { 114 | margin-left: 2px; 115 | } 116 | 117 | .laravel-todobar .laravel-todobar-content { 118 | padding: 0px; 119 | display: flex; 120 | flex-direction: column; 121 | height: calc(100vh - 56.8px); 122 | } 123 | 124 | .laravel-todobar .laravel-todobar-form { 125 | flex: 0; 126 | padding: 15px; 127 | background: #fcfcfd; 128 | border-bottom: 1px solid #ddd; 129 | } 130 | 131 | .laravel-todobar .laravel-todobar-content h3 { 132 | background: #eee; 133 | padding: 15px; 134 | border-top: 1px solid #dde; 135 | border-bottom: 1px solid #dde; 136 | margin-bottom: 0; 137 | flex: 0; 138 | } 139 | 140 | .laravel-todobar ul.laravel-todobar-list { 141 | list-style-type: none; 142 | padding: 0; 143 | line-height: 1.5; 144 | overflow-y: scroll; 145 | flex: 1; 146 | margin-bottom: 0; 147 | } 148 | 149 | .laravel-todobar ul.laravel-todobar-list li { 150 | padding: 15px; 151 | border-bottom: 1px solid #dde; 152 | } 153 | 154 | .laravel-todobar ul.laravel-todobar-list li input:checked+label .label-content { 155 | color: slategray; 156 | text-decoration: line-through; 157 | text-decoration-color: slategray; 158 | } 159 | 160 | .laravel-todobar ul.laravel-todobar-list li.custom-checkbox-odd { 161 | background: #f9f9f9; 162 | } 163 | 164 | .laravel-todobar .laravel-todobar-loader { 165 | background: rgba(255, 255, 255, 0.7) no-repeat 50% 50%; 166 | display: flex; 167 | align-items: center; 168 | justify-content: center; 169 | position: absolute; 170 | top: 0; 171 | left: 0; 172 | right: 0; 173 | bottom: 0; 174 | z-index: 10; 175 | } 176 | 177 | .laravel-todobar ul.laravel-todobar-list li .form-group, 178 | .laravel-todobar ul.laravel-todobar-list li .form-group label { 179 | margin-bottom: 0; 180 | } 181 | 182 | .laravel-todobar ul.laravel-todobar-list li .form-group input[type="checkbox"] { 183 | display: none; 184 | } 185 | 186 | .laravel-todobar ul.laravel-todobar-list li .form-group .checkbox-dummy { 187 | background: white; 188 | color: white; 189 | border: 1px solid #aaa; 190 | width: 20px; 191 | height: 20px; 192 | transform: translateY(100%) translateX(-30px); 193 | border-radius: 5px; 194 | display: flex; 195 | align-items: center; 196 | justify-content: center; 197 | } 198 | 199 | .laravel-todobar ul.laravel-todobar-list li input:checked+label .checkbox-dummy { 200 | background: green; 201 | } 202 | 203 | .laravel-todobar ul.laravel-todobar-list li label { 204 | display: block; 205 | padding-left: 30px; 206 | margin-top: -20px; 207 | } 208 | -------------------------------------------------------------------------------- /src/assets/todobar.js: -------------------------------------------------------------------------------- 1 | let todobar = { 2 | active_project: null, 3 | init: function () { 4 | todobar.projects.init(); 5 | }, 6 | projects: { 7 | init: function () { 8 | todobar.projects.get(todobar.utils.remember()); 9 | }, 10 | input_changed: function (input) { 11 | todobar.active_project = input.value; 12 | todobar.utils.memorize(); 13 | todobar.tasks.get(todobar.active_project); 14 | }, 15 | get: function (initial) { 16 | let projects_select = document.querySelector(".todobar-projects-select"); 17 | todobar.fetcher.get("projects", (result) => { 18 | let options = ""; 19 | result.data.forEach((element, key) => { 20 | options += ""; 21 | }); 22 | projects_select.innerHTML = options; 23 | projects_select.value = (initial == undefined || initial == null) ? -1 : initial; 24 | if(projects_select.value == ""){ 25 | projects_select.value = -1; 26 | } 27 | projects_select.dispatchEvent(new Event("change")); 28 | }); 29 | }, 30 | add: function () { 31 | let project_name = prompt("What will be the new project's name?"); 32 | if (project_name != null) { 33 | todobar.fetcher.post("projects", { 34 | name: project_name 35 | }, (result) => { 36 | if (result.status == "success") { 37 | todobar.projects.get(parseInt(result.id, 10)); 38 | } else if (result.status == "error") { 39 | alert(result.error); 40 | } 41 | }); 42 | } 43 | }, 44 | edit: function () { 45 | let selected_item = document.querySelector(".todobar-projects-select ").selectedOptions[0], 46 | project_name = selected_item.text, 47 | project_id = selected_item.value; 48 | if (project_id && project_id >= 0) { 49 | let new_project_name = prompt("What will be the new project name?", project_name); 50 | if (new_project_name !== null) { 51 | todobar.fetcher.patch("projects/" + project_id, { 52 | name: new_project_name 53 | }, (result) => { 54 | if (result.status == "success") { 55 | todobar.projects.get(parseInt(project_id, 10)); 56 | } else if (result.status == "error") { 57 | alert(result.error); 58 | } 59 | }); 60 | } 61 | } else { 62 | alert("You need to select a project first to change it's name!"); 63 | } 64 | }, 65 | delete: function () { 66 | let selected_item = document.querySelector(".todobar-projects-select ").selectedOptions[0], project_id = selected_item.value; 67 | if (project_id && project_id >= 0) { 68 | if (confirm("Are you sure to delete this project? This will also remove all associated tasks. And can't be undone. Are you still sure?")) { 69 | todobar.fetcher.delete("projects/"+project_id, (result) => { 70 | if (result.status == "success") { 71 | todobar.projects.get(); 72 | } else if (result.status == "error") { 73 | alert(result.error); 74 | } 75 | }); 76 | } 77 | } else { 78 | alert("You need to select a project first to delete it!"); 79 | } 80 | } 81 | }, 82 | tasks: { 83 | get: function (project_id) { 84 | todobar.fetcher.get("/projects/" + project_id + "/tasks", function (result) { 85 | if (result.status == "success") { 86 | document.querySelector(".laravel-todobar-list").innerHTML = result.html; 87 | } else if (result.status == "error") { 88 | alert(result.error); 89 | } 90 | }); 91 | }, 92 | add: function () { 93 | let project_id = todobar.active_project, task_input = document.querySelector("#laravel-task-input"), task = task_input.value; 94 | todobar.fetcher.post("/projects/" + project_id + "/tasks", { 95 | content: task 96 | }, function (result) { 97 | if (result.status == "success") { 98 | todobar.tasks.get(project_id); 99 | task_input.value = ""; 100 | task_input.focus(); 101 | } else if (result.status == "error") { 102 | alert(result.error); 103 | } 104 | }); 105 | }, 106 | edit_form: function(project_id, task_id, task){ 107 | document.querySelector("#laravel-todobar_project_id").value = project_id; 108 | document.querySelector("#laravel-todobar_task_id").value = task_id; 109 | document.querySelector("#laravel-todobar_content").value = task; 110 | $("#laravel-todobar-edit-modal").modal("show"); 111 | }, 112 | edit: function () { 113 | let project_id = document.querySelector("#laravel-todobar_project_id").value, 114 | task_id = document.querySelector("#laravel-todobar_task_id").value, 115 | content = document.querySelector("#laravel-todobar_content").value; 116 | 117 | todobar.fetcher.patch("/projects/" + project_id + "/tasks/" + task_id, { 118 | content: content 119 | }, function (result) { 120 | if (result.status == "success") { 121 | todobar.tasks.get(project_id); 122 | $("#laravel-todobar-edit-modal").modal("hide"); 123 | } else if (result.status == "error") { 124 | alert(result.error); 125 | } 126 | }); 127 | }, 128 | delete: function (project_id, task_id) { 129 | todobar.fetcher.delete("/projects/" + project_id + "/tasks/" + task_id, function (result) { 130 | if (result.status == "success") { 131 | todobar.tasks.get(project_id); 132 | } else if (result.status == "error") { 133 | alert(result.error); 134 | } 135 | }); 136 | }, 137 | setStatus: function (project_id, task_id, completed) { 138 | todobar.fetcher.patch("/projects/" + project_id + "/tasks/" + task_id, {status: completed}, function (result) { 139 | if (result.status == "success") { 140 | todobar.tasks.get(project_id); 141 | } else if (result.status == "error") { 142 | alert(result.error); 143 | } 144 | }); 145 | } 146 | }, 147 | fetcher: { 148 | get: function (endpoint, callback) { 149 | this.fetch("GET", endpoint, undefined, callback); 150 | }, 151 | delete: function (endpoint, callback) { 152 | this.fetch("DELETE", endpoint, undefined, callback); 153 | }, 154 | post: function (endpoint, data, callback) { 155 | this.fetch("POST", endpoint, data, callback); 156 | }, 157 | patch: function (endpoint, data, callback) { 158 | this.fetch("PATCH", endpoint, data, callback); 159 | }, 160 | fetch: function (method, endpoint, data, callback) { 161 | todobar.utils.show_loader(); 162 | fetch("/laravel-todobar/" + todobar.utils.trim_leading_slashes(endpoint), { 163 | method: method, 164 | headers: { 165 | "Accept": "application/json", 166 | "Content-Type": "application/json", 167 | "Authorization": "Bearer " + document.querySelector(".laravel-todobar input[name='todobar-token']").value 168 | }, 169 | body: (data !== undefined) ? JSON.stringify(data) : null 170 | }).then(response => { 171 | if (!response.ok) { 172 | throw new Error(response.statusText) 173 | } 174 | return response.json() 175 | }) 176 | .catch(error => { 177 | alert(`Request failed: ${error}`) 178 | }).then((result) => { 179 | if (result) { 180 | callback(result); 181 | } 182 | }).finally(() => { 183 | todobar.utils.hide_loader(); 184 | }); 185 | } 186 | }, 187 | utils: { 188 | trim_leading_slashes: function (str) { 189 | return str.replace(/^\/+/g, ''); 190 | }, 191 | show_loader: () => { 192 | if(document.querySelectorAll(".laravel-todobar-loader").length == 0){ 193 | document.querySelector(".laravel-todobar-list").insertAdjacentHTML("afterend", '
\ 194 | \ 195 | \ 196 | \ 203 | \ 204 | \ 205 | \ 212 | \ 213 | \ 214 |
'); 215 | } 216 | }, 217 | hide_loader: () => { 218 | if(document.querySelectorAll(".laravel-todobar-loader").length > 0){ 219 | document.querySelectorAll(".laravel-todobar-loader").forEach((item) => { 220 | item.remove(); 221 | }); 222 | } 223 | }, 224 | memorize: function () { 225 | window.localStorage.setItem("laravel-todobar-last-project", parseInt(todobar.active_project, 10)); 226 | }, 227 | remember: function () { 228 | return window.localStorage.getItem("laravel-todobar-last-project"); 229 | } 230 | } 231 | }; 232 | 233 | todobar.init(); 234 | -------------------------------------------------------------------------------- /src/config/todobar.php: -------------------------------------------------------------------------------- 1 | env("TODOBAR_ENABLED", true), 5 | "start_visible" => true, 6 | "overlay" => true, 7 | "dark_mode" => false, 8 | "storage" => [ 9 | "engine" => \TPaksu\TodoBar\Storage\JSONStorage::class, 10 | "params" => [ 11 | "file" => "items.json", 12 | ], 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /src/resources/views/partials/form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | 5 |
6 | 7 |
8 |
9 |
10 | -------------------------------------------------------------------------------- /src/resources/views/partials/handle.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/resources/views/partials/projects.blade.php: -------------------------------------------------------------------------------- 1 |
2 | 5 | 6 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /src/resources/views/partials/tasks.blade.php: -------------------------------------------------------------------------------- 1 | @isset($tasks) 2 | @forelse($tasks as $key => $task) 3 |
  • 4 |
    5 | completed ?? false) checked @endif 6 | onclick="todobar.tasks.setStatus('{{$project_id}}', '{{$key}}', document.querySelector('#' + this.id).checked);"> 7 | 20 |
    21 |
  • 22 | @empty 23 |
  • 24 | This project is empty. Start with adding some tasks to it! 25 |
  • 26 | @endforelse 27 | @endisset 28 | @if(!isset($tasks)) 29 |
  • 30 | Please select a project from the upper list to start. 31 |
  • 32 | @endif 33 | -------------------------------------------------------------------------------- /src/resources/views/todobar.blade.php: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
    @include("laravel-todobar::partials.handle")
    4 |
    5 | @include('laravel-todobar::partials.projects') 6 |
    7 |

    Tasks

    8 | @include("laravel-todobar::partials.form") 9 |
      10 | @include("laravel-todobar::partials.tasks") 11 |
    12 |
    13 |
    14 |
    15 | 35 | @if(config("todobar.start_visible")) 36 | 39 | @endif 40 | @if(!config('todobar.overlay')) 41 | 44 | @endif 45 | -------------------------------------------------------------------------------- /src/routes/web.php: -------------------------------------------------------------------------------- 1 | 'laravel-todobar'], function() { 6 | Route::resource('projects', TPaksu\TodoBar\Controllers\TodoBarProjects::class); 7 | Route::resource('projects.tasks', TPaksu\TodoBar\Controllers\TodoBarTasks::class); 8 | }); 9 | --------------------------------------------------------------------------------