diff --git a/app/Http/Controllers/SiteController.php b/app/Http/Controllers/SiteController.php index 683038c..843db21 100644 --- a/app/Http/Controllers/SiteController.php +++ b/app/Http/Controllers/SiteController.php @@ -3,18 +3,20 @@ namespace App\Http\Controllers; use App\Models\Site; +use App\Support\GuestPreview; use Illuminate\Contracts\View\View; class SiteController extends Controller { public function show(Site $site): View { - $pages = $site->pages()->orderBy('url')->paginate(50, ['*'], 'pages_page'); + $pages = GuestPreview::paginate($site->pages()->orderBy('url'), 50, 'pages_page'); - $backlinks = $site->inboundLinks() - ->with(['site', 'sitePage']) - ->latest() - ->paginate(50, ['*'], 'backlinks_page'); + $backlinks = GuestPreview::paginate( + $site->inboundLinks()->with(['site', 'sitePage'])->latest(), + 50, + 'backlinks_page' + ); $referringDomainsCount = $site->inboundLinks()->distinct()->count('site_id'); $outboundLinksCount = $site->outboundLinks()->count(); diff --git a/app/Livewire/Auth/Login.php b/app/Livewire/Auth/Login.php new file mode 100644 index 0000000..ed557f4 --- /dev/null +++ b/app/Livewire/Auth/Login.php @@ -0,0 +1,42 @@ +validate([ + 'email' => ['required', 'email'], + 'password' => ['required'], + ]); + + if (! Auth::attempt(['email' => $this->email, 'password' => $this->password], $this->remember)) { + throw ValidationException::withMessages([ + 'email' => 'These credentials do not match our records.', + ]); + } + + session()->regenerate(); + + $this->redirect(route('sites.index'), navigate: false); + } + + public function render(): View + { + return view('livewire.auth.login'); + } +} diff --git a/app/Livewire/Auth/Register.php b/app/Livewire/Auth/Register.php new file mode 100644 index 0000000..d09c8c4 --- /dev/null +++ b/app/Livewire/Auth/Register.php @@ -0,0 +1,48 @@ +validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + 'password' => ['required', 'confirmed', Password::defaults()], + ]); + + $user = User::create([ + 'name' => $validated['name'], + 'email' => $validated['email'], + 'password' => $validated['password'], + ]); + + Auth::login($user); + + session()->regenerate(); + + $this->redirect(route('sites.index'), navigate: false); + } + + public function render(): View + { + return view('livewire.auth.register'); + } +} diff --git a/app/Livewire/Discover/Search.php b/app/Livewire/Discover/Search.php index 3f50e60..e303a13 100644 --- a/app/Livewire/Discover/Search.php +++ b/app/Livewire/Discover/Search.php @@ -3,6 +3,7 @@ namespace App\Livewire\Discover; use App\Models\Site; +use App\Support\GuestPreview; use Illuminate\Contracts\View\View; use Illuminate\Pagination\LengthAwarePaginator; use Livewire\Attributes\Layout; @@ -67,7 +68,7 @@ class Search extends Component if (! $hasQuery) { $sites = new LengthAwarePaginator([], 0, 25, 1, ['path' => request()->url()]); } else { - $sites = Site::query() + $sites = GuestPreview::paginate(Site::query() ->when( $keyword !== '', fn ($query) => $query->whereHas( @@ -98,8 +99,7 @@ class Search extends Component } }]) ->orderByDesc('matching_pages_count') - ->orderBy('domain') - ->paginate(25); + ->orderBy('domain')); } return view('livewire.discover.search', ['sites' => $sites, 'hasQuery' => $hasQuery]); diff --git a/app/Livewire/Sites/Index.php b/app/Livewire/Sites/Index.php index 36251a7..a0a75da 100644 --- a/app/Livewire/Sites/Index.php +++ b/app/Livewire/Sites/Index.php @@ -3,6 +3,7 @@ namespace App\Livewire\Sites; use App\Models\Site; +use App\Support\GuestPreview; use Illuminate\Contracts\View\View; use Livewire\Attributes\Layout; use Livewire\Attributes\Url; @@ -24,14 +25,15 @@ class Index extends Component public function render(): View { - $sites = Site::query() - ->withCount('pages') - ->when( - $this->search !== '', - fn ($query) => $query->where('domain', 'like', '%'.$this->search.'%') - ) - ->orderBy('domain') - ->paginate(25); + $sites = GuestPreview::paginate( + Site::query() + ->withCount('pages') + ->when( + $this->search !== '', + fn ($query) => $query->where('domain', 'like', '%'.$this->search.'%') + ) + ->orderBy('domain') + ); return view('livewire.sites.index', ['sites' => $sites]); } diff --git a/app/Support/GuestPreview.php b/app/Support/GuestPreview.php new file mode 100644 index 0000000..1ecbe22 --- /dev/null +++ b/app/Support/GuestPreview.php @@ -0,0 +1,33 @@ +paginate(self::LIMIT, ['*'], $pageName, 1); + } + + return $query->paginate($fullPerPage, ['*'], $pageName); + } + + public static function isLimited(): bool + { + return ! auth()->check(); + } +} diff --git a/phpunit.xml b/phpunit.xml index e7f0a48..f896766 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -23,8 +23,12 @@ - - + + + + + + diff --git a/resources/views/components/guest-preview-notice.blade.php b/resources/views/components/guest-preview-notice.blade.php new file mode 100644 index 0000000..62d7abd --- /dev/null +++ b/resources/views/components/guest-preview-notice.blade.php @@ -0,0 +1,11 @@ +@props(['paginator']) + +@if ($paginator->total() > $paginator->count()) +

