Skip to content

Commit 6ce143f

Browse files
author
nejc
committed
feat: Complete feature requests package with Tabler icons and comprehensive testing
- Add Tabler thumbs up/down icons for better UX - Implement Reddit-style voting system with inline icons - Add comprehensive unit and feature tests - Fix voting functionality and cache invalidation - Improve UI/UX with consistent container layouts - Add authentication middleware for all customer routes - Implement proper vote counting and display - Add create form with 8-4 column layout and tips - Remove priority field in favor of voting-based priority - Add proper error handling and validation - Implement proper return type declarations - Add database schema corrections and migrations - Create complete package structure with service providers - Add proper routing for admin and customer interfaces
1 parent 8be4b8b commit 6ce143f

21 files changed

+1595
-598
lines changed

resources/views/layouts/public.blade.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,34 @@ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-
161161
</div>
162162
@endif
163163

164-
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 pb-20">
164+
<div class="py-8 pb-20">
165+
<!-- Flash Messages -->
166+
@if(session('success'))
167+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-6">
168+
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
169+
<div class="flex items-center">
170+
<svg class="w-5 h-5 text-green-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
171+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
172+
</svg>
173+
<p class="text-green-800 font-medium">{{ session('success') }}</p>
174+
</div>
175+
</div>
176+
</div>
177+
@endif
178+
179+
@if(session('error'))
180+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-6">
181+
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
182+
<div class="flex items-center">
183+
<svg class="w-5 h-5 text-red-600 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
184+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
185+
</svg>
186+
<p class="text-red-800 font-medium">{{ session('error') }}</p>
187+
</div>
188+
</div>
189+
</div>
190+
@endif
191+
165192
@yield('content')
166193
</div>
167194
</main>

resources/views/public/create.blade.php

Lines changed: 140 additions & 115 deletions
Large diffs are not rendered by default.

resources/views/public/index.blade.php

Lines changed: 177 additions & 106 deletions
Large diffs are not rendered by default.

resources/views/public/show.blade.php

Lines changed: 90 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
@section('subheader', 'Feature Request Details')
66

77
@section('content')
8-
<div class="max-w-6xl mx-auto">
8+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
99
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
1010
<!-- Main Content -->
1111
<div class="lg:col-span-2 space-y-6">
@@ -51,6 +51,15 @@
5151

5252
<div class="prose prose-lg max-w-none text-gray-600">
5353
{!! nl2br(e($featureRequest->description)) !!}
54+
55+
@if($featureRequest->additional_info)
56+
<div class="mt-6 pt-6 border-t border-gray-200">
57+
<h4 class="text-lg font-semibold text-gray-900 mb-3">Additional Information</h4>
58+
<div class="text-gray-600">
59+
{!! nl2br(e($featureRequest->additional_info)) !!}
60+
</div>
61+
</div>
62+
@endif
5463
</div>
5564
</div>
5665
</div>
@@ -63,40 +72,21 @@
6372

6473
<div class="p-6">
6574
<!-- Add Comment Form -->
66-
@auth
67-
<form action="{{ route('feature-requests.comments.store', $featureRequest->slug) }}" method="POST" class="mb-6">
68-
@csrf
69-
<div class="space-y-3">
70-
<textarea name="content"
71-
rows="3"
72-
placeholder="Share your thoughts on this feature request..."
73-
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"></textarea>
74-
<div class="flex justify-end">
75-
<button type="submit"
76-
class="px-6 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors">
77-
Post Comment
78-
</button>
79-
</div>
80-
</div>
81-
</form>
82-
@else
83-
<div class="text-center py-6 border border-dashed border-gray-300 rounded-lg mb-6">
84-
<svg class="h-8 w-8 text-gray-400 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
86-
</svg>
87-
<p class="text-sm text-gray-500 mb-3">Please sign in to leave a comment</p>
88-
<div class="flex justify-center space-x-3">
89-
<a href="{{ route('login') }}"
90-
class="px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors">
91-
Sign In
92-
</a>
93-
<a href="{{ route('register') }}"
94-
class="px-4 py-2 border border-blue-600 text-blue-600 font-semibold rounded-lg hover:bg-blue-50 transition-colors">
95-
Sign Up
96-
</a>
75+
<form action="{{ route('feature-requests.comments.store', $featureRequest->slug) }}" method="POST" class="mb-6">
76+
@csrf
77+
<div class="space-y-3">
78+
<textarea name="content"
79+
rows="3"
80+
placeholder="Share your thoughts on this feature request..."
81+
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"></textarea>
82+
<div class="flex justify-end">
83+
<button type="submit"
84+
class="px-6 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors">
85+
Post Comment
86+
</button>
9787
</div>
9888
</div>
99-
@endauth
89+
</form>
10090

