Initial commit
This commit is contained in:
172
app/Http/Livewire/MainPage/ArticleCardFull.php
Normal file
172
app/Http/Livewire/MainPage/ArticleCardFull.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\News;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Livewire\Component;
|
||||
|
||||
class ArticleCardFull extends Component
|
||||
{
|
||||
public $author;
|
||||
public $post = [];
|
||||
public $posts;
|
||||
public $media;
|
||||
public $postNr;
|
||||
public $related;
|
||||
|
||||
|
||||
public function mount($postNr, $related, Request $request)
|
||||
{
|
||||
$this->postNr = $postNr;
|
||||
$this->related = $related;
|
||||
|
||||
|
||||
$profile = getActiveProfile();
|
||||
|
||||
// SECURITY: If a profile is active, validate the user has access to it
|
||||
// This prevents session manipulation while allowing public access (null profile)
|
||||
if ($profile) {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
}
|
||||
|
||||
$location = $profile && $profile->locations ? $profile->locations()->first() : null;
|
||||
|
||||
$skipLocationFilter = false;
|
||||
|
||||
if ($location) {
|
||||
// If no division and no city as location set
|
||||
if (!$location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->country->id;
|
||||
$categoryable_type = Country::class;
|
||||
// Include also all other countries if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Country::pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// Division without city is set as location
|
||||
} elseif ($location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->division->id;
|
||||
$categoryable_type = Division::class;
|
||||
// Include also all other divisions in the same country if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Division::find($categoryable_id)->parent->divisions->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// City is set as location
|
||||
} elseif ($location->city) {
|
||||
$categoryable_id = Location::find($location->id)->city->id;
|
||||
$categoryable_type = City::class;
|
||||
// Include also all other cities in the same division if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = City::find($categoryable_id)->parent->cities->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
}
|
||||
// No matching location is set - skip location filtering
|
||||
} else {
|
||||
$skipLocationFilter = true;
|
||||
$categoryable_id = [];
|
||||
$categoryable_type = '';
|
||||
}
|
||||
|
||||
|
||||
// TODO: check what happens when multiple locations per user are used!
|
||||
|
||||
$post =
|
||||
Post::with([
|
||||
'postable' => function ($query) {
|
||||
$query->select(['id', 'name']);
|
||||
},
|
||||
'category' => function ($query) use ($categoryable_id, $categoryable_type, $skipLocationFilter) {
|
||||
$query->where('type', 'App\Models\Article');
|
||||
if (!$skipLocationFilter && !empty($categoryable_id)) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
})->orWhere(function ($query) {
|
||||
// Also include categories with no location set
|
||||
$query->whereNull('categoryable_id')
|
||||
->whereNull('categoryable_type');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
'translations' => function ($query) {
|
||||
$query->where('locale', App::getLocale());
|
||||
},
|
||||
'author',
|
||||
'media',
|
||||
])
|
||||
->whereHas('category', function ($query) use ($categoryable_id, $categoryable_type, $skipLocationFilter) {
|
||||
$query->where('type', 'App\Models\Article');
|
||||
if (!$skipLocationFilter && !empty($categoryable_id)) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
})->orWhere(function ($query) {
|
||||
// Also include categories with no location set
|
||||
$query->whereNull('categoryable_id')
|
||||
->whereNull('categoryable_type');
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
->whereHas('translations', function ($query) {
|
||||
$query
|
||||
->where('locale', App::getLocale())
|
||||
->whereDate('from', '<=', now())
|
||||
->where(function ($query) {
|
||||
$query->whereDate('till', '>', now())->orWhereNull('till');
|
||||
})
|
||||
->orderBy('updated_at', 'desc');
|
||||
})
|
||||
->get()->sortByDesc(function ($query) {
|
||||
if (isset($query->translations)) {
|
||||
return $query->translations->first()->updated_at;
|
||||
};
|
||||
})->values(); // Use values() method to reset the collection keys after sortBy
|
||||
|
||||
|
||||
$lastNr = $post->count() - 1;
|
||||
if ($postNr > $lastNr) {
|
||||
$post = null;
|
||||
} else {
|
||||
$post = $post[$postNr];
|
||||
}
|
||||
|
||||
if (isset($post->translations)) {
|
||||
$translation = $post->translations->first();
|
||||
$this->post = $translation;
|
||||
$this->post['from'] = $translation->from;
|
||||
$this->post['category'] = Category::find($post->category_id)->translations->where('locale', App::getLocale())->first()->name;
|
||||
$this->post['author'] = $post->author ? $post->author->name : timebank_config('posts.site-content-writer');
|
||||
$this->post['post_id'] = $post->id;
|
||||
|
||||
if ($post->media) {
|
||||
$this->media = Post::find($post->id)->getFirstMedia('posts');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.article-card-full');
|
||||
}
|
||||
}
|
||||
226
app/Http/Livewire/MainPage/CallCardCarousel.php
Normal file
226
app/Http/Livewire/MainPage/CallCardCarousel.php
Normal file
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Http\Livewire\Calls\CallCarouselScorer;
|
||||
use App\Http\Livewire\Calls\ProfileCalls;
|
||||
use App\Models\Call;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Locations\Location;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class CallCardCarousel extends Component
|
||||
{
|
||||
public array $calls = [];
|
||||
public bool $related = true;
|
||||
public bool $random = false;
|
||||
public int $maxCards = 0;
|
||||
public bool $showScore = false;
|
||||
private const UNKNOWN_COUNTRY_ID = 10;
|
||||
|
||||
public function mount(bool $related, bool $random = false, int $maxCards = 0): void
|
||||
{
|
||||
$this->related = $related;
|
||||
$this->random = $random;
|
||||
$this->maxCards = $maxCards;
|
||||
|
||||
$carouselCfg = timebank_config('calls.carousel', []);
|
||||
$this->maxCards = (int) ($carouselCfg['max_cards'] ?? ($maxCards ?: 12));
|
||||
$locale = App::getLocale();
|
||||
|
||||
// --- Resolve active profile and its location ---
|
||||
$profile = getActiveProfile();
|
||||
if ($profile) {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
}
|
||||
|
||||
$location = $profile?->locations ? $profile->locations()->first() : null;
|
||||
$profileCityId = $location ? ($location->city_id ?? $location->city?->id) : null;
|
||||
$profileDivisionId = $location ? ($location->division_id ?? $location->division?->id) : null;
|
||||
$profileCountryId = $location ? ($location->country_id ?? $location->country?->id) : null;
|
||||
|
||||
// Expand location IDs based on $related flag (same sibling logic as EventCardFull)
|
||||
$locationCityIds = [];
|
||||
$locationDivisionIds = [];
|
||||
$locationCountryIds = [];
|
||||
|
||||
if ($location) {
|
||||
if (!$location->division && !$location->city) {
|
||||
$country = Location::find($location->id)->country;
|
||||
$locationCountryIds = $related
|
||||
? Country::pluck('id')->toArray()
|
||||
: ($country ? [$country->id] : []);
|
||||
} elseif ($location->division && !$location->city) {
|
||||
$divisionId = Location::find($location->id)->division->id;
|
||||
$locationDivisionIds = $related
|
||||
? Division::find($divisionId)->parent->divisions->pluck('id')->toArray()
|
||||
: [$divisionId];
|
||||
} elseif ($location->city) {
|
||||
$cityId = Location::find($location->id)->city->id;
|
||||
$locationCityIds = $related
|
||||
? City::find($cityId)->parent->cities->pluck('id')->toArray()
|
||||
: [$cityId];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Base query — safety exclusions are always hardcoded ---
|
||||
$query = Call::with([
|
||||
'tag.contexts.category.translations',
|
||||
'tag.contexts.category.ancestors.translations',
|
||||
'translations',
|
||||
'location.city.translations',
|
||||
'location.country.translations',
|
||||
'callable.locations.city.translations',
|
||||
'callable.locations.division.translations',
|
||||
'callable.locations.country.translations',
|
||||
'callable.loveReactant.reactionCounters',
|
||||
'loveReactant.reactionCounters',
|
||||
])
|
||||
->whereNull('deleted_at')
|
||||
->where('is_paused', false)
|
||||
->where('is_suppressed', false)
|
||||
->where(fn ($q) => $q->whereNull('till')->orWhere('till', '>=', now()));
|
||||
|
||||
// Enforce is_public for guests or when config requires it
|
||||
if (!Auth::check() || ($carouselCfg['exclude_non_public'] ?? true)) {
|
||||
$query->where('is_public', true);
|
||||
}
|
||||
|
||||
// Configurable: exclude the active profile's own calls
|
||||
if ($profile && ($carouselCfg['exclude_own_calls'] ?? true)) {
|
||||
$query->where(function ($q) use ($profile) {
|
||||
$q->where('callable_type', '!=', get_class($profile))
|
||||
->orWhere('callable_id', '!=', $profile->id);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Locality filter ---
|
||||
$includeUnknown = $carouselCfg['include_unknown_location'] ?? true;
|
||||
$includeDivision = $carouselCfg['include_same_division'] ?? true;
|
||||
$includeCountry = $carouselCfg['include_same_country'] ?? true;
|
||||
|
||||
$hasLocalityFilter = $locationCityIds || $locationDivisionIds || $locationCountryIds
|
||||
|| ($includeDivision && $locationDivisionIds)
|
||||
|| ($includeCountry && $locationCountryIds)
|
||||
|| $includeUnknown;
|
||||
|
||||
if ($hasLocalityFilter) {
|
||||
$query->where(function ($q) use (
|
||||
$locationCityIds, $locationDivisionIds, $locationCountryIds,
|
||||
$includeDivision, $includeCountry, $includeUnknown
|
||||
) {
|
||||
// Always include calls matching the profile's city
|
||||
if ($locationCityIds) {
|
||||
$q->orWhereHas('location', fn ($lq) => $lq->whereIn('city_id', $locationCityIds));
|
||||
}
|
||||
if ($includeDivision && $locationDivisionIds) {
|
||||
$q->orWhereHas('location', fn ($lq) => $lq->whereIn('division_id', $locationDivisionIds));
|
||||
}
|
||||
if ($includeCountry && $locationCountryIds) {
|
||||
$q->orWhereHas('location', fn ($lq) => $lq->whereIn('country_id', $locationCountryIds));
|
||||
}
|
||||
if ($includeUnknown) {
|
||||
$q->orWhereHas('location', fn ($lq) => $lq->where('country_id', self::UNKNOWN_COUNTRY_ID));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch pool for scoring
|
||||
$isAdmin = getActiveProfileType() === 'Admin';
|
||||
$this->showScore = (bool) ($carouselCfg['show_score'] ?? false)
|
||||
|| ($isAdmin && (bool) ($carouselCfg['show_score_for_admins'] ?? true));
|
||||
$poolSize = $this->maxCards * max(1, (int) ($carouselCfg['pool_multiplier'] ?? 5));
|
||||
$calls = $query->limit($poolSize)->get();
|
||||
|
||||
// --- Score, sort, take top $maxCards ---
|
||||
$scorer = new CallCarouselScorer(
|
||||
$carouselCfg,
|
||||
$profileCityId,
|
||||
$profileDivisionId,
|
||||
$profileCountryId
|
||||
);
|
||||
|
||||
$this->calls = $calls->map(function (Call $call) use ($locale, $scorer) {
|
||||
$translation = $call->translations->firstWhere('locale', $locale)
|
||||
?? $call->translations->first();
|
||||
|
||||
$tag = $call->tag;
|
||||
$tagContext = $tag?->contexts->first();
|
||||
$tagCategory = $tagContext?->category;
|
||||
$tagColor = $tagCategory?->relatedColor ?? 'gray';
|
||||
$tagName = $tag?->translation?->name ?? $tag?->name;
|
||||
|
||||
$locationStr = null;
|
||||
if ($call->location) {
|
||||
$loc = $call->location;
|
||||
$parts = [];
|
||||
if ($loc->city) {
|
||||
$cityName = optional($loc->city->translations->first())->name;
|
||||
if ($cityName) $parts[] = $cityName;
|
||||
}
|
||||
if ($loc->country) {
|
||||
if ($loc->country->code === 'XX') {
|
||||
$parts[] = __('Location not specified');
|
||||
} elseif ($loc->country->code) {
|
||||
$parts[] = strtoupper($loc->country->code);
|
||||
}
|
||||
}
|
||||
$locationStr = $parts ? implode(', ', $parts) : null;
|
||||
}
|
||||
|
||||
$tagCategories = [];
|
||||
if ($tagCategory) {
|
||||
$ancestors = $tagCategory->ancestorsAndSelf()->get()->reverse();
|
||||
foreach ($ancestors as $cat) {
|
||||
$catName = $cat->translations->firstWhere('locale', $locale)?->name
|
||||
?? $cat->translations->first()?->name
|
||||
?? '';
|
||||
if ($catName) {
|
||||
$tagCategories[] = [
|
||||
'name' => $catName,
|
||||
'color' => $cat->relatedColor ?? 'gray',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$score = $scorer->score($call);
|
||||
|
||||
// Add random jitter when $random=true to vary order while preserving scoring preference
|
||||
if ($this->random) {
|
||||
$score *= (random_int(85, 115) / 100);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $call->id,
|
||||
'model' => Call::class,
|
||||
'title' => $tagName ?? '',
|
||||
'excerpt' => $translation?->content ?? '',
|
||||
'photo' => $call->callable?->profile_photo_url ?? '',
|
||||
'location' => $locationStr,
|
||||
'tag_color' => $tagColor,
|
||||
'tag_categories' => $tagCategories,
|
||||
'callable_name' => $call->callable?->name ?? '',
|
||||
'callable_location' => ProfileCalls::buildCallableLocation($call->callable),
|
||||
'till' => $call->till,
|
||||
'expiry_badge_text' => ProfileCalls::buildExpiryBadgeText($call->till),
|
||||
'like_count' => $call->loveReactant?->reactionCounters
|
||||
->firstWhere('reaction_type_id', 3)?->count ?? 0,
|
||||
'score' => $score,
|
||||
];
|
||||
})
|
||||
->sortByDesc('score')
|
||||
->take($this->maxCards)
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.call-card-carousel');
|
||||
}
|
||||
}
|
||||
180
app/Http/Livewire/MainPage/CallCardFull.php
Normal file
180
app/Http/Livewire/MainPage/CallCardFull.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Http\Livewire\Calls\ProfileCalls;
|
||||
use App\Models\Call;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Locations\Location;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Livewire\Component;
|
||||
|
||||
class CallCardFull extends Component
|
||||
{
|
||||
public $call = null;
|
||||
public int $postNr;
|
||||
public bool $related;
|
||||
public bool $random;
|
||||
|
||||
public function mount(int $postNr, bool $related, bool $random = false, Request $request): void
|
||||
{
|
||||
$this->postNr = $postNr;
|
||||
$this->related = $related;
|
||||
$this->random = $random;
|
||||
|
||||
$profile = getActiveProfile();
|
||||
|
||||
if ($profile) {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
}
|
||||
|
||||
$location = $profile && $profile->locations ? $profile->locations()->first() : null;
|
||||
|
||||
// Resolve location filter arrays (same pattern as EventCardFull)
|
||||
$locationCityIds = [];
|
||||
$locationDivisionIds = [];
|
||||
$locationCountryIds = [];
|
||||
|
||||
if ($location) {
|
||||
if (!$location->division && !$location->city) {
|
||||
$country = Location::find($location->id)->country;
|
||||
if ($related) {
|
||||
$locationCountryIds = Country::pluck('id')->toArray();
|
||||
} else {
|
||||
$locationCountryIds = $country ? [$country->id] : [];
|
||||
}
|
||||
} elseif ($location->division && !$location->city) {
|
||||
$divisionId = Location::find($location->id)->division->id;
|
||||
if ($related) {
|
||||
$locationDivisionIds = Division::find($divisionId)->parent->divisions->pluck('id')->toArray();
|
||||
} else {
|
||||
$locationDivisionIds = [$divisionId];
|
||||
}
|
||||
} elseif ($location->city) {
|
||||
$cityId = Location::find($location->id)->city->id;
|
||||
if ($related) {
|
||||
$locationCityIds = City::find($cityId)->parent->cities->pluck('id')->toArray();
|
||||
} else {
|
||||
$locationCityIds = [$cityId];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$locale = App::getLocale();
|
||||
|
||||
$query = Call::with([
|
||||
'tag.contexts.category.translations',
|
||||
'tag.contexts.category.ancestors.translations',
|
||||
'translations',
|
||||
'location.city.translations',
|
||||
'location.country.translations',
|
||||
'callable.locations.city.translations',
|
||||
'callable.locations.division.translations',
|
||||
'callable.locations.country.translations',
|
||||
'loveReactant.reactionCounters',
|
||||
])
|
||||
->where('is_public', true)
|
||||
->where('is_paused', false)
|
||||
->where('is_suppressed', false)
|
||||
->whereNull('deleted_at')
|
||||
->where(fn ($q) => $q->whereNull('till')->orWhere('till', '>=', now()));
|
||||
|
||||
// Apply location filter when a profile location is known
|
||||
if ($locationCityIds || $locationDivisionIds || $locationCountryIds) {
|
||||
$query->whereHas('location', function ($q) use ($locationCityIds, $locationDivisionIds, $locationCountryIds) {
|
||||
$q->where(function ($q) use ($locationCityIds, $locationDivisionIds, $locationCountryIds) {
|
||||
if ($locationCityIds) {
|
||||
$q->orWhereIn('city_id', $locationCityIds);
|
||||
}
|
||||
if ($locationDivisionIds) {
|
||||
$q->orWhereIn('division_id', $locationDivisionIds);
|
||||
}
|
||||
if ($locationCountryIds) {
|
||||
$q->orWhereIn('country_id', $locationCountryIds);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ($random) {
|
||||
$query->inRandomOrder();
|
||||
$call = $query->first();
|
||||
} else {
|
||||
$query->orderBy('till');
|
||||
$calls = $query->get();
|
||||
$call = $calls->count() > $postNr ? $calls[$postNr] : null;
|
||||
}
|
||||
|
||||
if (!$call) {
|
||||
return;
|
||||
}
|
||||
|
||||
$translation = $call->translations->firstWhere('locale', $locale)
|
||||
?? $call->translations->first();
|
||||
|
||||
$tag = $call->tag;
|
||||
$tagContext = $tag?->contexts->first();
|
||||
$tagCategory = $tagContext?->category;
|
||||
$tagColor = $tagCategory?->relatedColor ?? 'gray';
|
||||
$tagName = $tag?->translation?->name ?? $tag?->name;
|
||||
|
||||
$locationStr = null;
|
||||
if ($call->location) {
|
||||
$loc = $call->location;
|
||||
$parts = [];
|
||||
if ($loc->city) {
|
||||
$cityName = optional($loc->city->translations->first())->name;
|
||||
if ($cityName) $parts[] = $cityName;
|
||||
}
|
||||
if ($loc->country) {
|
||||
if ($loc->country->code === 'XX') {
|
||||
$parts[] = __('Location not specified');
|
||||
} elseif ($loc->country->code) {
|
||||
$parts[] = strtoupper($loc->country->code);
|
||||
}
|
||||
}
|
||||
$locationStr = $parts ? implode(', ', $parts) : null;
|
||||
}
|
||||
|
||||
$tagCategories = [];
|
||||
if ($tagCategory) {
|
||||
$ancestors = $tagCategory->ancestorsAndSelf()->get()->reverse();
|
||||
foreach ($ancestors as $cat) {
|
||||
$catName = $cat->translations->firstWhere('locale', $locale)?->name
|
||||
?? $cat->translations->first()?->name
|
||||
?? '';
|
||||
if ($catName) {
|
||||
$tagCategories[] = [
|
||||
'name' => $catName,
|
||||
'color' => $cat->relatedColor ?? 'gray',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->call = [
|
||||
'id' => $call->id,
|
||||
'model' => Call::class,
|
||||
'title' => $tagName ?? '',
|
||||
'excerpt' => $translation?->content ?? '',
|
||||
'photo' => $call->callable?->profile_photo_url ?? '',
|
||||
'location' => $locationStr,
|
||||
'tag_color' => $tagColor,
|
||||
'tag_categories' => $tagCategories,
|
||||
'callable_name' => $call->callable?->name ?? '',
|
||||
'callable_location' => ProfileCalls::buildCallableLocation($call->callable),
|
||||
'till' => $call->till,
|
||||
'expiry_badge_text' => ProfileCalls::buildExpiryBadgeText($call->till),
|
||||
'like_count' => $call->loveReactant?->reactionCounters->firstWhere('reaction_type_id', 3)?->count ?? 0,
|
||||
'score' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.call-card-full');
|
||||
}
|
||||
}
|
||||
222
app/Http/Livewire/MainPage/CallCardHalf.php
Normal file
222
app/Http/Livewire/MainPage/CallCardHalf.php
Normal file
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Http\Livewire\Calls\CallCarouselScorer;
|
||||
use App\Http\Livewire\Calls\ProfileCalls;
|
||||
use App\Models\Call;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Locations\Location;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class CallCardHalf extends Component
|
||||
{
|
||||
public array $calls = [];
|
||||
public bool $related = true;
|
||||
public bool $random = false;
|
||||
public int $rows = 1;
|
||||
public bool $showScore = false;
|
||||
|
||||
private const UNKNOWN_COUNTRY_ID = 10;
|
||||
|
||||
public function mount(bool $related, bool $random = false, int $rows = 1): void
|
||||
{
|
||||
$this->related = $related;
|
||||
$this->random = $random;
|
||||
$this->rows = max(1, $rows);
|
||||
|
||||
$carouselCfg = timebank_config('calls.carousel', []);
|
||||
$locale = App::getLocale();
|
||||
|
||||
// --- Resolve active profile and its location ---
|
||||
$profile = getActiveProfile();
|
||||
if ($profile) {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
}
|
||||
|
||||
$location = $profile?->locations ? $profile->locations()->first() : null;
|
||||
$profileCityId = $location ? ($location->city_id ?? $location->city?->id) : null;
|
||||
$profileDivisionId = $location ? ($location->division_id ?? $location->division?->id) : null;
|
||||
$profileCountryId = $location ? ($location->country_id ?? $location->country?->id) : null;
|
||||
|
||||
$locationCityIds = [];
|
||||
$locationDivisionIds = [];
|
||||
$locationCountryIds = [];
|
||||
|
||||
if ($location) {
|
||||
if (!$location->division && !$location->city) {
|
||||
$country = Location::find($location->id)->country;
|
||||
$locationCountryIds = $related
|
||||
? Country::pluck('id')->toArray()
|
||||
: ($country ? [$country->id] : []);
|
||||
} elseif ($location->division && !$location->city) {
|
||||
$divisionId = Location::find($location->id)->division->id;
|
||||
$locationDivisionIds = $related
|
||||
? Division::find($divisionId)->parent->divisions->pluck('id')->toArray()
|
||||
: [$divisionId];
|
||||
} elseif ($location->city) {
|
||||
$cityId = Location::find($location->id)->city->id;
|
||||
$locationCityIds = $related
|
||||
? City::find($cityId)->parent->cities->pluck('id')->toArray()
|
||||
: [$cityId];
|
||||
}
|
||||
}
|
||||
|
||||
// --- Base query ---
|
||||
$query = Call::with([
|
||||
'tag.contexts.category.translations',
|
||||
'tag.contexts.category.ancestors.translations',
|
||||
'translations',
|
||||
'location.city.translations',
|
||||
'location.country.translations',
|
||||
'callable.locations.city.translations',
|
||||
'callable.locations.division.translations',
|
||||
'callable.locations.country.translations',
|
||||
'callable.loveReactant.reactionCounters',
|
||||
'loveReactant.reactionCounters',
|
||||
])
|
||||
->whereNull('deleted_at')
|
||||
->where('is_paused', false)
|
||||
->where('is_suppressed', false)
|
||||
->where(fn ($q) => $q->whereNull('till')->orWhere('till', '>=', now()));
|
||||
|
||||
if (!Auth::check() || ($carouselCfg['exclude_non_public'] ?? true)) {
|
||||
$query->where('is_public', true);
|
||||
}
|
||||
|
||||
if ($profile && ($carouselCfg['exclude_own_calls'] ?? true)) {
|
||||
$query->where(function ($q) use ($profile) {
|
||||
$q->where('callable_type', '!=', get_class($profile))
|
||||
->orWhere('callable_id', '!=', $profile->id);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Locality filter ---
|
||||
$includeUnknown = $carouselCfg['include_unknown_location'] ?? true;
|
||||
$includeDivision = $carouselCfg['include_same_division'] ?? true;
|
||||
$includeCountry = $carouselCfg['include_same_country'] ?? true;
|
||||
|
||||
$hasLocalityFilter = $locationCityIds || $locationDivisionIds || $locationCountryIds
|
||||
|| ($includeDivision && $locationDivisionIds)
|
||||
|| ($includeCountry && $locationCountryIds)
|
||||
|| $includeUnknown;
|
||||
|
||||
if ($hasLocalityFilter) {
|
||||
$query->where(function ($q) use (
|
||||
$locationCityIds, $locationDivisionIds, $locationCountryIds,
|
||||
$includeDivision, $includeCountry, $includeUnknown
|
||||
) {
|
||||
if ($locationCityIds) {
|
||||
$q->orWhereHas('location', fn ($lq) => $lq->whereIn('city_id', $locationCityIds));
|
||||
}
|
||||
if ($includeDivision && $locationDivisionIds) {
|
||||
$q->orWhereHas('location', fn ($lq) => $lq->whereIn('division_id', $locationDivisionIds));
|
||||
}
|
||||
if ($includeCountry && $locationCountryIds) {
|
||||
$q->orWhereHas('location', fn ($lq) => $lq->whereIn('country_id', $locationCountryIds));
|
||||
}
|
||||
if ($includeUnknown) {
|
||||
$q->orWhereHas('location', fn ($lq) => $lq->where('country_id', self::UNKNOWN_COUNTRY_ID));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$isAdmin = getActiveProfileType() === 'Admin';
|
||||
$this->showScore = (bool) ($carouselCfg['show_score'] ?? false)
|
||||
|| ($isAdmin && (bool) ($carouselCfg['show_score_for_admins'] ?? true));
|
||||
|
||||
$limit = $this->rows * 2;
|
||||
$poolSize = $limit * max(1, (int) ($carouselCfg['pool_multiplier'] ?? 5));
|
||||
|
||||
$scorer = new CallCarouselScorer(
|
||||
$carouselCfg,
|
||||
$profileCityId,
|
||||
$profileDivisionId,
|
||||
$profileCountryId
|
||||
);
|
||||
|
||||
$calls = $query->limit($poolSize)->get();
|
||||
|
||||
$this->calls = $calls->map(function (Call $call) use ($locale, $scorer) {
|
||||
$translation = $call->translations->firstWhere('locale', $locale)
|
||||
?? $call->translations->first();
|
||||
|
||||
$tag = $call->tag;
|
||||
$tagContext = $tag?->contexts->first();
|
||||
$tagCategory = $tagContext?->category;
|
||||
$tagColor = $tagCategory?->relatedColor ?? 'gray';
|
||||
$tagName = $tag?->translation?->name ?? $tag?->name;
|
||||
|
||||
$locationStr = null;
|
||||
if ($call->location) {
|
||||
$loc = $call->location;
|
||||
$parts = [];
|
||||
if ($loc->city) {
|
||||
$cityName = optional($loc->city->translations->first())->name;
|
||||
if ($cityName) $parts[] = $cityName;
|
||||
}
|
||||
if ($loc->country) {
|
||||
if ($loc->country->code === 'XX') {
|
||||
$parts[] = __('Location not specified');
|
||||
} elseif ($loc->country->code) {
|
||||
$parts[] = strtoupper($loc->country->code);
|
||||
}
|
||||
}
|
||||
$locationStr = $parts ? implode(', ', $parts) : null;
|
||||
}
|
||||
|
||||
$tagCategories = [];
|
||||
if ($tagCategory) {
|
||||
$ancestors = $tagCategory->ancestorsAndSelf()->get()->reverse();
|
||||
foreach ($ancestors as $cat) {
|
||||
$catName = $cat->translations->firstWhere('locale', $locale)?->name
|
||||
?? $cat->translations->first()?->name
|
||||
?? '';
|
||||
if ($catName) {
|
||||
$tagCategories[] = [
|
||||
'name' => $catName,
|
||||
'color' => $cat->relatedColor ?? 'gray',
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$score = $scorer->score($call);
|
||||
|
||||
if ($this->random) {
|
||||
$score *= (random_int(85, 115) / 100);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $call->id,
|
||||
'model' => Call::class,
|
||||
'title' => $tagName ?? '',
|
||||
'excerpt' => $translation?->content ?? '',
|
||||
'photo' => $call->callable?->profile_photo_url ?? '',
|
||||
'location' => $locationStr,
|
||||
'tag_color' => $tagColor,
|
||||
'tag_categories' => $tagCategories,
|
||||
'callable_name' => $call->callable?->name ?? '',
|
||||
'callable_location' => ProfileCalls::buildCallableLocation($call->callable),
|
||||
'till' => $call->till,
|
||||
'expiry_badge_text' => ProfileCalls::buildExpiryBadgeText($call->till),
|
||||
'like_count' => $call->loveReactant?->reactionCounters
|
||||
->firstWhere('reaction_type_id', 3)?->count ?? 0,
|
||||
'score' => $score,
|
||||
];
|
||||
})
|
||||
->sortByDesc('score')
|
||||
->take($limit)
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.call-card-half');
|
||||
}
|
||||
}
|
||||
164
app/Http/Livewire/MainPage/EventCardFull.php
Normal file
164
app/Http/Livewire/MainPage/EventCardFull.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\Meeting;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Livewire\Component;
|
||||
|
||||
class EventCardFull extends Component
|
||||
{
|
||||
public $author;
|
||||
public $post = [];
|
||||
public $posts;
|
||||
public $media;
|
||||
public $postNr;
|
||||
public $related;
|
||||
|
||||
|
||||
public function mount($postNr, $related, Request $request)
|
||||
{
|
||||
$this->postNr = $postNr;
|
||||
$this->related = $related;
|
||||
|
||||
$profile = getActiveProfile();
|
||||
|
||||
// SECURITY: If a profile is active, validate the user has access to it
|
||||
// This prevents session manipulation while allowing public access (null profile)
|
||||
if ($profile) {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
}
|
||||
|
||||
$location = $profile && $profile->locations ? $profile->locations()->first() : null;
|
||||
|
||||
if ($location) {
|
||||
// If no division and no city as location set
|
||||
if (!$location->division && !$location->city) {
|
||||
$country = Location::find($location->id)->country;
|
||||
$categoryable_id = $country ? $country->id : null;
|
||||
$categoryable_type = Country::class;
|
||||
// Include also all other countries if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Country::pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// Division without city is set as location
|
||||
} elseif ($location && $location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->division->id;
|
||||
$categoryable_type = Division::class;
|
||||
// Include also all other divisions in the same country if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Division::find($categoryable_id)->parent->divisions->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// City is set as location
|
||||
} elseif ($location && $location->city) {
|
||||
$categoryable_id = Location::find($location->id)->city->id;
|
||||
$categoryable_type = City::class;
|
||||
// Include also all other cities in the same division if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = City::find($categoryable_id)->parent->cities->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
}
|
||||
// No matching location is set
|
||||
} else {
|
||||
$categoryable_id = [];
|
||||
$categoryable_type = '';
|
||||
}
|
||||
|
||||
$post =
|
||||
Post::with([
|
||||
'postable' => function ($query) {
|
||||
$query->select(['id', 'name']);
|
||||
},
|
||||
'category' => function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->where('type', Meeting::class)
|
||||
->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
});
|
||||
},
|
||||
'translations' => function ($query) {
|
||||
$query
|
||||
->where('locale', App::getLocale())
|
||||
;
|
||||
},
|
||||
'meeting',
|
||||
'media',
|
||||
])
|
||||
->whereHas('category', function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->where('type', Meeting::class)
|
||||
->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
});
|
||||
})
|
||||
->whereHas('translations', function ($query) {
|
||||
$query
|
||||
->where('locale', App::getLocale())
|
||||
->whereDate('from', '<=', now())
|
||||
->where(function ($query) {
|
||||
$query->whereDate('till', '>', now())->orWhereNull('till');
|
||||
})
|
||||
->orderBy('updated_at', 'desc');
|
||||
})
|
||||
->get()->sortBy(function ($query) {
|
||||
if (isset($query->meeting->from)) {
|
||||
return $query->meeting->from;
|
||||
};
|
||||
})->values(); // Use values() method to reset the collection keys after sortBy
|
||||
|
||||
$lastNr = $post->count() - 1;
|
||||
|
||||
if ($postNr > $lastNr) {
|
||||
$post = null;
|
||||
} else {
|
||||
$post = $post[$postNr];
|
||||
}
|
||||
|
||||
if (isset($post->translations)) {
|
||||
$this->post = $post->translations->first();
|
||||
$this->post['start'] = Carbon::parse($post->translations->first()->updated_at)->isoFormat('LL');
|
||||
$this->post['category'] = Category::find($post->category_id)->translations->where('locale', App::getLocale())->first()->name;
|
||||
$this->post['author'] = $post->postable->name;
|
||||
$this->post['id'] = $post->id;
|
||||
$this->post['model'] = get_class($post);
|
||||
$this->post['like_count'] = $post->like_count ?? 0;
|
||||
if ($post->meeting) {
|
||||
$this->post['address'] = ($post->meeting->address) ? $post->meeting->address : '';
|
||||
$this->post['from'] = ($post->meeting->from) ? $post->meeting->from : '';
|
||||
$this->post['venue'] = ($post->meeting->venue) ? $post->meeting->venue : '';
|
||||
$this->post['city'] = $post->meeting->location?->first()?->city?->translations?->first()?->name ?? '';
|
||||
$this->post['price'] = ($post->meeting->price) ? tbFormat($post->meeting->price) : '';
|
||||
}
|
||||
|
||||
if ($post->media) {
|
||||
$this->media = Post::find($post->id)->getFirstMedia('posts');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.event-card-full');
|
||||
}
|
||||
}
|
||||
199
app/Http/Livewire/MainPage/ImageCardFull.php
Normal file
199
app/Http/Livewire/MainPage/ImageCardFull.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\News;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Livewire\Component;
|
||||
|
||||
class ImageCardFull extends Component
|
||||
{
|
||||
public $author;
|
||||
public $post = [];
|
||||
public $posts;
|
||||
public $media;
|
||||
public $postNr;
|
||||
public $related;
|
||||
public bool $random = false;
|
||||
|
||||
|
||||
public function mount(Request $request, $postNr, $related, $random = null)
|
||||
{
|
||||
$this->postNr = $postNr;
|
||||
$this->related = $related;
|
||||
|
||||
if ($random) {
|
||||
$this->random = true;
|
||||
}
|
||||
|
||||
|
||||
$profile = getActiveProfile();
|
||||
|
||||
// SECURITY: If a profile is active, validate the user has access to it
|
||||
// This prevents session manipulation while allowing public access (null profile)
|
||||
if ($profile) {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
}
|
||||
|
||||
$location = $profile && $profile->locations ? $profile->locations()->first() : null;
|
||||
|
||||
$skipLocationFilter = false;
|
||||
|
||||
if ($location) {
|
||||
// If no division and no city as location set
|
||||
if (!$location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->country->id;
|
||||
$categoryable_type = Country::class;
|
||||
// Include also all other countries if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Country::pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// Division without city is set as location
|
||||
} elseif ($location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->division->id;
|
||||
$categoryable_type = Division::class;
|
||||
// Include also all other divisions in the same country if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Division::find($categoryable_id)->parent->divisions->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// City is set as location
|
||||
} elseif ($location->city) {
|
||||
$categoryable_id = Location::find($location->id)->city->id;
|
||||
$categoryable_type = City::class;
|
||||
// Include also all other cities in the same division if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = City::find($categoryable_id)->parent->cities->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
}
|
||||
// No matching location is set - skip location filtering
|
||||
} else {
|
||||
$skipLocationFilter = true;
|
||||
$categoryable_id = [];
|
||||
$categoryable_type = '';
|
||||
}
|
||||
|
||||
|
||||
// TODO: check what happens when multiple locations per user are used!
|
||||
|
||||
$locale = App::getLocale();
|
||||
$categoryTypes = ['App\Models\ImagePost', 'App\Models\ImagePost\\' . $locale];
|
||||
|
||||
$post =
|
||||
Post::with([
|
||||
'postable' => function ($query) {
|
||||
$query->select(['id', 'name']);
|
||||
},
|
||||
'category' => function ($query) use ($categoryable_id, $categoryable_type, $skipLocationFilter, $categoryTypes) {
|
||||
$query->whereIn('type', $categoryTypes);
|
||||
if (!$skipLocationFilter && !empty($categoryable_id)) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
})->orWhere(function ($query) {
|
||||
// Also include categories with no location set
|
||||
$query->whereNull('categoryable_id')
|
||||
->whereNull('categoryable_type');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
'translations' => function ($query) {
|
||||
$query->where('locale', App::getLocale());
|
||||
},
|
||||
'author',
|
||||
'media',
|
||||
])
|
||||
->whereHas('category', function ($query) use ($categoryable_id, $categoryable_type, $skipLocationFilter, $categoryTypes) {
|
||||
$query->whereIn('type', $categoryTypes);
|
||||
if (!$skipLocationFilter && !empty($categoryable_id)) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
})->orWhere(function ($query) {
|
||||
// Also include categories with no location set
|
||||
$query->whereNull('categoryable_id')
|
||||
->whereNull('categoryable_type');
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
->whereHas('translations', function ($query) {
|
||||
$query
|
||||
->where('locale', App::getLocale())
|
||||
->whereDate('from', '<=', now())
|
||||
->where(function ($query) {
|
||||
$query->whereDate('till', '>', now())->orWhereNull('till');
|
||||
})
|
||||
->orderBy('updated_at', 'desc');
|
||||
});
|
||||
|
||||
// Apply random or sorted ordering
|
||||
if ($this->random) {
|
||||
$post = $post->inRandomOrder()->get();
|
||||
} else {
|
||||
$post = $post->get()->sortByDesc(function ($query) {
|
||||
if (isset($query->translations)) {
|
||||
return $query->translations->first()->updated_at;
|
||||
};
|
||||
})->values(); // Use values() method to reset the collection keys after sortBy
|
||||
}
|
||||
|
||||
$lastNr = $post->count() - 1;
|
||||
|
||||
if ($postNr > $lastNr || $post->isEmpty()) {
|
||||
$post = null;
|
||||
$this->post = null;
|
||||
} else {
|
||||
$post = $post[$postNr];
|
||||
}
|
||||
|
||||
if ($post && isset($post->translations) && $post->translations->isNotEmpty()) {
|
||||
$translation = $post->translations->first();
|
||||
$this->post = $translation;
|
||||
$this->post['slug'] = $translation->slug;
|
||||
$this->post['from'] = $translation->from;
|
||||
|
||||
$category = Category::find($post->category_id);
|
||||
$categoryTranslation = $category ? $category->translations->where('locale', App::getLocale())->first() : null;
|
||||
$this->post['category'] = $categoryTranslation ? $categoryTranslation->name : '';
|
||||
|
||||
$this->post['author'] = $post->author ? $post->author->name : timebank_config('posts.site-content-writer');
|
||||
$this->post['author_id'] = $post->author ? $post->author->id : null;
|
||||
$this->post['author_profile_photo_path'] = $post->author && $post->author->profile_photo_path ? $post->author->profile_photo_path : null;
|
||||
$this->post['post_id'] = $post->id;
|
||||
|
||||
if ($post->media && $post->media->isNotEmpty()) {
|
||||
$this->media = Post::find($post->id)->getFirstMedia('posts');
|
||||
|
||||
// Get media owner and caption
|
||||
$this->post['media_owner'] = $this->media->getCustomProperty('owner', '');
|
||||
$captionKey = 'caption-' . App::getLocale();
|
||||
$this->post['media_caption'] = $this->media->getCustomProperty($captionKey, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.image-card-full');
|
||||
}
|
||||
}
|
||||
199
app/Http/Livewire/MainPage/ImageLocalizedCardFull.php
Normal file
199
app/Http/Livewire/MainPage/ImageLocalizedCardFull.php
Normal file
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\News;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Livewire\Component;
|
||||
|
||||
class ImageLocalizedCardFull extends Component
|
||||
{
|
||||
public $author;
|
||||
public $post = [];
|
||||
public $posts;
|
||||
public $media;
|
||||
public $postNr;
|
||||
public $related;
|
||||
public bool $random = false;
|
||||
|
||||
|
||||
public function mount(Request $request, $postNr, $related, $random = null)
|
||||
{
|
||||
$this->postNr = $postNr;
|
||||
$this->related = $related;
|
||||
|
||||
if ($random) {
|
||||
$this->random = true;
|
||||
}
|
||||
|
||||
|
||||
$profile = getActiveProfile();
|
||||
|
||||
// SECURITY: If a profile is active, validate the user has access to it
|
||||
// This prevents session manipulation while allowing public access (null profile)
|
||||
if ($profile) {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
}
|
||||
|
||||
$location = $profile && $profile->locations ? $profile->locations()->first() : null;
|
||||
|
||||
$skipLocationFilter = false;
|
||||
|
||||
if ($location) {
|
||||
// If no division and no city as location set
|
||||
if (!$location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->country->id;
|
||||
$categoryable_type = Country::class;
|
||||
// Include also all other countries if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Country::pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// Division without city is set as location
|
||||
} elseif ($location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->division->id;
|
||||
$categoryable_type = Division::class;
|
||||
// Include also all other divisions in the same country if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Division::find($categoryable_id)->parent->divisions->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// City is set as location
|
||||
} elseif ($location->city) {
|
||||
$categoryable_id = Location::find($location->id)->city->id;
|
||||
$categoryable_type = City::class;
|
||||
// Include also all other cities in the same division if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = City::find($categoryable_id)->parent->cities->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
}
|
||||
// No matching location is set - skip location filtering
|
||||
} else {
|
||||
$skipLocationFilter = true;
|
||||
$categoryable_id = [];
|
||||
$categoryable_type = '';
|
||||
}
|
||||
|
||||
|
||||
// TODO: check what happens when multiple locations per user are used!
|
||||
|
||||
$locale = App::getLocale();
|
||||
$categoryType = 'App\\Models\\ImagePost\\' . $locale;
|
||||
|
||||
$post =
|
||||
Post::with([
|
||||
'postable' => function ($query) {
|
||||
$query->select(['id', 'name']);
|
||||
},
|
||||
'category' => function ($query) use ($categoryable_id, $categoryable_type, $skipLocationFilter, $categoryType) {
|
||||
$query->where('type', $categoryType);
|
||||
if (!$skipLocationFilter && !empty($categoryable_id)) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
})->orWhere(function ($query) {
|
||||
// Also include categories with no location set
|
||||
$query->whereNull('categoryable_id')
|
||||
->whereNull('categoryable_type');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
'translations' => function ($query) {
|
||||
$query->where('locale', App::getLocale());
|
||||
},
|
||||
'author',
|
||||
'media',
|
||||
])
|
||||
->whereHas('category', function ($query) use ($categoryable_id, $categoryable_type, $skipLocationFilter, $categoryType) {
|
||||
$query->where('type', $categoryType);
|
||||
if (!$skipLocationFilter && !empty($categoryable_id)) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
})->orWhere(function ($query) {
|
||||
// Also include categories with no location set
|
||||
$query->whereNull('categoryable_id')
|
||||
->whereNull('categoryable_type');
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
->whereHas('translations', function ($query) {
|
||||
$query
|
||||
->where('locale', App::getLocale())
|
||||
->whereDate('from', '<=', now())
|
||||
->where(function ($query) {
|
||||
$query->whereDate('till', '>', now())->orWhereNull('till');
|
||||
})
|
||||
->orderBy('updated_at', 'desc');
|
||||
});
|
||||
|
||||
// Apply random or sorted ordering
|
||||
if ($this->random) {
|
||||
$post = $post->inRandomOrder()->get();
|
||||
} else {
|
||||
$post = $post->get()->sortByDesc(function ($query) {
|
||||
if (isset($query->translations)) {
|
||||
return $query->translations->first()->updated_at;
|
||||
};
|
||||
})->values(); // Use values() method to reset the collection keys after sortBy
|
||||
}
|
||||
|
||||
$lastNr = $post->count() - 1;
|
||||
|
||||
if ($postNr > $lastNr || $post->isEmpty()) {
|
||||
$post = null;
|
||||
$this->post = null;
|
||||
} else {
|
||||
$post = $post[$postNr];
|
||||
}
|
||||
|
||||
if ($post && isset($post->translations) && $post->translations->isNotEmpty()) {
|
||||
$translation = $post->translations->first();
|
||||
$this->post = $translation;
|
||||
$this->post['slug'] = $translation->slug;
|
||||
$this->post['from'] = $translation->from;
|
||||
|
||||
$category = Category::find($post->category_id);
|
||||
$categoryTranslation = $category ? $category->translations->where('locale', App::getLocale())->first() : null;
|
||||
$this->post['category'] = $categoryTranslation ? $categoryTranslation->name : '';
|
||||
|
||||
$this->post['author'] = $post->author ? $post->author->name : timebank_config('posts.site-content-writer');
|
||||
$this->post['author_id'] = $post->author ? $post->author->id : null;
|
||||
$this->post['author_profile_photo_path'] = $post->author && $post->author->profile_photo_path ? $post->author->profile_photo_path : null;
|
||||
$this->post['post_id'] = $post->id;
|
||||
|
||||
if ($post->media && $post->media->isNotEmpty()) {
|
||||
$this->media = Post::find($post->id)->getFirstMedia('posts');
|
||||
|
||||
// Get media owner and caption
|
||||
$this->post['media_owner'] = $this->media->getCustomProperty('owner', '');
|
||||
$captionKey = 'caption-' . App::getLocale();
|
||||
$this->post['media_caption'] = $this->media->getCustomProperty($captionKey, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.image-localized-card-full');
|
||||
}
|
||||
}
|
||||
183
app/Http/Livewire/MainPage/NewsCardFull.php
Normal file
183
app/Http/Livewire/MainPage/NewsCardFull.php
Normal file
@@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Locations\City;
|
||||
use App\Models\Locations\Country;
|
||||
use App\Models\Locations\Division;
|
||||
use App\Models\Locations\Location;
|
||||
use App\Models\News;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Livewire\Component;
|
||||
|
||||
class NewsCardFull extends Component
|
||||
{
|
||||
public $author;
|
||||
public $post = [];
|
||||
public $posts;
|
||||
public $media;
|
||||
public $postNr;
|
||||
public $related;
|
||||
|
||||
|
||||
public function mount($postNr, $related, Request $request)
|
||||
{
|
||||
$this->postNr = $postNr;
|
||||
$this->related = $related;
|
||||
|
||||
|
||||
$profile = getActiveProfile();
|
||||
|
||||
// SECURITY: If a profile is active, validate the user has access to it
|
||||
// This prevents session manipulation while allowing public access (null profile)
|
||||
if ($profile) {
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
}
|
||||
|
||||
$location = $profile && $profile->locations ? $profile->locations()->first() : null;
|
||||
|
||||
$skipLocationFilter = false;
|
||||
|
||||
if ($location) {
|
||||
// If no division and no city as location set
|
||||
if (!$location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->country->id;
|
||||
$categoryable_type = Country::class;
|
||||
// Include also all other countries if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Country::pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// Division without city is set as location
|
||||
} elseif ($location->division && !$location->city) {
|
||||
$categoryable_id = Location::find($location->id)->division->id;
|
||||
$categoryable_type = Division::class;
|
||||
// Include also all other divisions in the same country if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = Division::find($categoryable_id)->parent->divisions->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
// City is set as location
|
||||
} elseif ($location->city) {
|
||||
$categoryable_id = Location::find($location->id)->city->id;
|
||||
$categoryable_type = City::class;
|
||||
// Include also all other cities in the same division if $related is set in view
|
||||
if ($related) {
|
||||
$categoryable_id = City::find($categoryable_id)->parent->cities->pluck('id');
|
||||
} else {
|
||||
$categoryable_id = [$categoryable_id];
|
||||
}
|
||||
}
|
||||
// No matching location is set - skip location filtering
|
||||
} else {
|
||||
$skipLocationFilter = true;
|
||||
$categoryable_id = [];
|
||||
$categoryable_type = '';
|
||||
}
|
||||
|
||||
|
||||
// TODO: check what happens when multiple locations per user are used!
|
||||
$post =
|
||||
Post::with([
|
||||
'postable' => function ($query) {
|
||||
$query->select(['id', 'name']);
|
||||
},
|
||||
'category' => function ($query) use ($categoryable_id, $categoryable_type, $skipLocationFilter) {
|
||||
$query->where('type', News::class)->with('categoryable.translations');
|
||||
if (!$skipLocationFilter && !empty($categoryable_id)) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
})->orWhere(function ($query) {
|
||||
// Also include categories with no location set
|
||||
$query->whereNull('categoryable_id')
|
||||
->whereNull('categoryable_type');
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
'translations' => function ($query) {
|
||||
$query->where('locale', App::getLocale());
|
||||
},
|
||||
'author',
|
||||
'media',
|
||||
])
|
||||
->whereHas('category', function ($query) use ($categoryable_id, $categoryable_type, $skipLocationFilter) {
|
||||
$query->where('type', News::class);
|
||||
if (!$skipLocationFilter && !empty($categoryable_id)) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query->where(function ($query) use ($categoryable_id, $categoryable_type) {
|
||||
$query
|
||||
->whereIn('categoryable_id', $categoryable_id)
|
||||
->where('categoryable_type', $categoryable_type);
|
||||
})->orWhere(function ($query) {
|
||||
// Also include categories with no location set
|
||||
$query->whereNull('categoryable_id')
|
||||
->whereNull('categoryable_type');
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
->whereHas('translations', function ($query) {
|
||||
$query
|
||||
->where('locale', App::getLocale())
|
||||
->whereDate('from', '<=', now())
|
||||
->where(function ($query) {
|
||||
$query->whereDate('till', '>', now())->orWhereNull('till');
|
||||
})
|
||||
->orderBy('updated_at', 'desc');
|
||||
})
|
||||
->get()->sortByDesc(function ($query) {
|
||||
if (isset($query->translations)) {
|
||||
return $query->translations->first()->updated_at;
|
||||
};
|
||||
})->values(); // Use values() method to reset the collection keys after sortBy
|
||||
|
||||
|
||||
$lastNr = $post->count() - 1;
|
||||
if ($postNr > $lastNr) {
|
||||
$post = null;
|
||||
} else {
|
||||
$post = $post[$postNr];
|
||||
}
|
||||
|
||||
// if ($post != null) { // Show only posts if it has the category type of this model's class
|
||||
if (isset($post->translations)) {
|
||||
$translation = $post->translations->first();
|
||||
$this->post = $translation;
|
||||
$this->post['start'] = Carbon::parse(strtotime($translation->updated_at))->isoFormat('LL');
|
||||
$this->post['category'] = Category::find($post->category_id)->translations->where('locale', App::getLocale())->first()->name;
|
||||
$this->post['author'] = $post->author ? $post->author->name : timebank_config('posts.site-content-writer');
|
||||
|
||||
// Prepend location name to excerpt if available
|
||||
$excerpt = $translation->excerpt;
|
||||
if ($post->category && $post->category->categoryable && $post->category->categoryable->translations) {
|
||||
$locationTranslation = $post->category->categoryable->translations->where('locale', App::getLocale())->first();
|
||||
if ($locationTranslation && $locationTranslation->name) {
|
||||
$locationName = strtoupper($locationTranslation->name);
|
||||
$excerpt = $locationName . ' - ' . $excerpt;
|
||||
}
|
||||
}
|
||||
$this->post['excerpt'] = $excerpt;
|
||||
|
||||
if ($post->media) {
|
||||
$this->media = Post::find($post->id)->getFirstMedia('posts');
|
||||
}
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.news-card-full');
|
||||
}
|
||||
}
|
||||
862
app/Http/Livewire/MainPage/SkillsCardFull.php
Normal file
862
app/Http/Livewire/MainPage/SkillsCardFull.php
Normal file
@@ -0,0 +1,862 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Livewire\MainPage;
|
||||
|
||||
use App\Helpers\StringHelper;
|
||||
use App\Http\Livewire\MainPage;
|
||||
use App\Jobs\SendEmailNewTag;
|
||||
use App\Models\Category;
|
||||
use App\Models\Language;
|
||||
use App\Models\Tag;
|
||||
use App\Models\TaggableLocale;
|
||||
use App\Traits\TaggableWithLocale;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Component;
|
||||
use Throwable;
|
||||
use WireUi\Traits\WireUiActions;
|
||||
|
||||
class SkillsCardFull extends Component
|
||||
{
|
||||
use TaggableWithLocale;
|
||||
use WireUiActions;
|
||||
|
||||
public $label;
|
||||
public $tagsArray = [];
|
||||
public bool $tagsArrayChanged = false;
|
||||
public bool $saveDisabled = false;
|
||||
public $initTagIds = [];
|
||||
public $initTagsArray = [];
|
||||
public $initTagsArrayTranslated = [];
|
||||
public $newTagsArray;
|
||||
public $suggestions = [];
|
||||
|
||||
public $modalVisible = false;
|
||||
|
||||
public $newTag = [];
|
||||
public $newTagCategory;
|
||||
public $categoryOptions = [];
|
||||
public $categoryColor = 'gray';
|
||||
|
||||
public bool $translationPossible = true;
|
||||
public bool $translationAllowed = true;
|
||||
public bool $translationVisible = false;
|
||||
public $translationLanguages = [];
|
||||
public $selectTranslationLanguage;
|
||||
public $translationOptions = [];
|
||||
public $selectTagTranslation;
|
||||
public $inputTagTranslation = [];
|
||||
public bool $inputDisabled = true;
|
||||
public $translateRadioButton = null;
|
||||
|
||||
public bool $sessionLanguageOk = false;
|
||||
public bool $sessionLanguageIgnored = false;
|
||||
public bool $transLanguageOk = false;
|
||||
public bool $transLanguageIgnored = false;
|
||||
|
||||
protected $langDetector = null;
|
||||
protected $listeners = [
|
||||
'save',
|
||||
'cancelCreateTag',
|
||||
'refreshComponent' => '$refresh',
|
||||
'tagifyFocus',
|
||||
'tagifyBlur',
|
||||
'saveCard'=> 'save',
|
||||
];
|
||||
|
||||
protected function rules()
|
||||
{
|
||||
return [
|
||||
'newTagsArray' => 'array',
|
||||
'newTag' => 'array',
|
||||
'newTag.name' => Rule::when(
|
||||
function ($input) {
|
||||
// Check if newTag is not an empty array
|
||||
return count($input['newTag']) > 0;
|
||||
},
|
||||
array_merge(
|
||||
timebank_config('tags.name_rule'),
|
||||
timebank_config('tags.exists_in_current_locale_rule'),
|
||||
[
|
||||
'sometimes',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!$this->sessionLanguageOk && !$this->sessionLanguageIgnored) {
|
||||
$locale = app()->getLocale();
|
||||
$localeName = \Locale::getDisplayName($locale, $locale);
|
||||
$fail(
|
||||
__('Is this :locale? Please confirm here below', [
|
||||
'locale' => $localeName
|
||||
])
|
||||
);
|
||||
}
|
||||
},
|
||||
]
|
||||
),
|
||||
),
|
||||
'newTagCategory' => Rule::when(
|
||||
function ($input) {
|
||||
if (count($input['newTag']) > 0 && $this->translationVisible === true && $this->translateRadioButton == 'input') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (count($input['newTag']) > 0 && $this->translationVisible === false) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
['required', 'int'],
|
||||
),
|
||||
'selectTagTranslation' => Rule::when(
|
||||
function ($input) {
|
||||
// Check if existing tag translation is selected
|
||||
return $this->translationVisible === true && $this->translateRadioButton == 'select';
|
||||
},
|
||||
['required', 'int'],
|
||||
),
|
||||
'inputTagTranslation' => 'array',
|
||||
'inputTagTranslation.name' => Rule::when(
|
||||
fn ($input) => $this->translationVisible === true && $this->translateRadioButton === 'input',
|
||||
array_merge(
|
||||
timebank_config('tags.name_rule'),
|
||||
timebank_config('tags.exists_in_current_locale_rule'),
|
||||
[
|
||||
'sometimes',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!$this->transLanguageOk && !$this->transLanguageIgnored) {
|
||||
$baseLocale = $this->selectTranslationLanguage;
|
||||
$locale = \Locale::getDisplayName($baseLocale, $baseLocale);
|
||||
$fail(__('Is this :locale? Please confirm here below', [
|
||||
'locale' => $locale
|
||||
]));
|
||||
}
|
||||
},
|
||||
function ($attribute, $value, $fail) {
|
||||
$existsInTransLationLanguage = DB::table('taggable_tags')
|
||||
->join('taggable_locales', 'taggable_tags.tag_id', '=', 'taggable_locales.taggable_tag_id')
|
||||
->where('taggable_locales.locale', $this->selectTranslationLanguage)
|
||||
->where(function ($query) use ($value) {
|
||||
$query->where('taggable_tags.name', $value)
|
||||
->orWhere('taggable_tags.normalized', $value);
|
||||
})
|
||||
->exists();
|
||||
|
||||
if ($existsInTransLationLanguage) {
|
||||
$fail(__('This tag already exists.'));
|
||||
}
|
||||
},
|
||||
]
|
||||
),
|
||||
[]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public function mount($label = null)
|
||||
{
|
||||
if ($label === null) {
|
||||
$label = __('Activities or skills you offer to other ' . platform_users());
|
||||
}
|
||||
$this->label = $label;
|
||||
|
||||
$owner = getActiveProfile();
|
||||
|
||||
if (!$owner) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($owner);
|
||||
|
||||
$owner->cleanTaggables();
|
||||
|
||||
$this->checkTranslationAllowed();
|
||||
$this->checkTranslationPossible();
|
||||
|
||||
$this->getSuggestions();
|
||||
$this->getInitialTags();
|
||||
$this->getLanguageDetector();
|
||||
$this->dispatch('load');
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected function getSuggestions()
|
||||
{
|
||||
$suggestions = (new Tag())->localTagArray(app()->getLocale());
|
||||
|
||||
$this->suggestions = collect($suggestions)->map(function ($value) {
|
||||
return app()->getLocale() == 'de' ? $value : StringHelper::DutchTitleCase($value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
protected function getInitialTags()
|
||||
{
|
||||
$this->initTagIds = getActiveProfile()->tags()->get()->pluck('tag_id');
|
||||
|
||||
$this->initTagsArray = TaggableLocale::whereIn('taggable_tag_id', $this->initTagIds)
|
||||
->select('taggable_tag_id', 'locale', 'updated_by_user')
|
||||
->get()
|
||||
->toArray();
|
||||
|
||||
$translatedTags = collect((new Tag())->translateTagIdsWithContexts($this->initTagIds));
|
||||
|
||||
$tags = $translatedTags->map(function ($item, $key) {
|
||||
return [
|
||||
'original_tag_id' => $item['original_tag_id'],
|
||||
'tag_id' => $item['tag_id'],
|
||||
'value' => $item['locale'] == App::getLocale() ? $item['tag'] : $item['tag'] . ' (' . strtoupper($item['locale']) . ')',
|
||||
'readonly' => false, // Tags are never readonly so the remove button is always visible
|
||||
'class' => $item['locale'] == App::getLocale() ? '' : 'tag-foreign-locale', // Mark foreign-locale tags with a class for diagonal stripe styling
|
||||
'locale' => $item['locale'],
|
||||
'category' => $item['category'],
|
||||
'category_path' => $item['category_path'],
|
||||
'category_color' => $item['category_color'],
|
||||
'title' => $item['category_path'], // 'title' is used by Tagify script for text that shows on hover
|
||||
'style' =>
|
||||
'--tag-bg:' .
|
||||
tailwindColorToHex($item['category_color'] . '-400') .
|
||||
'; --tag-text-color:#000' . // #111827 is gray-900
|
||||
'; --tag-hover:' .
|
||||
tailwindColorToHex($item['category_color'] . '-200'), // 'style' is used by Tagify script for background color, tailwindColorToHex is a helper function in app/Helpers/StyleHelper.php
|
||||
];
|
||||
});
|
||||
|
||||
$tags = $tags->sortBy('category_color')->values();
|
||||
$this->initTagsArrayTranslated = $tags->toArray();
|
||||
$this->tagsArray = json_encode($tags->toArray());
|
||||
}
|
||||
|
||||
|
||||
public function checkSessionLanguage()
|
||||
{
|
||||
// Ensure the language detector is initialized
|
||||
$this->getLanguageDetector();
|
||||
|
||||
$detectedLanguage = $this->langDetector->detectSimple($this->newTag['name']);
|
||||
if ($detectedLanguage === session('locale')) {
|
||||
$this->sessionLanguageOk = true;
|
||||
// No need to ignore language detection when session locale is detected
|
||||
$this->sessionLanguageIgnored = false;
|
||||
} else {
|
||||
$this->sessionLanguageOk = false;
|
||||
}
|
||||
|
||||
$this->validateOnly('newTag.name');
|
||||
}
|
||||
|
||||
|
||||
public function checkTransLanguage()
|
||||
{
|
||||
// Ensure the language detector is initialized
|
||||
$this->getLanguageDetector();
|
||||
$detectedLanguage = $this->langDetector->detectSimple($this->inputTagTranslation['name']);
|
||||
|
||||
if ($detectedLanguage === $this->selectTranslationLanguage) {
|
||||
$this->transLanguageOk = true;
|
||||
// No need to ignore language detection when base locale is detected
|
||||
$this->transLanguageIgnored = false;
|
||||
} else {
|
||||
$this->transLanguageOk = false;
|
||||
}
|
||||
$this->validateOnly('inputTagTranslation.name');
|
||||
}
|
||||
|
||||
|
||||
public function checkTranslationAllowed()
|
||||
{
|
||||
// Check if translations are allowed based on config and profile type
|
||||
$allowTranslations = timebank_config('tags.allow_tag_transations_for_non_admins', false);
|
||||
$profileType = getActiveProfileType();
|
||||
|
||||
// If config is false, only admins can add translations
|
||||
if (!$allowTranslations) {
|
||||
$this->translationAllowed = ($profileType === 'admin');
|
||||
} else {
|
||||
// If config is true, all profile types can add translations
|
||||
$this->translationAllowed = true;
|
||||
}
|
||||
}
|
||||
|
||||
public function checkTranslationPossible()
|
||||
{
|
||||
// Check if profile is capable to do any translations
|
||||
$countNonBaseLanguages = getActiveProfile()->languages()->where('lang_code', '!=', timebank_config('base_language'))->count();
|
||||
if ($countNonBaseLanguages === 0 && app()->getLocale() === timebank_config('base_language')) {
|
||||
$this->translationPossible = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getLanguageDetector()
|
||||
{
|
||||
if (!$this->langDetector) {
|
||||
$this->langDetector = new \Text_LanguageDetect();
|
||||
$this->langDetector->setNameMode(2); // iso language code with 2 characters
|
||||
}
|
||||
return $this->langDetector;
|
||||
}
|
||||
|
||||
|
||||
public function updatedNewTagName()
|
||||
{
|
||||
$this->resetErrorBag('newTag.name');
|
||||
|
||||
// Check if name is the profiles's session's locale
|
||||
$this->checkSessionLanguage();
|
||||
// Only fall back to initTagsArray if newTagsArray has not been set yet,
|
||||
// to preserve any tags the user already added before opening the create modal
|
||||
if ($this->newTagsArray === null) {
|
||||
$this->newTagsArray = collect($this->initTagsArray);
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedSessionLanguageIgnored()
|
||||
{
|
||||
if (!$this->sessionLanguageIgnored) {
|
||||
$this->checkSessionLanguage();
|
||||
}
|
||||
|
||||
// Revalidate the newTag.name field
|
||||
$this->validateOnly('newTag.name');
|
||||
}
|
||||
|
||||
|
||||
public function updatedTransLanguageIgnored()
|
||||
{
|
||||
if (!$this->transLanguageIgnored) {
|
||||
$this->checkTransLanguage();
|
||||
} else {
|
||||
$this->resetErrorBag('inputTagTranslation.name');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updatedSelectTranslationLanguage()
|
||||
{
|
||||
$this->selectTagTranslation = [];
|
||||
// Suggest related tags in the selected translation language
|
||||
$this->translationOptions = $this->getTranslationOptions($this->selectTranslationLanguage);
|
||||
}
|
||||
|
||||
|
||||
public function updatedNewTagCategory()
|
||||
{
|
||||
$this->categoryColor = collect($this->categoryOptions)
|
||||
->firstWhere('category_id', $this->newTagCategory)['color'] ?? 'gray';
|
||||
$this->selectTagTranslation = [];
|
||||
// Suggest related tags in the selected translation language
|
||||
$this->translationOptions = $this->getTranslationOptions($this->selectTranslationLanguage);
|
||||
$this->resetErrorBag('inputTagTranslationCategory');
|
||||
}
|
||||
|
||||
|
||||
public function updatedInputTagTranslationName()
|
||||
{
|
||||
$this->validateOnly('inputTagTranslation.name');
|
||||
}
|
||||
|
||||
|
||||
public function updatedTagsArray()
|
||||
{
|
||||
// Prevent save during updating cycle of the tagsArray
|
||||
$this->saveDisabled = true;
|
||||
$this->newTagsArray = collect(json_decode($this->tagsArray, true));
|
||||
|
||||
$localesToCheck = [app()->getLocale(), '']; // Only current locale and tags without locale should be checked for any new tag keywords
|
||||
$newTagsArrayLocal = $this->newTagsArray->whereIn('locale', $localesToCheck);
|
||||
// map suggestion to lower case for search normalization of the $newEntries
|
||||
$suggestions = collect($this->suggestions)->map(function ($value) {
|
||||
return strtolower($value);
|
||||
});
|
||||
// Retrieve new tag entries not present in suggestions
|
||||
$newEntries = $newTagsArrayLocal->filter(function ($newItem) use ($suggestions) {
|
||||
return !$suggestions->contains(strtolower($newItem['value']));
|
||||
});
|
||||
// Add a new tag modal if there are new entries
|
||||
if (count($newEntries) > 0) {
|
||||
|
||||
$this->newTag['name'] = app()->getLocale() == 'de' ? $newEntries->flatten()->first() : ucfirst($newEntries->flatten()->first());
|
||||
$this->categoryOptions = Category::where('type', Tag::class)
|
||||
->get()
|
||||
->map(function ($category) {
|
||||
// Include all attributes, including appended ones
|
||||
return [
|
||||
'category_id' => $category->id,
|
||||
'name' => ucfirst($category->translation->name ?? ''), // Use the appended 'translation' attribute
|
||||
'description' => $category->relatedPathExSelfTranslation ?? '', // Appended attribute
|
||||
'color' => $category->relatedColor ?? 'gray',
|
||||
];
|
||||
})
|
||||
->sortBy('name')
|
||||
->values();
|
||||
|
||||
// Open the create tag modal
|
||||
$this->modalVisible = true;
|
||||
|
||||
// For proper validation, this needs to be done after the netTag.name input of the modal is visible
|
||||
$this->checkSessionLanguage();
|
||||
|
||||
} else {
|
||||
$newEntries = false;
|
||||
|
||||
// Enable save button as updating cycle tagsArray is finished by now
|
||||
$this->saveDisabled = false;
|
||||
}
|
||||
$this->checkChangesTagsArray();
|
||||
}
|
||||
|
||||
|
||||
public function checkChangesTagsArray()
|
||||
{
|
||||
// Check if tagsArray has been changed, to display 'unsaved changes' message next to save button
|
||||
$initTagIds = collect($this->initTagIds);
|
||||
$newTagIds = $this->newTagsArray->pluck('tag_id');
|
||||
$diffFromNew = $newTagIds->diff($initTagIds);
|
||||
$diffFromInit = $initTagIds->diff($newTagIds);
|
||||
|
||||
if ($diffFromNew->isNotEmpty() || $diffFromInit->isNotEmpty()) {
|
||||
$this->tagsArrayChanged = true;
|
||||
} else {
|
||||
$this->tagsArrayChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// When tagify raises focus, disable the save button
|
||||
public function tagifyFocus()
|
||||
{
|
||||
$this->saveDisabled = true;
|
||||
}
|
||||
|
||||
// When tagify looses focus, enable the save button
|
||||
public function tagifyBlur()
|
||||
{
|
||||
$this->saveDisabled = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function renderedModalVisible()
|
||||
{
|
||||
// Enable save button as updating cycle tagsArray is finished by now
|
||||
$this->saveDisabled = false;
|
||||
}
|
||||
|
||||
|
||||
public function updatedTranslationVisible()
|
||||
{
|
||||
if ($this->translationVisible && $this->translationAllowed) {
|
||||
$this->updatedNewTagCategory();
|
||||
|
||||
$profile = getActiveProfile();
|
||||
|
||||
if (!$profile) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($profile);
|
||||
|
||||
// Get all languages of the profile with good competence
|
||||
$this->translationLanguages = $profile
|
||||
->languages()
|
||||
->wherePivot('competence', 1)
|
||||
->where('lang_code', '!=', app()->getLocale())
|
||||
->get()
|
||||
->map(function ($language) {
|
||||
$language->name = trans($language->name); // Map the name property to a translation key
|
||||
return $language;
|
||||
});
|
||||
|
||||
// Make sure that always the base language is included even if the profile does not has it as a competence 1
|
||||
if (!$this->translationLanguages->contains('lang_code', 'en')) {
|
||||
$transLanguage = Language::where('lang_code', timebank_config('base_language'))->first();
|
||||
if ($transLanguage) {
|
||||
$transLanguage->name = trans($transLanguage->name); // Map the name property to a translation key
|
||||
// Add the base language to the translationLanguages collection
|
||||
$this->translationLanguages = collect($this->translationLanguages)
|
||||
->push($transLanguage);
|
||||
}
|
||||
|
||||
// Set the default selection to base language
|
||||
if (app()->getLocale() != timebank_config('base_language')) {
|
||||
$this->selectTranslationLanguage = timebank_config('base_language');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function updatedTranslateRadioButton()
|
||||
{
|
||||
if ($this->translateRadioButton === 'select') {
|
||||
$this->inputDisabled = true;
|
||||
$this->dispatch('disableInput');
|
||||
} elseif ($this->translateRadioButton === 'input') {
|
||||
$this->inputDisabled = false;
|
||||
// $this->dispatch('disableSelect'); // Script inside view skills-form.blade.php
|
||||
}
|
||||
$this->resetErrorBag('selectTagTranslation');
|
||||
$this->resetErrorBag('inputTagTranslation.name');
|
||||
$this->resetErrorBag('newTagCategory');
|
||||
}
|
||||
|
||||
|
||||
public function updatedSelectTagTranslation()
|
||||
{
|
||||
if ($this->selectTagTranslation) {
|
||||
$this->categoryColor = Tag::find($this->selectTagTranslation)->categories->first()->relatedColor ?? 'gray';
|
||||
$this->translateRadioButton = 'select';
|
||||
$this->dispatch('disableInput');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updatedInputTagTranslation()
|
||||
{
|
||||
$this->translateRadioButton = 'input';
|
||||
$this->inputDisabled = false;
|
||||
// $this->dispatch('disableSelect'); // Script inside view skills-form.blade.php
|
||||
$this->checkTransLanguage();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Updates the visibility of the modal. If the modal becomes invisible, dispatches the 'remove' event to remove the last value of the tags array on the front-end.
|
||||
*/
|
||||
public function updatedModalVisible()
|
||||
{
|
||||
if ($this->modalVisible == false) {
|
||||
$this->dispatch('remove'); // Removes last value of the tagsArray on front-end only
|
||||
$this->dispatch('reinitializeComponent');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves a list of related tags based on the specified category and locale.
|
||||
* Get all translation options in the choosen locale,
|
||||
* but exclude all tags already have a similar context in the current $appLocal.
|
||||
*
|
||||
* @param int|null $category The ID of the category to filter related tags. If null, all tags in the locale are suggested.
|
||||
* @param string|null $locale The locale to use for tag names. If not provided, the application's current locale is used.
|
||||
*
|
||||
* @return \Illuminate\Support\Collection A collection of tags containing 'tag_id' and 'name' keys, sorted by name.
|
||||
*/
|
||||
|
||||
public function getTranslationOptions($locale)
|
||||
{
|
||||
$appLocale = app()->getLocale();
|
||||
|
||||
// 1) Get all context_ids used by tags that match app()->getLocale()
|
||||
$contextIdsInAppLocale = DB::table('taggable_locale_context')
|
||||
->whereIn('tag_id', function ($query) use ($appLocale) {
|
||||
$query->select('taggable_tag_id')
|
||||
->from('taggable_locales')
|
||||
->where('locale', $appLocale);
|
||||
})
|
||||
->pluck('context_id');
|
||||
|
||||
// 2) Exclude tags that share these context_ids
|
||||
$tags = Tag::with(['locale', 'contexts.category'])
|
||||
->whereHas('locale', function ($query) use ($locale) {
|
||||
$query->where('locale', $locale);
|
||||
})
|
||||
->whereNotIn('tag_id', function ($subquery) use ($contextIdsInAppLocale) {
|
||||
$subquery->select('tag_id')
|
||||
->from('taggable_locale_context')
|
||||
->whereIn('context_id', $contextIdsInAppLocale);
|
||||
})
|
||||
->get();
|
||||
|
||||
// 3) Build the options array. Adjust the name logic to your preference.
|
||||
$options = $tags->map(function ($tag) use ($locale) {
|
||||
$category = optional($tag->contexts->first())->category;
|
||||
$description = optional(optional($category)->translation)->name ?? '';
|
||||
|
||||
return [
|
||||
'tag_id' => $tag->tag_id,
|
||||
'name' => $locale == 'de' ? $tag->name : StringHelper::DutchTitleCase($tag->normalized),
|
||||
'description' => $description,
|
||||
];
|
||||
})->sortBy('name')->values();
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Cancels the creation of a new tag by resetting error messages,
|
||||
* clearing input fields, hiding translation and modal visibility,
|
||||
* and resetting tag arrays to their initial state.
|
||||
*/
|
||||
public function cancelCreateTag()
|
||||
{
|
||||
$this->resetErrorBag();
|
||||
$this->newTag = [];
|
||||
$this->newTagCategory = null;
|
||||
$this->translationVisible = false;
|
||||
$this->translateRadioButton = null;
|
||||
$this->sessionLanguageOk = false;
|
||||
$this->sessionLanguageIgnored = false;
|
||||
$this->transLanguageOk = false;
|
||||
$this->transLanguageIgnored = false;
|
||||
$this->categoryColor = 'gray';
|
||||
$this->selectTagTranslation = null;
|
||||
$this->inputTagTranslation = [];
|
||||
$this->inputDisabled = true;
|
||||
// $this->newTagsArray = collect($this->initTagsArray);
|
||||
// $this->tagsArray = json_encode($this->initTagsArray);
|
||||
|
||||
// Remove last value of the tagsArray
|
||||
$tagsArray = is_string($this->tagsArray) ? json_decode($this->tagsArray, true) : $this->tagsArray;
|
||||
array_pop($tagsArray);
|
||||
$this->tagsArray = json_encode($tagsArray);
|
||||
|
||||
// Check of there were also other unsaved new tags in the tagsArray
|
||||
$hasNoTagId = false;
|
||||
if (is_array($tagsArray)) {
|
||||
$this->tagsArrayChanged = count(array_filter($tagsArray, function ($tag) {
|
||||
return !array_key_exists('tag_id', $tag) || $tag['tag_id'] === null;
|
||||
})) > 0;
|
||||
} else {
|
||||
$this->tagsArrayChanged = false;
|
||||
}
|
||||
|
||||
$this->modalVisible = false;
|
||||
$this->updatedModalVisible();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handles the save button of the create tag modal.
|
||||
*
|
||||
* Creates a new tag for the currently active profile and optionally
|
||||
* associates it with a category or base-language translation.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function createTag()
|
||||
{
|
||||
// TODO: MAKE A TRANSACTION
|
||||
|
||||
$this->validate();
|
||||
$this->resetErrorBag();
|
||||
|
||||
// Format strings to correct case
|
||||
$this->newTag['name'] = app()->getLocale() == 'de' ? $this->newTag['name'] : StringHelper::DutchTitleCase($this->newTag['name']);
|
||||
|
||||
$name = $this->newTag['name'];
|
||||
$normalized = call_user_func(config('taggable.normalizer'), $name);
|
||||
|
||||
// Create the tag and attach the owner and context
|
||||
$tag = Tag::create([
|
||||
'name' => $name,
|
||||
'normalized' => $normalized,
|
||||
]);
|
||||
$owner = getActiveProfile();
|
||||
|
||||
if (!$owner) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($owner);
|
||||
$owner->tagById($tag->tag_id);
|
||||
$context = [
|
||||
'category_id' => $this->newTagCategory,
|
||||
'updated_by_user' => Auth::guard('web')->user()->id, // use the logged user, not the active profile
|
||||
];
|
||||
|
||||
if ($this->translationVisible) {
|
||||
if ($this->translateRadioButton === 'select') {
|
||||
// Attach an existing context in the base language to the new tag. See timebank_config('base_language')
|
||||
// Note that the category_id and updated_by_user is not updated when selecting an existing context
|
||||
$tagContext = Tag::find($this->selectTagTranslation)
|
||||
->contexts()
|
||||
->first();
|
||||
$tag->contexts()->attach($tagContext->id);
|
||||
} elseif ($this->translateRadioButton === 'input') {
|
||||
// Create a new context for the new tag
|
||||
$tagContext = $tag->contexts()->create($context);
|
||||
|
||||
// Create a new translation of the tag
|
||||
$this->inputTagTranslation['name'] = $this->selectTranslationLanguage == 'de' ? $this->inputTagTranslation['name'] : StringHelper::DutchTitleCase($this->inputTagTranslation['name']);
|
||||
// $owner->tag($this->inputTagTranslation['name']);
|
||||
$nameTranslation = $this->inputTagTranslation['name'];
|
||||
$normalizedTranslation = call_user_func(config('taggable.normalizer'), $nameTranslation);
|
||||
$locale = ['locale' => $this->selectTranslationLanguage ];
|
||||
|
||||
// Create the translation tag with the locale and attach the context
|
||||
$tagTranslation = Tag::create([
|
||||
'name' => $nameTranslation,
|
||||
'normalized' => $normalizedTranslation,
|
||||
]);
|
||||
$tagTranslation->locale()->create($locale);
|
||||
$tagTranslation->contexts()->attach($tagContext->id);
|
||||
|
||||
// The translation now has been recorded. Next, detach owner from this translation as only the locale tag should be attached to the owner
|
||||
$owner->untagById([$tagTranslation->tag_id]);
|
||||
// Also clean up owner's tags that have similar context but have different locale. Only the tag in owner's app()->getLocale() should remain in db.
|
||||
$owner->cleanTaggables(); // In TaggableWithLocale trait
|
||||
|
||||
}
|
||||
} else {
|
||||
// Create a new context for the new tag without translation
|
||||
$tagContext = $tag->contexts()->create($context);
|
||||
}
|
||||
|
||||
$this->modalVisible = false;
|
||||
$this->saveDisabled = false;
|
||||
// Attach the new collection of tags to the active profile
|
||||
$this->save();
|
||||
$this->tagsArrayChanged = false;
|
||||
|
||||
// Dispatch the SendEmailNewTag job
|
||||
SendEmailNewTag::dispatch($tag->tag_id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Saves the newTagsArray: attaches the current tags to the profile model.
|
||||
* Ignores the tags that are marked read-only (no app locale and no base language locale).
|
||||
* Dispatches notification on success or error.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
if ($this->saveDisabled === false) {
|
||||
|
||||
if ($this->newTagsArray) {
|
||||
try {
|
||||
// Use a transaction for saving skill tags
|
||||
DB::transaction(function () {
|
||||
// Make sure we can count newTag for conditional validation rules
|
||||
if ($this->newTag === null) {
|
||||
$this->newTag = [];
|
||||
}
|
||||
|
||||
$owner = getActiveProfile();
|
||||
|
||||
if (!$owner) {
|
||||
abort(403, 'No active profile');
|
||||
}
|
||||
|
||||
// CRITICAL SECURITY: Validate user has ownership/access to this profile
|
||||
\App\Helpers\ProfileAuthorizationHelper::authorize($owner);
|
||||
|
||||
$this->validate();
|
||||
// try {
|
||||
// $this->validate();
|
||||
// } catch (ValidationException $e) {
|
||||
// // Dump all validation errors to the log or screen
|
||||
// logger()->error('Validation failed', $e->errors());
|
||||
// dd($e->errors()); // or use dump() if you prefer
|
||||
// }
|
||||
$this->resetErrorBag();
|
||||
|
||||
$initTags = collect($this->initTagsArray)->pluck('taggable_tag_id');
|
||||
|
||||
$newTagsArray = collect($this->newTagsArray);
|
||||
|
||||
$newTags = $newTagsArray
|
||||
->where('tag_id', null)
|
||||
->pluck('value')->toArray();
|
||||
$owner->tag($newTags);
|
||||
|
||||
$remainingTags = $this->newTagsArray
|
||||
->where('tag_id')
|
||||
->pluck('tag_id')->toArray();
|
||||
|
||||
$removedTags = $initTags->diff($remainingTags)->toArray();
|
||||
$owner->untagById($removedTags);
|
||||
|
||||
// Finaly clean up taggables table: remove duplicate contexts and any orphaned taggables
|
||||
// In TaggableWithLocale trait
|
||||
$owner->cleanTaggables();
|
||||
|
||||
$owner->touch(); // Observer catches this and reindexes search index
|
||||
|
||||
// WireUI notification
|
||||
$this->notification()->success($title = __('Your have updated your profile successfully!'));
|
||||
});
|
||||
// end of transaction
|
||||
} catch (Throwable $e) {
|
||||
// WireUI notification
|
||||
// TODO!: create event to send error notification to admin
|
||||
$this->notification([
|
||||
'title' => __('Update failed!'),
|
||||
'description' => __('Sorry, your data could not be saved!') . '<br /><br />' . __('Our team has ben notified about this error. Please try again later.') . '<br /><br />' . $e->getMessage(),
|
||||
'icon' => 'error',
|
||||
'timeout' => 100000,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->tagsArrayChanged = false;
|
||||
$this->dispatch('saved');
|
||||
$this->forgetCachedSkills();
|
||||
$this->cacheSkills();
|
||||
$this->initTagsArray = [];
|
||||
$this->newTag = null;
|
||||
$this->newTagsArray = null;
|
||||
$this->newTagCategory = null;
|
||||
$this->dispatch('refreshComponent');
|
||||
$this->dispatch('reinitializeTagify');
|
||||
$this->dispatch('reloadPage');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function forgetCachedSkills()
|
||||
{
|
||||
// Get the profile type (user / organization) from the session and convert to lowercase
|
||||
$profileType = strtolower(basename(str_replace('\\', '/', session('activeProfileType'))));
|
||||
// Get the supported locales from the config
|
||||
$locales = config('app.supported_locales', [app()->getLocale()]);
|
||||
// Iterate over each locale and forget the cache
|
||||
foreach ($locales as $locale) {
|
||||
Cache::forget('skills-' . $profileType . '-' . session('activeProfileId') . '-lang-' . $locale);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function cacheSkills()
|
||||
{
|
||||
$profileType = strtolower(basename(str_replace('\\', '/', session('activeProfileType')))); // Get the profile type (user / organization) from the session and convert to lowercase
|
||||
|
||||
$skillsCache = Cache::remember('skills-' . $profileType . '-' . session('activeProfileId') . '-lang-' . app()->getLocale(), now()->addDays(7), function () {
|
||||
// remember cache for 7 days
|
||||
$tagIds = session('activeProfileType')::find(session('activeProfileId'))->tags->pluck('tag_id');
|
||||
$translatedTags = collect((new Tag())->translateTagIdsWithContexts($tagIds, App::getLocale(), App::getFallbackLocale())); // Translate to app locale, if not available to fallback locale, if not available do not translate
|
||||
$skills = $translatedTags->map(function ($item, $key) {
|
||||
return [
|
||||
'original_tag_id' => $item['original_tag_id'],
|
||||
'tag_id' => $item['tag_id'],
|
||||
'name' => $item['tag'],
|
||||
'foreign' => $item['locale'] == App::getLocale() ? false : true, // Mark all tags in a foreign language read-only, as users need to switch locale to edit/update/etc foreign tags
|
||||
'locale' => $item['locale'],
|
||||
'category' => $item['category'],
|
||||
'category_path' => $item['category_path'],
|
||||
'category_color' => $item['category_color'],
|
||||
];
|
||||
});
|
||||
$skills = collect($skills);
|
||||
|
||||
return $skills;
|
||||
});
|
||||
|
||||
$this->tagsArray = json_encode($skillsCache->toArray());
|
||||
}
|
||||
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.main-page.skills-card-full');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user