+ Showing a preview ({{ $paginator->count() }} of {{ number_format($paginator->total()) }} results). + Sign in + or + create a free account + to see everything. +

+@endif diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php index a2bcd2f..3c0c134 100644 --- a/resources/views/components/layouts/app.blade.php +++ b/resources/views/components/layouts/app.blade.php @@ -12,11 +12,23 @@
- Huginn -
diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php new file mode 100644 index 0000000..37173e1 --- /dev/null +++ b/resources/views/home.blade.php @@ -0,0 +1,27 @@ + +
+

Find out who you're really competing with

+

+ Huginn indexes the web using Common Crawl's public archive and helps you find competitors + by product, location, and even the platform they're built on — e.g. "who sells honey with a + .es domain," or "which of my competitors run WordPress." +

+ +
+ @auth + Go to Discover + Browse sites + @else + Sign up free + Log in + @endauth +
+ + @guest +

+ Browsing without an account shows a limited preview of results — + create a free account to see everything. +

+ @endguest +
+
diff --git a/resources/views/livewire/auth/login.blade.php b/resources/views/livewire/auth/login.blade.php new file mode 100644 index 0000000..0dbda45 --- /dev/null +++ b/resources/views/livewire/auth/login.blade.php @@ -0,0 +1,46 @@ +
+

Log in

+ +
+
+ + + @error('email')

{{ $message }}

@enderror +
+ +
+ + + @error('password')

{{ $message }}

@enderror +
+ + + + +
+ +

+ Don't have an account? Sign up. +

+
diff --git a/resources/views/livewire/auth/register.blade.php b/resources/views/livewire/auth/register.blade.php new file mode 100644 index 0000000..7978d68 --- /dev/null +++ b/resources/views/livewire/auth/register.blade.php @@ -0,0 +1,64 @@ +
+

Create an account

+ +
+
+ + + @error('name')

{{ $message }}

@enderror +
+ +
+ + + @error('email')

{{ $message }}

@enderror +
+ +
+ + + @error('password')

{{ $message }}

@enderror +
+ +
+ + +
+ + +
+ +

+ Already have an account? Log in. +

