From 3750638122c97c43fb46c119c3c67b98644e0b47 Mon Sep 17 00:00:00 2001 From: kusowl Date: Fri, 16 Jan 2026 19:38:30 +0530 Subject: [PATCH] feature(report a deal): users can report a deal --- app/Enums/ReportStatus.php | 14 ++++ app/Enums/ReportType.php | 17 ++++ .../Controllers/InteractionController.php | 6 +- app/Http/Controllers/ReportController.php | 82 +++++++++++++++++++ app/Http/Requests/StoreReportRequest.php | 31 +++++++ app/Models/Deal.php | 14 ++-- app/Models/Report.php | 30 +++++++ app/Traits/EnumAsArray.php | 7 ++ ...2026_01_16_091636_create_reports_table.php | 34 ++++++++ ..._01_16_093141_create_deal_report_table.php | 31 +++++++ resources/js/modal.js | 10 +++ resources/js/report-deal.js | 60 ++++++++++++++ resources/js/toast.js | 15 ++++ .../dashboard/user/action-toolbar.blade.php | 8 +- .../dashboard/user/listing-card.blade.php | 2 +- .../dashboard/user/report-modal.blade.php | 36 ++++++++ .../views/components/icon-square.blade.php | 3 +- .../views/components/ui/button.blade.php | 2 +- .../components/ui/inline-error.blade.php | 2 +- resources/views/components/ui/modal.blade.php | 6 ++ .../views/components/ui/select.blade.php | 2 +- .../views/components/ui/textarea.blade.php | 8 +- resources/views/components/ui/toast.blade.php | 6 ++ resources/views/explore.blade.php | 4 +- routes/web.php | 41 +--------- routes/web/auth.php | 15 ++++ routes/web/broker.php | 18 ++++ routes/web/interaction.php | 21 +++++ 28 files changed, 467 insertions(+), 58 deletions(-) create mode 100644 app/Enums/ReportStatus.php create mode 100644 app/Enums/ReportType.php create mode 100644 app/Http/Controllers/ReportController.php create mode 100644 app/Http/Requests/StoreReportRequest.php create mode 100644 app/Models/Report.php create mode 100644 database/migrations/2026_01_16_091636_create_reports_table.php create mode 100644 database/migrations/2026_01_16_093141_create_deal_report_table.php create mode 100644 resources/js/modal.js create mode 100644 resources/js/report-deal.js create mode 100644 resources/js/toast.js create mode 100644 resources/views/components/dashboard/user/report-modal.blade.php create mode 100644 resources/views/components/ui/modal.blade.php create mode 100644 resources/views/components/ui/toast.blade.php create mode 100644 routes/web/auth.php create mode 100644 routes/web/broker.php create mode 100644 routes/web/interaction.php diff --git a/app/Enums/ReportStatus.php b/app/Enums/ReportStatus.php new file mode 100644 index 0000000..aa80f7d --- /dev/null +++ b/app/Enums/ReportStatus.php @@ -0,0 +1,14 @@ +json(['error' => 'This interaction is not supported'], 400); } @@ -46,6 +46,7 @@ public function togglesState(Deal $deal, InteractionType $type) $message = "{$type->value} added to deal"; } + return response()->json(['message' => $message]); } catch (\Throwable $e) { @@ -62,5 +63,4 @@ public function togglesState(Deal $deal, InteractionType $type) return response()->json(['error' => 'Something went wrong.'], 500); } } - } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php new file mode 100644 index 0000000..23f1c97 --- /dev/null +++ b/app/Http/Controllers/ReportController.php @@ -0,0 +1,82 @@ +validated(); + $data['user_id'] = Auth::id(); + + // Check if the user already reported the deal + $alreadyReported = $deal->reports()->where('user_id', Auth::id())->first(); + if ($alreadyReported) { + return response()->json(['message' => 'You already reported this report'], 405); + } + + try { + DB::transaction(function () use ($data, $deal) { + Report::unguard(); + Report::create($data)->deals()->attach($deal); + Report::reguard(); + }); + + return response()->json(['message' => 'Report created'], 201); + + } catch (\Throwable $exception) { + Log::error('Error creating report', [ + 'user_id' => Auth::id(), + 'deal_id' => $deal->id, + 'error' => $exception->getMessage(), + 'trace' => $exception->getTraceAsString(), + ]); + + return response()->json(['message' => 'Error creating report'], 500); + } + + } + + /** + * Display the specified resource. + */ + public function show(Report $report) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Report $report) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Report $report) + { + // + } +} diff --git a/app/Http/Requests/StoreReportRequest.php b/app/Http/Requests/StoreReportRequest.php new file mode 100644 index 0000000..ee7412f --- /dev/null +++ b/app/Http/Requests/StoreReportRequest.php @@ -0,0 +1,31 @@ +|string> + */ + public function rules(): array + { + return [ + 'type' => ['required', Rule::in(ReportType::values())], + 'description' => 'required|string|min:10|max:500', + ]; + } +} diff --git a/app/Models/Deal.php b/app/Models/Deal.php index 5a80ed5..1c663c4 100644 --- a/app/Models/Deal.php +++ b/app/Models/Deal.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Facades\Auth; @@ -28,8 +29,6 @@ public function interactions(): HasMany /** * Get deals that are active - * @param Builder $query - * @return Builder */ public function scopeWithActiveDeals(Builder $query): Builder { @@ -38,8 +37,6 @@ public function scopeWithActiveDeals(Builder $query): Builder /** * Get if the current user has liked or favorite the deal - * @param Builder $query - * @return Builder */ public function scopeWithCurrentUserInteractions(Builder $query): Builder { @@ -51,7 +48,7 @@ public function scopeWithCurrentUserInteractions(Builder $query): Builder 'interactions as is_favorite' => function ($query) { $query->where('user_id', Auth::id()) ->where('type', InteractionType::Favorite); - } + }, ]); } @@ -60,7 +57,12 @@ public function scopeWithLikes(Builder $query): Builder return $query->withCount([ 'interactions as total_likes' => function ($query) { $query->where('type', InteractionType::Like); - } + }, ]); } + + public function reports(): BelongsToMany + { + return $this->belongsToMany(Report::class); + } } diff --git a/app/Models/Report.php b/app/Models/Report.php new file mode 100644 index 0000000..0ec71b5 --- /dev/null +++ b/app/Models/Report.php @@ -0,0 +1,30 @@ +belongsTo(User::class); + } + + public function deals(): BelongsToMany + { + return $this->belongsToMany(Deal::class); + } + + protected function casts(): array + { + return [ + 'type' => ReportType::class, + 'status' => ReportStatus::class, + ]; + } +} diff --git a/app/Traits/EnumAsArray.php b/app/Traits/EnumAsArray.php index 12a991e..c6e71e0 100644 --- a/app/Traits/EnumAsArray.php +++ b/app/Traits/EnumAsArray.php @@ -8,4 +8,11 @@ public static function values(): array { return array_column(self::cases(), 'value'); } + + public static function assocValues(): array + { + return array_map(function ($enum) { + return ['name' => $enum->name, 'value' => $enum->value]; + }, self::cases()); + } } diff --git a/database/migrations/2026_01_16_091636_create_reports_table.php b/database/migrations/2026_01_16_091636_create_reports_table.php new file mode 100644 index 0000000..1dd539a --- /dev/null +++ b/database/migrations/2026_01_16_091636_create_reports_table.php @@ -0,0 +1,34 @@ +id(); + $table->enum('type', ReportType::values()); + $table->enum('status', ReportStatus::values())->default(ReportStatus::Pending); + $table->text('description'); + $table->foreignIdFor(User::class); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('reports'); + } +}; diff --git a/database/migrations/2026_01_16_093141_create_deal_report_table.php b/database/migrations/2026_01_16_093141_create_deal_report_table.php new file mode 100644 index 0000000..c65d2ea --- /dev/null +++ b/database/migrations/2026_01_16_093141_create_deal_report_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignIdFor(Deal::class); + $table->foreignIdFor(Report::class); + $table->timestamp('created_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('deal_report'); + } +}; diff --git a/resources/js/modal.js b/resources/js/modal.js new file mode 100644 index 0000000..74d3afc --- /dev/null +++ b/resources/js/modal.js @@ -0,0 +1,10 @@ +export function showModal(id){ + let modal = document.getElementById(id); + modal.showModal(); +} +export function closeModal(id){ + let modal = document.getElementById(id); + modal.close(); +} +document.showModal = showModal; +document.closeModal = closeModal; diff --git a/resources/js/report-deal.js b/resources/js/report-deal.js new file mode 100644 index 0000000..afa7744 --- /dev/null +++ b/resources/js/report-deal.js @@ -0,0 +1,60 @@ +import {closeModal, showModal} from './modal.js'; +import {showToast} from './toast.js'; +const reportModal = document.getElementById('report-modal'); +const reportForm = document.getElementById('report-form'); + + +function showReportModal(dealId, dealTitle) { + // Clear the fields + reportForm.reset(); + const oldErrors = reportForm.querySelectorAll('.text-red-500'); + oldErrors.forEach(error => error.remove()); + + reportModal.dataset.dealId = dealId; + const reportModalTitle = document.getElementById('report-modal-title'); + const reportModalId = document.getElementById('report-modal-id'); + reportModalTitle.innerText = dealTitle; + reportModalId.innerText = dealId; + showModal('report-modal'); +} + +reportForm.addEventListener('submit', async function (form) { + form.preventDefault(); + const formData = new FormData(this); + const dealId = reportModal.dataset.dealId + try { + + let response = await axios.post( + `report/${dealId}`, + formData + ); + + showToast('Report submitted. Thank you for keeping DealHub safe!'); + closeModal('report-modal') + + } catch (error) { + + if (error.response.status === 405) { + closeModal('report-modal'); + showToast('You already have reported this deal !'); + return; + } + + // Iterate over the all error messages spans and show validation errors + if (error.response.status === 422) { + + let errors = error.response.data.errors; + + Object.keys(errors).forEach(error => { + let errorField = reportForm.querySelector(`[name="${error}"]`) + const errorSpan = document.createElement('span'); + errorSpan.textContent = errors[error][0]; + errorSpan.classList.add('text-red-500'); + errorSpan.classList.add('text-sm'); + errorField.insertAdjacentElement('afterend', errorSpan); + }) + } + } +}); + +document.showReportModal = showReportModal; diff --git a/resources/js/toast.js b/resources/js/toast.js new file mode 100644 index 0000000..32b1c89 --- /dev/null +++ b/resources/js/toast.js @@ -0,0 +1,15 @@ +const toast = document.querySelector('.toast'); +const toastBtn = document.querySelector('#toast-btn'); +let toastMessage = toast.querySelector('#toast-message'); +export function showToast(message) { + toast.classList.toggle('hidden') + toastMessage.textContent = message; + setTimeout(() => { + toast.classList.toggle('hidden'); + }, 3000) +} +function hideToast() { + toast.classList.toggle('hidden') +} +document.hideToast = hideToast; +document.showToast = showToast; diff --git a/resources/views/components/dashboard/user/action-toolbar.blade.php b/resources/views/components/dashboard/user/action-toolbar.blade.php index e9b9b40..5324808 100644 --- a/resources/views/components/dashboard/user/action-toolbar.blade.php +++ b/resources/views/components/dashboard/user/action-toolbar.blade.php @@ -1,6 +1,6 @@ -@props(['id', 'like' => false, 'favourite' => false]) +@props(['deal_id', 'deal_title', 'like' => false, 'favourite' => false])
- $like]) onclick="like(this, {{$id}})"> + $like]) onclick="like(this, {{$deal_id}})"> - + - +
diff --git a/resources/views/components/dashboard/user/listing-card.blade.php b/resources/views/components/dashboard/user/listing-card.blade.php index 2aba423..8e56355 100644 --- a/resources/views/components/dashboard/user/listing-card.blade.php +++ b/resources/views/components/dashboard/user/listing-card.blade.php @@ -6,7 +6,7 @@ {{$deal->category->name}} @ds($deal) - +

