Initial commit
This commit is contained in:
387
app/Http/Livewire/ReactionButton.php
Normal file
387
app/Http/Livewire/ReactionButton.php
Normal file
@@ -0,0 +1,387 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class ReactionButton extends Component
|
||||
{
|
||||
public $targetModel = null;
|
||||
public $modelClass = null;
|
||||
public $modelId = null;
|
||||
public $count = 0;
|
||||
public $activeProfile;
|
||||
public $reactedByReactor = false;
|
||||
public $typeName;
|
||||
public $showCounter = true;
|
||||
public $isEnabled = false;
|
||||
public $canReact = false;
|
||||
public $requiresInteraction = false;
|
||||
public $hasInteraction = false;
|
||||
public $interactionMethod = null;
|
||||
public $disabledReason = null;
|
||||
public $size = 'w-10 h-10'; // Default size, can be overridden
|
||||
public $showConfirmModal = false;
|
||||
public $inverseColors = false; // Use light/background colors instead of reaction colors
|
||||
public ?string $redirectUrl = null; // When set and user is not logged in, clicking redirects here
|
||||
|
||||
public function mount($typeName = 'like', $showCounter = true, $reactionCounter = null, $modelInstance = null, $modelClass = null, $modelId = null, $inverseColors = false, $redirectUrl = null)
|
||||
{
|
||||
$this->typeName = $typeName;
|
||||
$this->count = $reactionCounter ?? 0;
|
||||
$this->showCounter = $showCounter;
|
||||
$this->inverseColors = $inverseColors;
|
||||
$this->redirectUrl = $redirectUrl;
|
||||
$this->activeProfile = getActiveProfile();
|
||||
|
||||
// Handle different ways of passing model data
|
||||
if ($modelInstance !== null) {
|
||||
// Traditional way: full model instance passed
|
||||
$this->targetModel = $modelInstance;
|
||||
|
||||
// For reserve reactions on Posts, ensure meeting is loaded
|
||||
if ($typeName === 'reserve' && $modelInstance instanceof \App\Models\Post) {
|
||||
if (!$modelInstance->relationLoaded('meeting')) {
|
||||
$this->targetModel->load('meeting');
|
||||
}
|
||||
}
|
||||
} elseif ($modelClass !== null && $modelId !== null) {
|
||||
// New way: class and ID passed separately
|
||||
$this->modelClass = $modelClass;
|
||||
$this->modelId = $modelId;
|
||||
try {
|
||||
if ($typeName === 'reserve' && $modelClass === 'App\Models\Post') {
|
||||
$this->targetModel = $modelClass::with('meeting')->find($modelId);
|
||||
} else {
|
||||
$this->targetModel = $modelClass::find($modelId);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('ReactionButton: Failed to load model: ' . $e->getMessage());
|
||||
$this->targetModel = null;
|
||||
}
|
||||
} else {
|
||||
$this->targetModel = null;
|
||||
}
|
||||
|
||||
// If model not found or invalid, set component as disabled and return early
|
||||
if (!$this->targetModel || !is_object($this->targetModel)) {
|
||||
$this->isEnabled = false;
|
||||
$this->canReact = false;
|
||||
$this->disabledReason = 'model_not_found';
|
||||
return;
|
||||
}
|
||||
|
||||
$this->initializeReactionSettings();
|
||||
$this->checkReactionPermissions();
|
||||
$this->checkIfReacted();
|
||||
}
|
||||
|
||||
private function initializeReactionSettings()
|
||||
{
|
||||
// Check if this reaction type is enabled in config
|
||||
$this->isEnabled = timebank_config("reactions.{$this->typeName}.enabled", false);
|
||||
|
||||
// Check if interaction is required for this reaction type
|
||||
$this->requiresInteraction = timebank_config("reactions.{$this->typeName}.only_with_interaction", false);
|
||||
|
||||
// Get the interaction method to use for checking
|
||||
$this->interactionMethod = timebank_config("reactions.{$this->typeName}.interaction", 'hasTransactionsWith');
|
||||
}
|
||||
|
||||
private function checkReactionPermissions()
|
||||
{
|
||||
// If reaction type is not enabled, early return
|
||||
if (!$this->isEnabled) {
|
||||
$this->canReact = false;
|
||||
$this->disabledReason = 'reaction_disabled';
|
||||
return;
|
||||
}
|
||||
|
||||
// Guests cannot react
|
||||
if (!$this->activeProfile) {
|
||||
$this->canReact = false;
|
||||
$this->disabledReason = 'not_logged_in';
|
||||
return;
|
||||
}
|
||||
|
||||
// Admins cannot react
|
||||
if ($this->activeProfile instanceof \App\Models\Admin) {
|
||||
$this->canReact = false;
|
||||
$this->disabledReason = 'admin_cannot_react';
|
||||
return;
|
||||
}
|
||||
|
||||
// Profile must be registered as love reacter
|
||||
if (
|
||||
!method_exists($this->activeProfile, 'isRegisteredAsLoveReacter') ||
|
||||
!$this->activeProfile->isRegisteredAsLoveReacter()
|
||||
) {
|
||||
$this->canReact = false;
|
||||
$this->disabledReason = 'not_registered_reacter';
|
||||
return;
|
||||
}
|
||||
|
||||
// Cannot react to your own profile/model
|
||||
if ($this->isOwnProfile()) {
|
||||
$this->canReact = false;
|
||||
$this->disabledReason = 'cannot_react_own_profile';
|
||||
return;
|
||||
}
|
||||
|
||||
// Check interaction requirement
|
||||
if ($this->requiresInteraction) {
|
||||
// Use the configured interaction method
|
||||
if (method_exists($this->activeProfile, $this->interactionMethod)) {
|
||||
$this->hasInteraction = $this->activeProfile->{$this->interactionMethod}($this->targetModel);
|
||||
if (!$this->hasInteraction) {
|
||||
$this->canReact = false;
|
||||
$this->disabledReason = 'no_interaction';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Fallback if method doesn't exist - treat as no interaction
|
||||
$this->canReact = false;
|
||||
$this->disabledReason = 'no_interaction';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->canReact = true;
|
||||
$this->disabledReason = null;
|
||||
}
|
||||
|
||||
private function isOwnProfile()
|
||||
{
|
||||
// Safety check - ensure we have a valid model object
|
||||
if (!$this->targetModel || !is_object($this->targetModel) || !isset($this->targetModel->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the model class name safely
|
||||
$modelClass = get_class($this->targetModel);
|
||||
$modelId = $this->targetModel->id;
|
||||
|
||||
return session('activeProfileType') === $modelClass &&
|
||||
session('activeProfileId') === $modelId;
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('ReactionButton isOwnProfile error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function checkIfReacted()
|
||||
{
|
||||
if (!$this->canReact || !$this->activeProfile->isRegisteredAsLoveReacter()) {
|
||||
$this->reactedByReactor = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->reactedByReactor = $this->activeProfile
|
||||
->viaLoveReacter()
|
||||
->hasReactedTo($this->targetModel, $this->typeName);
|
||||
}
|
||||
|
||||
public function react()
|
||||
{
|
||||
// For reserve reaction type, show confirmation modal first
|
||||
if ($this->typeName === 'reserve') {
|
||||
$this->showConfirmModal = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$this->confirmReaction();
|
||||
}
|
||||
|
||||
public function confirmReaction()
|
||||
{
|
||||
// Close modal if open
|
||||
$this->showConfirmModal = false;
|
||||
|
||||
// Security checks
|
||||
if (!$this->canReactSecurityCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't react if already reacted
|
||||
if ($this->reactedByReactor) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->activeProfile->viaLoveReacter()->reactTo($this->targetModel, $this->typeName);
|
||||
|
||||
// Update count and reacted state
|
||||
$this->count++;
|
||||
$this->reactedByReactor = true;
|
||||
|
||||
// Optional: dispatch browser event for success feedback
|
||||
$this->dispatch('reaction-added', [
|
||||
'type' => $this->typeName,
|
||||
'model' => get_class($this->targetModel),
|
||||
'modelId' => $this->targetModel->id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// Log error and show user-friendly message
|
||||
\Log::error('Failed to add reaction: ' . $e->getMessage());
|
||||
$this->dispatch('reaction-error', ['message' => 'Failed to add reaction. Please try again.']);
|
||||
}
|
||||
}
|
||||
|
||||
public function unReact()
|
||||
{
|
||||
// Security checks
|
||||
if (!$this->canReactSecurityCheck()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't unreact if not reacted
|
||||
if (!$this->reactedByReactor) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->activeProfile->viaLoveReacter()->unReactTo($this->targetModel, $this->typeName);
|
||||
$this->count = max(0, $this->count - 1); // Prevent negative counts
|
||||
$this->reactedByReactor = false;
|
||||
|
||||
// Optional: dispatch browser event for success feedback
|
||||
$this->dispatch('reaction-removed', [
|
||||
'type' => $this->typeName,
|
||||
'model' => get_class($this->targetModel),
|
||||
'modelId' => $this->targetModel->id
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
// Log error and show user-friendly message
|
||||
\Log::error('Failed to remove reaction: ' . $e->getMessage());
|
||||
$this->dispatch('reaction-error', ['message' => 'Failed to remove reaction. Please try again.']);
|
||||
}
|
||||
}
|
||||
|
||||
private function canReactSecurityCheck()
|
||||
{
|
||||
// Re-run all security checks on each action for security
|
||||
if (!$this->targetModel || (method_exists($this->targetModel, 'isRemoved') && $this->targetModel->isRemoved())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->isEnabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->activeProfile instanceof \App\Models\Admin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
!method_exists($this->activeProfile, 'isRegisteredAsLoveReacter') ||
|
||||
!$this->activeProfile->isRegisteredAsLoveReacter()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isOwnProfile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
$this->requiresInteraction &&
|
||||
method_exists($this->activeProfile, $this->interactionMethod) &&
|
||||
!$this->activeProfile->{$this->interactionMethod}($this->targetModel)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function getDisabledReasonText()
|
||||
{
|
||||
// Only show text for specific disabled reasons that should be visible to users
|
||||
$visibleReasons = [
|
||||
'cannot_react_own_profile',
|
||||
'no_interaction'
|
||||
];
|
||||
|
||||
if (!in_array($this->disabledReason, $visibleReasons)) {
|
||||
return null; // Don't show text for other reasons
|
||||
}
|
||||
|
||||
// Get reaction-specific disabled reason text
|
||||
$reason = timebank_config("reactions.{$this->typeName}.disabled_reasons.{$this->disabledReason}");
|
||||
|
||||
// Fallback to default messages if reaction-specific ones don't exist
|
||||
if (!$reason) {
|
||||
$fallbackReasons = [
|
||||
'cannot_react_own_profile' => __('You cannot react to your own content.'),
|
||||
'no_interaction' => __('You need an interaction to react.'),
|
||||
];
|
||||
$reason = $fallbackReasons[$this->disabledReason] ?? '';
|
||||
}
|
||||
|
||||
return $reason;
|
||||
}
|
||||
|
||||
private function shouldShowComponent()
|
||||
{
|
||||
// Hide component if model is not loaded
|
||||
if (!$this->targetModel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hide component for these reasons instead of showing disabled state
|
||||
$hiddenReasons = [
|
||||
'reaction_disabled',
|
||||
'admin_cannot_react',
|
||||
'not_registered_reacter',
|
||||
'model_not_found'
|
||||
];
|
||||
|
||||
return !in_array($this->disabledReason, $hiddenReasons);
|
||||
}
|
||||
|
||||
private function getIconSvg($typeName)
|
||||
{
|
||||
// Get icon file from config
|
||||
$iconFile = timebank_config("reactions.{$typeName}.icon_file");
|
||||
|
||||
if (!$iconFile) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$svgPath = public_path('storage/app-images/' . $iconFile);
|
||||
|
||||
if (!file_exists($svgPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$svgContent = file_get_contents($svgPath);
|
||||
|
||||
// Extract only the path content from the SVG (remove outer svg tag)
|
||||
if (preg_match('/<path[^>]*>.*?<\/path>|<path[^>]*\/>/s', $svgContent, $matches)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
// If no path found, try to extract circle or other shapes
|
||||
if (preg_match('/<(?:circle|rect|polygon|line)[^>]*>.*?<\/(?:circle|rect|polygon|line)>|<(?:circle|rect|polygon|line)[^>]*\/>/s', $svgContent, $matches)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
return $svgContent;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$iconSvg = $this->getIconSvg($this->typeName);
|
||||
$shouldShow = $this->shouldShowComponent();
|
||||
$disabledReasonText = $this->getDisabledReasonText();
|
||||
|
||||
return view('livewire.reaction-button', [
|
||||
'iconSvg' => $iconSvg,
|
||||
'disabledReasonText' => $disabledReasonText,
|
||||
'shouldShow' => $shouldShow,
|
||||
'inverseColors' => $this->inverseColors,
|
||||
'redirectUrl' => $this->redirectUrl,
|
||||
])->layout('layouts.app');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user