Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2

[compose.yaml]
indent_size = 4
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,63 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.4.0] - 2026-04-28

### Added

- **Visitor Tracker**: First-party visitor analytics for dev-tools.online,
powered by the `ghdj/laravel-visitor-tracker` package.
- New `/tools/visitor-tracker` public-facing tool page (and sitemap entry)
- `TrackVisitor` middleware wired into the `web` group so all web requests
are recorded
- Built-in dashboard at `/admin/visitor-tracker`, with env-driven auth:
token-based locally, `allow_unprotected` in production where Cloudflare
Access gates `/admin/*` at the edge
- Geolocation enabled (ip-api provider) for country breakdowns
- Test scaffolding: `phpunit.xml`, `tests/TestCase.php`, and unit/feature
tests for the Base64, CSV, Markdown, SQL, and YAML services/APIs

### Operations

- New `cron/migrate.php` entrypoint that bootstraps Laravel and runs
`migrate --force`, used by the OVH cron to apply package migrations on
shared hosting.
- Deploy workflow (`deploy.yml`) now strips `database/*.sqlite*` and
`bootstrap/cache/*.php` from the build before SFTP, so dev artifacts
never reach production and the server re-caches config/routes after
deploy.
- `.gitignore` excludes local SQLite databases and cached bootstrap files.
- Tests workflow (`tests.yml`) sets `VISITOR_TRACKER_DASHBOARD_ENABLED=false`
so the package's boot-time auth guard does not trip during CI.

### Operator notes

When deploying to a fresh environment, set in the server `.env`:

- `VISITOR_TRACKER_ALLOW_UNPROTECTED=true` (or `VISITOR_TRACKER_TOKEN=...`)
— required, otherwise the package's service provider throws on boot.
- `DB_CONNECTION=sqlite` if no DB server is available.

## [1.3.0] - 2025-12-15

### Added

- **Sort Lines**: New text line manipulation tool
- Sort alphabetically (A-Z, Z-A)
- Natural sort for alphanumeric strings (file1, file2, file10)
- Numeric sort (ascending/descending)
- Sort by line length
- Reverse line order
- Remove duplicates (dedupe)
- Shuffle/randomize lines
- Options: case sensitive, trim whitespace, remove empty lines

## [1.2.1] - 2025-12-15

### Fixed

- Code Editor: Fix PHP parse error when loading the page (`<?php` string in JavaScript was interpreted as PHP tag)

## [1.2.0] - 2025-12-15

### Added
Expand Down Expand Up @@ -178,6 +235,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- RESTful API endpoints for all tools
- 146 tests with 386 assertions

[1.4.0]: https://github.com/GhDj/dev-tools/releases/tag/v1.4.0
[1.3.0]: https://github.com/GhDj/dev-tools/releases/tag/v1.3.0
[1.2.1]: https://github.com/GhDj/dev-tools/releases/tag/v1.2.1
[1.2.0]: https://github.com/GhDj/dev-tools/releases/tag/v1.2.0
[1.1.0]: https://github.com/GhDj/dev-tools/releases/tag/v1.1.0
[1.0.0]: https://github.com/GhDj/dev-tools/releases/tag/v1.0.0
11 changes: 11 additions & 0 deletions app/Http/Controllers/ToolController.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ public function index(): View
'route' => 'tools.diff',
'icon' => 'diff',
],
[
'name' => 'Sort Lines',
'description' => 'Sort, deduplicate, reverse, and shuffle text lines',
'route' => 'tools.sort-lines',
'icon' => 'sort',
],
[
'name' => 'Visitor Tracker',
'description' => 'Server-side visitor analytics for Laravel applications',
Expand Down Expand Up @@ -273,6 +279,11 @@ public function diff(): View
return view('tools.diff');
}

public function sortLines(): View
{
return view('tools.sort-lines');
}

public function visitorTracker(): View
{
$stats = app(\Ghdj\VisitorTracker\Services\StatisticsService::class);
Expand Down
5 changes: 5 additions & 0 deletions resources/views/home.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"/>
</svg>
@break
@case('sort')
<svg class="w-6 h-6 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12"/>
</svg>
@break
@endswitch
</div>
<div class="flex-1 min-w-0">
Expand Down
2 changes: 1 addition & 1 deletion resources/views/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class="absolute top-1 w-6 h-6 rounded-full shadow-lg transition-all duration-500
<a href="{{ route('about') }}" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">About</a>
<a href="{{ route('privacy') }}" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">Privacy</a>
<a href="https://github.com/GhDj/dev-tools" target="_blank" rel="noopener noreferrer" class="hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors">GitHub</a>
<span class="text-gray-500 dark:text-gray-500">v1.2.0</span>
<span class="text-gray-500 dark:text-gray-500">v1.4.0</span>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/tools/code-editor.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ function getDefaultContent(language) {
case 'css': return '/* Styles */\n';
case 'javascript': return '// JavaScript\n';
case 'json': return '{\n \n}';
case 'php': return '<?php\n\n';
case 'php': return '<' + '?php\n\n';
case 'sql': return '-- SQL Query\n';
default: return '';
}
Expand Down
2 changes: 2 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
Route::get('/jwt', [ToolController::class, 'jwt'])->name('jwt');
Route::get('/timestamp', [ToolController::class, 'timestamp'])->name('timestamp');
Route::get('/diff', [ToolController::class, 'diff'])->name('diff');
Route::get('/sort-lines', [ToolController::class, 'sortLines'])->name('sort-lines');
Route::get('/visitor-tracker', [ToolController::class, 'visitorTracker'])->name('visitor-tracker');
});