10191
<!-- Comments List -->
10292
<div class="space-y-4">
@@ -135,45 +125,77 @@ class="px-4 py-2 border border-blue-600 text-blue-600 font-semibold rounded-lg h
135125
<div class="p-6">
136126
<h3 class="text-lg font-semibold text-gray-900 mb-4">Vote for this feature</h3>
137127

138-
@auth
139-
<div class="text-center">
140-
<div class="mb-4">
141-
<div class="text-3xl font-bold text-gray-900 mb-1">{{ $featureRequest->vote_count }}</div>
142-
<div class="text-sm text-gray-500">votes</div>
128+
<div class="text-center">
129+
<div class="mb-4">
130+
<div class="text-3xl font-bold text-gray-900 mb-1">{{ ($featureRequest->up_votes ?? 0) - ($featureRequest->down_votes ?? 0) }}</div>
131+
<div class="text-sm text-gray-500">net votes</div>
132+
<div class="text-xs text-gray-400 mt-1">
133+
{{ $featureRequest->up_votes ?? 0 }} up • {{ $featureRequest->down_votes ?? 0 }} down
143134
</div>
144-
145-
<form action="{{ route('feature-requests.vote', $featureRequest->slug) }}" method="POST" class="space-y-3">
146-
@csrf
147-
<button type="submit"
148-
class="w-full px-4 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors">
149-
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
150-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V18m-7-8a2 2 0 112 0 2 2 0 01-2 0z"></path>
151-
</svg>
152-
Vote for this feature
153-
</button>
154-
</form>
155135
</div>
156-
@else
157-
<div class="text-center">
158-
<div class="mb-4">
159-
<div class="text-3xl font-bold text-gray-900 mb-1">{{ $featureRequest->vote_count }}</div>
160-
<div class="text-sm text-gray-500">votes</div>
161-
</div>
162-
<div class="space-y-3">
163-
<a href="{{ route('login') }}"
164-
class="w-full inline-flex items-center justify-center px-4 py-3 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-700 transition-colors">
165-
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
166-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
136+
137+
@if(isset($featureRequest->user_has_voted) && $featureRequest->user_has_voted)
138+
<!-- Already Voted - Show Current Vote Status -->
139+
<div class="flex items-center justify-center space-x-4">
140+
<!-- Thumbs Up Button (Disabled) -->
141+
<div class="p-3 rounded-full
142+
@if($featureRequest->user_vote_type === 'up') bg-orange-100 text-orange-500
143+
@else bg-gray-100 text-gray-400
144+
@endif">
145+
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
146+
<path d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558-.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.833 0-1.612.453-1.918 1.227z"/>
147+
</svg>
148+
</div>
149+
150+
<!-- Vote Count -->
151+
<div class="text-2xl font-bold text-gray-700 min-w-[40px] text-center">
152+
{{ ($featureRequest->up_votes ?? 0) - ($featureRequest->down_votes ?? 0) }}
153+
</div>
154+
155+
<!-- Thumbs Down Button (Disabled) -->
156+
<div class="p-3 rounded-full
157+
@if($featureRequest->user_vote_type === 'down') bg-blue-100 text-blue-500
158+
@else bg-gray-100 text-gray-400
159+
@endif">
160+
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
161+
<path d="M15.73 5.25h1.035A7.465 7.465 0 0118 9.375a7.465 7.465 0 01-1.235 4.125h-.148c-.806 0-1.355.673-1.355 1.456 0 .31.12.616.33.835.806.875 1.309 1.76 1.309 2.889 0 .563-.115 1.109-.33 1.59-.215.48-.53.923-.93 1.309-.4.386-.885.69-1.44.923-.555.233-1.17.35-1.8.35H9.75a.75.75 0 01-.75-.75V3.75a.75.75 0 01.75-.75h.148c.806 0 1.355-.673 1.355-1.456 0-.31-.12-.616-.33-.835-.806-.875-1.309-1.76-1.309-2.889 0-.563.115-1.109.33-1.59-.215-.48-.53-.923-.93-1.309-.4-.386-.885-.69-1.44-.923-.555-.233-1.17-.35-1.8-.35H15.73zM21.669 13.023a11.969 11.969 0 00.831-4.398 12 12 0 00-.52-3.507c-.26-.85-1.084-1.368-1.973-1.368H19.1c-.445 0-.72.498-.523.898a8.963 8.963 0 01.924 3.977c0 1.708-.476 3.305-1.302 4.666-.245.403.028.959.5.959h.148c.833 0 1.612-.453 1.918-1.227z"/>
167162
</svg>
168-
Sign in to vote
169-
</a>
170-
<a href="{{ route('register') }}"
171-
class="w-full inline-flex items-center justify-center px-4 py-3 border border-blue-600 text-blue-600 font-semibold rounded-lg hover:bg-blue-50 transition-colors">
172-
Create Account
173-
</a>
163+
</div>
174164
</div>
175-
</div>
176-
@endauth
165+
@else
166+
<!-- Inline Vote Buttons -->
167+
<div class="flex items-center justify-center space-x-4">
168+
<!-- Thumbs Up Button -->
169+
<form action="{{ route('feature-requests.vote', $featureRequest->slug) }}" method="POST">
170+
@csrf
171+
<input type="hidden" name="vote_type" value="up">
172+
<button type="submit"
173+
class="p-3 rounded-full hover:bg-orange-100 transition-colors group">
174+
<svg class="w-6 h-6 text-gray-400 group-hover:text-orange-500" fill="currentColor" viewBox="0 0 24 24">
175+
<path d="M7.493 18.75c-.425 0-.82-.236-.975-.632A7.48 7.48 0 016 15.375c0-1.75.599-3.358 1.602-4.634.151-.192.373-.309.6-.397.473-.183.89-.514 1.212-.924a9.042 9.042 0 012.861-2.4c.723-.384 1.35-.956 1.653-1.715a4.498 4.498 0 00.322-1.672V3a.75.75 0 01.75-.75 2.25 2.25 0 012.25 2.25c0 1.152-.26 2.243-.723 3.218-.266.558-.107 1.282.725 1.282h3.126c1.026 0 1.945.694 2.054 1.715.045.422.068.85.068 1.285a11.95 11.95 0 01-2.649 7.521c-.388.482-.987.729-1.605.729H14.23c-.483 0-.964-.078-1.423-.23l-3.114-1.04a4.501 4.501 0 00-1.423-.23h-.777zM2.331 10.977a11.969 11.969 0 00-.831 4.398 12 12 0 00.52 3.507c.26.85 1.084 1.368 1.973 1.368H4.9c.445 0 .72-.498.523-.898a8.963 8.963 0 01-.924-3.977c0-1.708.476-3.305 1.302-4.666.245-.403-.028-.959-.5-.959H4.25c-.833 0-1.612.453-1.918 1.227z"/>
176+
</svg>
177+
</button>
178+
</form>
179+
180+
<!-- Vote Count -->
181+
<div class="text-2xl font-bold text-gray-700 min-w-[40px] text-center">
182+
{{ ($featureRequest->up_votes ?? 0) - ($featureRequest->down_votes ?? 0) }}
183+
</div>
184+
185+
<!-- Thumbs Down Button -->
186+
<form action="{{ route('feature-requests.vote', $featureRequest->slug) }}" method="POST">
187+
@csrf
188+
<input type="hidden" name="vote_type" value="down">
189+
<button type="submit"
190+
class="p-3 rounded-full hover:bg-blue-100 transition-colors group">
191+
<svg class="w-6 h-6 text-gray-400 group-hover:text-blue-500" fill="currentColor" viewBox="0 0 24 24">
192+
<path d="M15.73 5.25h1.035A7.465 7.465 0 0118 9.375a7.465 7.465 0 01-1.235 4.125h-.148c-.806 0-1.355.673-1.355 1.456 0 .31.12.616.33.835.806.875 1.309 1.76 1.309 2.889 0 .563-.115 1.109-.33 1.59-.215.48-.53.923-.93 1.309-.4.386-.885.69-1.44.923-.555.233-1.17.35-1.8.35H9.75a.75.75 0 01-.75-.75V3.75a.75.75 0 01.75-.75h.148c.806 0 1.355-.673 1.355-1.456 0-.31-.12-.616-.33-.835-.806-.875-1.309-1.76-1.309-2.889 0-.563.115-1.109.33-1.59-.215-.48-.53-.923-.93-1.309-.4-.386-.885-.69-1.44-.923-.555-.233-1.17-.35-1.8-.35H15.73zM21.669 13.023a11.969 11.969 0 00.831-4.398 12 12 0 00-.52-3.507c-.26-.85-1.084-1.368-1.973-1.368H19.1c-.445 0-.72.498-.523.898a8.963 8.963 0 01.924 3.977c0 1.708-.476 3.305-1.302 4.666-.245.403.028.959.5.959h.148c.833 0 1.612-.453 1.918-1.227z"/>
193+
</svg>
194+
</button>
195+
</form>
196+
</div>
197+
@endif
198+
</div>
177199
</div>
178200
</div>
179201