{{$deal->title}}

diff --git a/resources/views/components/dashboard/user/report-modal.blade.php b/resources/views/components/dashboard/user/report-modal.blade.php new file mode 100644 index 0000000..446b04d --- /dev/null +++ b/resources/views/components/dashboard/user/report-modal.blade.php @@ -0,0 +1,36 @@ + +
+
+ + + +

Report Deal

+
+ +
+ +

Help us maintain a trusted platform by reporting inappropriate or suspicious content.

+

Deal being reported

+
+

+

ID: #

+
+ +
+ @csrf + + + + +

+ Note: Your report will be reviewed by out moderation team within 24-48 hours.
+ False reports may result in account restrictions. +

+
+ Cancel + Submit report +
+ +
diff --git a/resources/views/components/icon-square.blade.php b/resources/views/components/icon-square.blade.php index a60efb9..34f3771 100644 --- a/resources/views/components/icon-square.blade.php +++ b/resources/views/components/icon-square.blade.php @@ -2,12 +2,13 @@ @php $variants = [ 'blue' => "bg-blue-100 text-blue-700", + 'red' => 'bg-red-100 text-red-700', 'purple' => "bg-purple-100 text-purple-700", 'pink' => "bg-pink-100 text-pink-700", 'green' => "bg-green-100 text-green-700", 'ghost' => 'bg-gray-100 text-gray-900' ] @endphp -
+
merge(["class" => "$variants[$variant] p-4 w-fit rounded-xl"])}}> {{$slot}}
diff --git a/resources/views/components/ui/button.blade.php b/resources/views/components/ui/button.blade.php index 5615407..d35854a 100644 --- a/resources/views/components/ui/button.blade.php +++ b/resources/views/components/ui/button.blade.php @@ -18,7 +18,7 @@
@else -