Expand Down Expand Up @@ -63,6 +64,7 @@
['loc' => route('tools.jwt'), 'priority' => '0.8', 'changefreq' => 'monthly'],
['loc' => route('tools.timestamp'), 'priority' => '0.8', 'changefreq' => 'monthly'],
['loc' => route('tools.diff'), 'priority' => '0.8', 'changefreq' => 'monthly'],
['loc' => route('tools.sort-lines'), 'priority' => '0.8', 'changefreq' => 'monthly'],
['loc' => route('tools.visitor-tracker'), 'priority' => '0.9', 'changefreq' => 'monthly'],
['loc' => route('about'), 'priority' => '0.5', 'changefreq' => 'monthly'],
['loc' => route('privacy'), 'priority' => '0.3', 'changefreq' => 'yearly'],
Expand Down
19 changes: 19 additions & 0 deletions tests/Feature/ExampleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Tests\Feature;

// use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');

$response->assertStatus(200);
}
}
33 changes: 28 additions & 5 deletions tests/Feature/WebRoutesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public function test_home_page_displays_all_tools(): void
$response->assertSee('JWT Decoder');
$response->assertSee('Timestamp Converter');
$response->assertSee('Diff Checker');
$response->assertSee('Sort Lines');
}

public function test_home_page_has_tool_links(): void
Expand Down Expand Up @@ -70,6 +71,7 @@ public function test_home_page_has_tool_links(): void
$response->assertSee('href="' . route('tools.jwt') . '"', false);
$response->assertSee('href="' . route('tools.timestamp') . '"', false);
$response->assertSee('href="' . route('tools.diff') . '"', false);
$response->assertSee('href="' . route('tools.sort-lines') . '"', false);
}

public function test_csv_tool_page_loads(): void
Expand Down Expand Up @@ -516,9 +518,30 @@ public function test_diff_tool_has_required_elements(): void
$response->assertSee('Compare');
}

public function test_sort_lines_page_loads(): void
{
$response = $this->get('/tools/sort-lines');

$response->assertStatus(200);
$response->assertSee('Sort Lines');
$response->assertSee('Sort, deduplicate, reverse, and shuffle text lines');
}

public function test_sort_lines_has_required_elements(): void
{
$response = $this->get('/tools/sort-lines');

$response->assertStatus(200);
$response->assertSee('Input Text');
$response->assertSee('Sort Options');
$response->assertSee('Sort A-Z');
$response->assertSee('Remove Duplicates');
$response->assertSee('Shuffle');
}

public function test_all_pages_have_navigation(): void
{
$pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
$pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];

foreach ($pages as $page) {
$response = $this->get($page);
Expand All @@ -530,7 +553,7 @@ public function test_all_pages_have_navigation(): void

public function test_all_pages_have_theme_toggle(): void
{
$pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
$pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];

foreach ($pages as $page) {
$response = $this->get($page);
Expand All @@ -543,7 +566,7 @@ public function test_all_pages_have_theme_toggle(): void
public function test_all_pages_load_vite_assets(): void
{
// Code editor uses standalone template without Vite
$pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
$pages = ['/', '/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];

foreach ($pages as $page) {
$response = $this->get($page);
Expand All @@ -556,7 +579,7 @@ public function test_all_pages_load_vite_assets(): void
public function test_all_tool_pages_have_back_link(): void
{
// Code editor uses standalone template with home link instead of back
$toolPages = ['/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
$toolPages = ['/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];

foreach ($toolPages as $page) {
$response = $this->get($page);
Expand Down Expand Up @@ -609,7 +632,7 @@ public function test_api_routes_reject_get_requests(): void

public function test_pages_have_csrf_token(): void
{
$pages = ['/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff'];
$pages = ['/tools/csv', '/tools/yaml', '/tools/markdown', '/tools/sql', '/tools/base64', '/tools/uuid', '/tools/hash', '/tools/url', '/tools/code-editor', '/tools/regex', '/tools/base-converter', '/tools/slug-generator', '/tools/color-picker', '/tools/qr-code', '/tools/html-entity', '/tools/text-case', '/tools/password', '/tools/lorem', '/tools/cron', '/tools/jwt', '/tools/timestamp', '/tools/diff', '/tools/sort-lines'];

foreach ($pages as $page) {
$response = $this->get($page);
Expand Down
16 changes: 16 additions & 0 deletions tests/Unit/ExampleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;

class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_that_true_is_true(): void
{
$this->assertTrue(true);
}
}
Loading