routes/web.php

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,27 @@
1616
|
1717
*/
1818

19-
Route::prefix('feature-requests')->name('feature-requests.')->middleware(['web'])->group(function () {
19+
Route::prefix('feature-requests')->name('feature-requests.')->middleware(['web', 'auth'])->group(function () {
2020

21-
// Public Feature Requests (Customer View)
21+
// Feature Requests (Customer View) - All require authentication
2222
Route::get('/', [FeatureRequestController::class, 'publicIndex'])->name('index');
2323

24-
// Create Feature Request (Requires authentication) - Must come before /{slug}
25-
Route::middleware(['auth'])->group(function () {
26-
Route::get('/create', [FeatureRequestController::class, 'create'])->name('create');
27-
Route::post('/', [FeatureRequestController::class, 'store'])->name('store');
28-
});
24+
// Create Feature Request - Must come before /{slug}
25+
Route::get('/create', [FeatureRequestController::class, 'create'])->name('create');
26+
Route::post('/', [FeatureRequestController::class, 'store'])->name('store');
2927

3028
Route::get('/{slug}', [FeatureRequestController::class, 'publicShow'])->name('show');
3129

32-
// Voting (Requires authentication)
33-
Route::middleware(['auth'])->group(function () {
34-
Route::post('/{slug}/vote', [VoteController::class, 'store'])->name('vote');
35-
Route::delete('/{slug}/vote', [VoteController::class, 'destroy'])->name('unvote');
36-
});
30+
// Voting
31+
Route::post('/{slug}/vote', [VoteController::class, 'store'])->name('vote');
32+
Route::delete('/{slug}/vote', [VoteController::class, 'destroy'])->name('unvote');
3733

38-
// Comments (Requires authentication)
39-
Route::middleware(['auth'])->group(function () {
40-
Route::post('/{slug}/comments', [CommentController::class, 'store'])->name('comments.store');
41-
Route::put('/comments/{comment}', [CommentController::class, 'update'])->name('comments.update');
42-
Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy');
43-
});
34+
// Comments
35+
Route::post('/{slug}/comments', [CommentController::class, 'store'])->name('comments.store');
36+
Route::put('/comments/{comment}', [CommentController::class, 'update'])->name('comments.update');
37+
Route::delete('/comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy');
4438

45-
// Public Categories (Read-only)
39+
// Categories (Read-only)
4640
Route::prefix('categories')->name('categories.')->group(function () {
4741
Route::get('/', [CategoryController::class, 'publicIndex'])->name('index');
4842
Route::get('/{slug}', [CategoryController::class, 'publicShow'])->name('show');

0 commit comments

Comments
 (0)