Add unsubscribe confirmation page
- Created Unsubscribe.vue with matching site styling
- Added showUnsubscribe method to SubscriptionController
- Updated routes: GET /unsubscribe/{token} shows confirmation page
- POST /api/unsubscribe/{token} processes unsubscribe
- Updated email template to use new unsubscribe route
- Prevents accidental unsubscribes with confirmation dialog
This commit is contained in:
parent
80b5adf3f4
commit
474df2aa61
4 changed files with 174 additions and 3 deletions
|
|
@ -62,7 +62,24 @@ class SubscriptionController extends Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe using token.
|
* Show unsubscribe confirmation page.
|
||||||
|
*/
|
||||||
|
public function showUnsubscribe(string $token)
|
||||||
|
{
|
||||||
|
$subscription = Subscription::where('unsubscribe_token', $token)->first();
|
||||||
|
|
||||||
|
if (!$subscription) {
|
||||||
|
abort(404, 'Invalid unsubscribe token.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return inertia('Unsubscribe', [
|
||||||
|
'token' => $token,
|
||||||
|
'email' => $subscription->email,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process unsubscribe request.
|
||||||
*/
|
*/
|
||||||
public function unsubscribe(Request $request, string $token): JsonResponse
|
public function unsubscribe(Request $request, string $token): JsonResponse
|
||||||
{
|
{
|
||||||
|
|
|
||||||
153
resources/js/Pages/Unsubscribe.vue
Normal file
153
resources/js/Pages/Unsubscribe.vue
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Head, router } from '@inertiajs/vue3';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
token: string;
|
||||||
|
email?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const isProcessing = ref(false);
|
||||||
|
const message = ref('');
|
||||||
|
const isSuccess = ref(false);
|
||||||
|
|
||||||
|
const confirmUnsubscribe = async () => {
|
||||||
|
isProcessing.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/unsubscribe/${props.token}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
isSuccess.value = true;
|
||||||
|
message.value = data.message;
|
||||||
|
} else {
|
||||||
|
isSuccess.value = false;
|
||||||
|
message.value = data.message;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
isSuccess.value = false;
|
||||||
|
message.value = 'An error occurred. Please try again.';
|
||||||
|
} finally {
|
||||||
|
isProcessing.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
window.location.href = '/';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Head title="Unsubscribe - Court Docket Notifications" />
|
||||||
|
|
||||||
|
<div class="min-h-screen flex items-center justify-center px-4" style="background-color: #6E6362;">
|
||||||
|
<div class="max-w-md w-full">
|
||||||
|
<!-- Unsubscribe Card -->
|
||||||
|
<div class="bg-white rounded-lg shadow-lg p-8">
|
||||||
|
<div v-if="!message" class="text-center">
|
||||||
|
<!-- Icon -->
|
||||||
|
<div class="mx-auto w-16 h-16 rounded-full flex items-center justify-center mb-4"
|
||||||
|
style="background-color: #FEF3C7;">
|
||||||
|
<svg class="w-8 h-8 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">
|
||||||
|
Unsubscribe from Notifications?
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
Are you sure you want to unsubscribe from email notifications for court docket updates?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p v-if="email" class="text-sm text-gray-500 mb-6">
|
||||||
|
Email: <span class="font-semibold">{{ email }}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<button
|
||||||
|
@click="confirmUnsubscribe"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
class="w-full px-6 py-3 rounded-lg font-semibold text-white transition-colors"
|
||||||
|
:class="isProcessing ? 'opacity-50 cursor-not-allowed' : ''"
|
||||||
|
style="background-color: #EF4444;"
|
||||||
|
@mouseover="!isProcessing && (($event.target as HTMLElement).style.backgroundColor = '#DC2626')"
|
||||||
|
@mouseout="!isProcessing && (($event.target as HTMLElement).style.backgroundColor = '#EF4444')"
|
||||||
|
>
|
||||||
|
{{ isProcessing ? 'Processing...' : 'Yes, Unsubscribe' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="goHome"
|
||||||
|
:disabled="isProcessing"
|
||||||
|
class="w-full px-6 py-3 rounded-lg font-semibold transition-colors"
|
||||||
|
:class="isProcessing ? 'opacity-50 cursor-not-allowed' : ''"
|
||||||
|
style="background-color: #6b9080; color: white;"
|
||||||
|
@mouseover="!isProcessing && (($event.target as HTMLElement).style.backgroundColor = '#5a8070')"
|
||||||
|
@mouseout="!isProcessing && (($event.target as HTMLElement).style.backgroundColor = '#6b9080')"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Success/Error Message -->
|
||||||
|
<div v-else class="text-center">
|
||||||
|
<!-- Success Icon -->
|
||||||
|
<div v-if="isSuccess" class="mx-auto w-16 h-16 rounded-full flex items-center justify-center mb-4"
|
||||||
|
style="background-color: #D1FAE5;">
|
||||||
|
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Icon -->
|
||||||
|
<div v-else class="mx-auto w-16 h-16 rounded-full flex items-center justify-center mb-4"
|
||||||
|
style="background-color: #FEE2E2;">
|
||||||
|
<svg class="w-8 h-8 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 mb-2">
|
||||||
|
{{ isSuccess ? 'Unsubscribed Successfully' : 'Error' }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="text-gray-600 mb-6">
|
||||||
|
{{ message }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="goHome"
|
||||||
|
class="w-full px-6 py-3 rounded-lg font-semibold text-white transition-colors"
|
||||||
|
style="background-color: #6b9080;"
|
||||||
|
@mouseover="($event.target as HTMLElement).style.backgroundColor = '#5a8070'"
|
||||||
|
@mouseout="($event.target as HTMLElement).style.backgroundColor = '#6b9080'"
|
||||||
|
>
|
||||||
|
Return to Home
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="text-center mt-6">
|
||||||
|
<p class="text-sm text-gray-300">
|
||||||
|
You can resubscribe anytime from the homepage
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -145,7 +145,7 @@
|
||||||
Court Docket Notification System
|
Court Docket Notification System
|
||||||
</p>
|
</p>
|
||||||
<p style="margin-top: 10px;">
|
<p style="margin-top: 10px;">
|
||||||
<a href="{{ config('app.url') }}/unsubscribe/{{ $unsubscribeToken }}">Unsubscribe from notifications</a>
|
<a href="{{ url('/unsubscribe/' . $unsubscribeToken) }}">Unsubscribe from notifications</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ Route::get('/', [HomeController::class, 'index'])->name('home');
|
||||||
|
|
||||||
// Subscription API routes
|
// Subscription API routes
|
||||||
Route::post('/api/subscribe', [SubscriptionController::class, 'subscribe'])->name('api.subscribe');
|
Route::post('/api/subscribe', [SubscriptionController::class, 'subscribe'])->name('api.subscribe');
|
||||||
Route::get('/api/unsubscribe/{token}', [SubscriptionController::class, 'unsubscribe'])->name('api.unsubscribe');
|
Route::get('/unsubscribe/{token}', [SubscriptionController::class, 'showUnsubscribe'])->name('unsubscribe');
|
||||||
|
Route::post('/api/unsubscribe/{token}', [SubscriptionController::class, 'unsubscribe'])->name('api.unsubscribe');
|
||||||
|
|
||||||
// Document download route
|
// Document download route
|
||||||
Route::get('/api/documents/{id}/download', [DocumentController::class, 'download'])->name('api.documents.download');
|
Route::get('/api/documents/{id}/download', [DocumentController::class, 'download'])->name('api.documents.download');
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue