Skip to content

Commit

Permalink
feat: Add experiment feedback form
Browse files Browse the repository at this point in the history
  • Loading branch information
lewislarsen committed Aug 19, 2024
1 parent 66680bd commit 9dd1d0f
Showing 1 changed file with 207 additions and 17 deletions.
224 changes: 207 additions & 17 deletions resources/views/livewire/profile/experiments-manager.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@
use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Validation\ValidationException;
use Laravel\Pennant\Feature;
use Livewire\Attributes\Computed;
use Livewire\Volt\Component;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Http;
/**
* Experiment Management Component
*
* Handles the display and management of feature experiments.
* Allows users to view, enable, and disable experiments.
* Allows users to view, enable, disable experiments, and provide feedback.
*/
new class extends Component
{
new class extends Component {
/**
* The current view state of the component.
* Can be 'list' or 'no-content'.
*/
public string $currentView = 'list';
Expand All @@ -26,11 +26,32 @@
*/
public ?string $selectedExperiment = null;
/**
* The feedback text entered by the user.
*/
public string $feedbackText = '';
/**
* The email address entered by the user for feedback.
*/
public string $feedbackEmail = '';
/**
* Controls the visibility of the feedback modal.
*/
public bool $showFeedbackModal = false;
/**
* Initialize the component state.
*/
public function mount(): void
{
$this->currentView = $this->hasExperiments ? 'list' : 'no-content';
}
/**
* Determine if there are any experiments available.
*/
#[Computed]
public function hasExperiments(): bool
{
Expand All @@ -39,6 +60,9 @@ public function hasExperiments(): bool
return $hasExperiments;
}
/**
* Retrieve and process all available experiments.
*/
#[Computed]
public function experiments(): Collection
{
Expand All @@ -53,6 +77,9 @@ public function experiments(): Collection
return $experiments;
}
/**
* Toggle the active state of an experiment for the current user.
*/
public function toggleExperiment(string $experiment): void
{
$user = $this->getCurrentUser();
Expand All @@ -71,18 +98,109 @@ public function toggleExperiment(string $experiment): void
$this->dispatch('experiment-toggled');
}
/**
* Switch to the experiments list view.
*/
public function viewExperiments(): void
{
$this->currentView = 'list';
}
/**
* Open the details modal for a specific experiment.
*/
public function viewExperimentDetails(string $experiment): void
{
$this->selectedExperiment = $experiment;
Log::debug('Viewing experiment details', ['experiment' => $experiment]);
$this->dispatch('open-modal', 'experiment-details');
}
/**
* Open the feedback modal and close the experiment details modal.
*/
public function openFeedbackModal(): void
{
$this->showFeedbackModal = true;
$this->dispatch('close-modal', 'experiment-details');
$this->dispatch('open-modal', 'experiment-feedback');
}
/**
* Submit user feedback for the selected experiment.
*/
public function submitFeedback(): void
{
$this->validate([
'selectedExperiment' => 'required|string|max:255',
'feedbackText' => 'required|string|max:10000',
'feedbackEmail' => 'nullable|email|max:255',
]);
$feedbackServiceUrl = 'https://feedback.vanguardbackup.com/api/feedback';
try {
$response = Http::post($feedbackServiceUrl, [
'experiment' => trim($this->selectedExperiment),
'feedback' => trim($this->feedbackText),
'php_version' => trim(phpversion()),
'vanguard_version' => trim(obtain_vanguard_version()),
'email_address' => $this->feedbackEmail ? trim($this->feedbackEmail) : null,
]);
if ($response->successful() && $response->json('status') === 'success') {
Toaster::success($response->json('message', 'Thank you for your feedback!'));
$this->resetFeedbackForm();
} elseif ($response->status() === 422) {
$this->handleValidationErrors($response->json('errors'));
} elseif ($response->status() === 429) {
Toaster::error('Too many requests. Please try again later.');
} else {
throw new RuntimeException('Unexpected response from feedback service');
}
} catch (Exception $e) {
Log::error('Failed to submit feedback', ['error' => $e->getMessage()]);
Toaster::error('Failed to submit feedback. Please try again later.');
}
}
/**
* Reset the feedback form after successful submission.
*/
private function resetFeedbackForm(): void
{
$this->feedbackText = '';
$this->feedbackEmail = '';
$this->showFeedbackModal = false;
$this->dispatch('close-modal', 'experiment-feedback');
$this->dispatch('open-modal', 'experiment-details');
}
/**
* Handle validation errors from the API response.
*/
private function handleValidationErrors(array $errors): void
{
$messages = [];
foreach ($errors as $field => $errorMessages) {
$messages[$field] = implode(' ', $errorMessages);
}
throw ValidationException::withMessages($messages);
}
/**
* Close the feedback modal and reopen the experiment details modal.
*/
public function closeFeedbackModal(): void
{
$this->showFeedbackModal = false;
$this->dispatch('close-modal', 'experiment-feedback');
$this->dispatch('open-modal', 'experiment-details');
}
/**
* Retrieve detailed information about a specific experiment.
*/
private function getExperimentDetails(string $experiment): array
{
$user = $this->getCurrentUser();
Expand All @@ -109,16 +227,25 @@ private function getExperimentDetails(string $experiment): array
];
}
/**
* Generate a human-readable title for an experiment.
*/
private function getExperimentTitle(string $experiment): string
{
return ucfirst(str_replace('-', ' ', $experiment));
}
/**
* Generate a default description for an experiment.
*/
private function getExperimentDescription(string $experiment): string
{
return "This is the {$this->getExperimentTitle($experiment)} experiment.";
}
/**
* Retrieve the current authenticated user.
*/
private function getCurrentUser(): User
{
$user = auth()->user();
Expand Down Expand Up @@ -159,15 +286,18 @@ private function getCurrentUser(): User
<ul class="space-y-3">
<li class="flex items-start">
@svg('heroicon-o-check-circle', 'w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0')
<span class="text-blue-700 dark:text-blue-300">{{ __('Test new features before they\'re widely available') }}</span>
<span
class="text-blue-700 dark:text-blue-300">{{ __('Test new features before they\'re widely available') }}</span>
</li>
<li class="flex items-start">
@svg('heroicon-o-check-circle', 'w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0')
<span class="text-blue-700 dark:text-blue-300">{{ __('Provide valuable feedback to improve features') }}</span>
<span
class="text-blue-700 dark:text-blue-300">{{ __('Provide valuable feedback to improve features') }}</span>
</li>
<li class="flex items-start">
@svg('heroicon-o-check-circle', 'w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0')
<span class="text-blue-700 dark:text-blue-300">{{ __('Customize your experience by enabling or disabling specific experiments') }}</span>
<span
class="text-blue-700 dark:text-blue-300">{{ __('Customize your experience by enabling or disabling specific experiments') }}</span>
</li>
</ul>
</div>
Expand All @@ -177,7 +307,8 @@ private function getCurrentUser(): User
<div class="mb-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">{{ $group }}</h3>
@foreach ($groupExperiments as $experiment)
<div class="border border-gray-200 dark:border-gray-600 rounded-lg transition-all duration-200 overflow-hidden mb-4">
<div
class="border border-gray-200 dark:border-gray-600 rounded-lg transition-all duration-200 overflow-hidden mb-4">
<div class="p-6">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center mb-4 sm:mb-0">
Expand Down Expand Up @@ -232,33 +363,92 @@ class="mr-3"
{{ $experiment['description'] }}
</p>
<div class="flex items-center mb-2">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 mr-2">{{ __('Status:') }}</span>
<span class="px-2 py-1 text-xs font-semibold rounded-full {{ $experiment['enabled'] ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
<span
class="text-sm font-medium text-gray-600 dark:text-gray-400 mr-2">{{ __('Status:') }}</span>
<span
class="px-2 py-1 text-xs font-semibold rounded-full {{ $experiment['enabled'] ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $experiment['enabled'] ? __('Enabled') : __('Disabled') }}
</span>
</div>
<div class="flex items-center mb-2">
<span class="text-sm font-medium text-gray-600 dark:text-gray-400 mr-2">{{ __('Group:') }}</span>
<span
class="text-sm font-medium text-gray-600 dark:text-gray-400 mr-2">{{ __('Group:') }}</span>
<span class="text-sm text-gray-900 dark:text-gray-100">{{ $experiment['group'] }}</span>
</div>
</div>

<div class="mb-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-md">
<div
class="mb-4 p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-md">
<p class="text-sm text-yellow-700 dark:text-yellow-200">
<span class="font-medium">{{ __('Note:') }}</span>
{{ __('You may need to reload the page (F5 or Cmd/Ctrl + R) to see the effects of enabling or disabling an experiment.') }}
</p>
</div>

<div class="mt-6 flex justify-between items-center">
<button
wire:click="openFeedbackModal"
class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-0"
>
@svg('heroicon-o-chat-bubble-left-right', 'w-5 h-5 mr-2 -ml-1')
{{ __('Give Feedback') }}
</button>
<div class="flex space-x-3">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Close') }}
</x-secondary-button>
<x-primary-button wire:click="toggleExperiment('{{ $experiment['name'] }}')">
{{ $experiment['enabled'] ? __('Disable Experiment') : __('Enable Experiment') }}
</x-primary-button>
</div>
</div>
@endif
</x-modal>

<x-modal name="experiment-feedback" :show="$showFeedbackModal" focusable>
<x-slot name="title">
{{ __('Provide Feedback') }}
</x-slot>
<x-slot name="description">
{{ __('Share your thoughts on this experiment to help us improve.') }}
</x-slot>
<x-slot name="icon">
heroicon-o-chat-bubble-left-right
</x-slot>

<form wire:submit.prevent="submitFeedback">
<div class="mt-4">
<x-textarea
wire:model="feedbackText"
id="feedback"
class="mt-1 block w-full"
rows="4"
placeholder="{{ __('Your feedback here...') }}"
></x-textarea>
<x-input-error :messages="$errors->get('feedbackText')" class="mt-2" />
<x-text-input
name="feedbackEmail"
wire:model="feedbackEmail"
id="feedbackEmail"
class="mt-3 block w-full"
type="email"
placeholder="{{ __('Your email address (optional)') }}"
/>
<x-input-error :messages="$errors->get('feedbackEmail')" class="mt-2" />
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
{{ __('Note: Your Vanguard version and PHP version will be shared with this feedback. No other information will be included.') }}
</p>
</div>

<div class="mt-6 flex justify-end space-x-3">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Close') }}
<x-secondary-button wire:click="closeFeedbackModal" type="button">
{{ __('Cancel') }}
</x-secondary-button>
<x-primary-button wire:click="toggleExperiment('{{ $experiment['name'] }}')">
{{ $experiment['enabled'] ? __('Disable Experiment') : __('Enable Experiment') }}
<x-primary-button type="submit" action="submitFeedback" loadingText="Submitting...">
{{ __('Submit Feedback') }}
</x-primary-button>
</div>
@endif
</form>
</x-modal>
@endif
</div>

0 comments on commit 9dd1d0f

Please sign in to comment.