+
diff --git a/resources/views/livewire/discover/search.blade.php b/resources/views/livewire/discover/search.blade.php index dc961a5..45b0171 100644 --- a/resources/views/livewire/discover/search.blade.php +++ b/resources/views/livewire/discover/search.blade.php @@ -69,6 +69,12 @@
- {{ $sites->links() }} + @auth + {{ $sites->links() }} + @else + @if ($hasQuery) + + @endif + @endauth
diff --git a/resources/views/livewire/sites/index.blade.php b/resources/views/livewire/sites/index.blade.php index 215d65d..cdbba69 100644 --- a/resources/views/livewire/sites/index.blade.php +++ b/resources/views/livewire/sites/index.blade.php @@ -41,6 +41,10 @@
- {{ $sites->links() }} + @auth + {{ $sites->links() }} + @else + + @endauth
diff --git a/resources/views/sites/show.blade.php b/resources/views/sites/show.blade.php index 0a827d9..b543de2 100644 --- a/resources/views/sites/show.blade.php +++ b/resources/views/sites/show.blade.php @@ -62,7 +62,11 @@
- {{ $pages->links() }} + @auth + {{ $pages->links() }} + @else + + @endauth

@@ -127,7 +131,11 @@
- {{ $backlinks->links() }} + @auth + {{ $backlinks->links() }} + @else + + @endauth
diff --git a/routes/web.php b/routes/web.php index b2ca880..d5f7ff2 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,10 +1,27 @@ name('sites.index'); +Route::view('/', 'home')->name('home'); +Route::get('/sites', SitesIndex::class)->name('sites.index'); Route::get('/sites/{site}', [SiteController::class, 'show'])->name('sites.show'); Route::get('/discover', DiscoverSearch::class)->name('discover'); + +Route::middleware('guest')->group(function () { + Route::get('/login', Login::class)->name('login'); + Route::get('/register', Register::class)->name('register'); +}); + +Route::post('/logout', function () { + Auth::logout(); + request()->session()->invalidate(); + request()->session()->regenerateToken(); + + return redirect()->route('home'); +})->middleware('auth')->name('logout'); diff --git a/tests/Feature/AuthFlowTest.php b/tests/Feature/AuthFlowTest.php new file mode 100644 index 0000000..b6fa474 --- /dev/null +++ b/tests/Feature/AuthFlowTest.php @@ -0,0 +1,66 @@ +set('name', 'Test User') + ->set('email', 'testuser@example.com') + ->set('password', 'correct-horse-battery-staple') + ->set('password_confirmation', 'correct-horse-battery-staple') + ->call('register') + ->assertRedirect(route('sites.index')); + + $this->assertAuthenticated(); + $this->assertDatabaseHas('users', ['email' => 'testuser@example.com']); + } + + public function test_a_user_can_log_in(): void + { + $user = User::factory()->create(['password' => 'correct-horse-battery-staple']); + + Livewire::test(Login::class) + ->set('email', $user->email) + ->set('password', 'correct-horse-battery-staple') + ->call('login') + ->assertRedirect(route('sites.index')); + + $this->assertAuthenticatedAs($user); + } + + public function test_login_fails_with_wrong_password(): void + { + $user = User::factory()->create(['password' => 'correct-horse-battery-staple']); + + Livewire::test(Login::class) + ->set('email', $user->email) + ->set('password', 'wrong-password') + ->call('login') + ->assertHasErrors('email'); + + $this->assertGuest(); + } + + public function test_a_user_can_log_out(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->post(route('logout')) + ->assertRedirect(route('home')); + + $this->assertGuest(); + } +} diff --git a/tests/Feature/GuestPreviewTest.php b/tests/Feature/GuestPreviewTest.php new file mode 100644 index 0000000..26dc99b --- /dev/null +++ b/tests/Feature/GuestPreviewTest.php @@ -0,0 +1,44 @@ + 'CC-MAIN-TEST', + 'domain' => "example{$i}.com", + 'url' => "https://example{$i}.com/", + ]); + } + } + + public function test_guest_sees_only_a_preview_of_sites(): void + { + $this->seedSites(8); + + $response = $this->get(route('sites.index')); + + $response->assertOk(); + $response->assertSeeText('Showing a preview (5 of 8 results)'); + } + + public function test_authenticated_user_sees_all_sites(): void + { + $this->seedSites(8); + + $response = $this->actingAs(User::factory()->create())->get(route('sites.index')); + + $response->assertOk(); + $response->assertDontSeeText('Showing a preview'); + } +}