Changes
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
use Carbon\Carbon;
|
||||
|
||||
class MainSliderDTO
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $title,
|
||||
public ?string $content,
|
||||
public ?string $image,
|
||||
public ?string $link,
|
||||
public ?string $link_text,
|
||||
public ?string $color_theme,
|
||||
public ?bool $is_active,
|
||||
public ?Carbon $start_time,
|
||||
public ?Carbon $end_time,
|
||||
public ?int $sort,
|
||||
) {}
|
||||
|
||||
// Опционально: метод для создания DTO из массива
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
return new self(
|
||||
title: $data['title'] ?? null,
|
||||
content: $data['content'] ?? null,
|
||||
image: $data['image'] ?? null,
|
||||
link: $data['link'] ?? null,
|
||||
link_text: $data['link_text'] ?? null,
|
||||
color_theme: $data['color_theme'] ?? 'white',
|
||||
is_active: $data['is_active'] ?? true,
|
||||
start_time: isset($data['start_time']) ? Carbon::parse($data['start_time']) : null,
|
||||
end_time: isset($data['end_time']) ? Carbon::parse($data['end_time']) : null,
|
||||
sort: $data['sort'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
// Опционально: метод для преобразования DTO в массив
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'title' => $this->title,
|
||||
'content' => $this->content,
|
||||
'image' => $this->image,
|
||||
'link' => $this->link,
|
||||
'link_text' => $this->link_text,
|
||||
'color_theme' => $this->color_theme,
|
||||
'is_active' => $this->is_active,
|
||||
'start_time' => $this->start_time?->toDateTimeString(),
|
||||
'end_time' => $this->end_time?->toDateTimeString(),
|
||||
'sort' => $this->sort,
|
||||
'created_at' => $this->created_at?->toDateTimeString(),
|
||||
'updated_at' => $this->updated_at?->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use App\Models\PageReferenceList;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\Builder;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Grid;
|
||||
@@ -97,7 +98,7 @@ class PostForm
|
||||
]),
|
||||
Tabs\Tab::make('Содержание новости')
|
||||
->schema([
|
||||
ContentBuilderItem::getItem('content')
|
||||
ContentBuilderItem::getItem('content')->required(),
|
||||
]),
|
||||
Tabs\Tab::make('Изображения')
|
||||
->schema([
|
||||
@@ -118,6 +119,51 @@ class PostForm
|
||||
->multiple()
|
||||
->directory('images'),
|
||||
]),
|
||||
Tabs\Tab::make('Добавление новости в слайдер')
|
||||
->schema([
|
||||
Toggle::make('is_slider_enabled')
|
||||
->label('Добавить новый слайд')
|
||||
->live()
|
||||
->dehydrated(false)
|
||||
->default(false),
|
||||
Section::make()
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('slide.title')
|
||||
->label('Заголовок слайда'),
|
||||
Forms\Components\Textarea::make('slide.content')
|
||||
->label('Текст слайда'),
|
||||
FileUpload::make('slide.image')
|
||||
->label('Изображение')
|
||||
->image()
|
||||
->optimize('webp')
|
||||
->resize(50)
|
||||
->disk('public')
|
||||
->directory('images')
|
||||
->imageEditor()
|
||||
->required(),
|
||||
Grid::make(2)->schema([
|
||||
Toggle::make('disable_link_text')
|
||||
->label('Отключить текст кнопки (ссылка будет открываться при нажатии на слайд)')
|
||||
->live()
|
||||
->inline(false)
|
||||
->dehydrated(false)
|
||||
->default(false),
|
||||
Forms\Components\TextInput::make('slide.link_text')
|
||||
->default('Читать')
|
||||
->label('Текст кнопки')
|
||||
->disabled(fn (Forms\Get $get) => $get('disable_link_text')),
|
||||
]),
|
||||
|
||||
DateTimePicker::make('slide.end_time')
|
||||
->label('Слайд действует до')
|
||||
->native()
|
||||
->displayFormat('d/m/Y')
|
||||
->minDate(Carbon::now())
|
||||
->maxDate(Carbon::now()->addWeek()),
|
||||
])
|
||||
->disabled(fn (Forms\Get $get) => !$get('is_slider_enabled')) // Отключаем секцию, если Toggle выключен
|
||||
->hidden(fn (Forms\Get $get) => !$get('is_slider_enabled')), // Скрываем секцию, если Toggle выключен
|
||||
])->hidden(fn (string $context): bool => $context === 'edit'),
|
||||
]),
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -10,6 +10,7 @@ use Filament\Forms;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Columns\TextInputColumn;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
@@ -29,7 +30,7 @@ class AcceptedInvitationResource extends Resource implements HasShieldPermission
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
//
|
||||
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -37,7 +38,8 @@ class AcceptedInvitationResource extends Resource implements HasShieldPermission
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
//
|
||||
Tables\Columns\TextColumn::make('receiver.name')->label('Приглашенный пользователь'),
|
||||
TextInputColumn::make('post_limit')->label('Лимит постов')->default(0)->rules(['required', 'max:10', 'integer'])
|
||||
])
|
||||
->filters([
|
||||
//
|
||||
|
||||
@@ -104,4 +104,16 @@ class ContactWidgetResource extends Resource
|
||||
'edit' => Pages\EditContactWidget::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPermissionPrefixes(): array
|
||||
{
|
||||
return [
|
||||
'view',
|
||||
'view_any',
|
||||
'create',
|
||||
'update',
|
||||
'delete',
|
||||
'delete_any',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Filament\Resources;
|
||||
|
||||
use App\Enums\FormEducation;
|
||||
use App\Filament\Resources\EducationalGroupResource\Pages;
|
||||
use App\Filament\Resources\EducationalGroupResource\RelationManagers;
|
||||
use App\Models\EducationalGroup;
|
||||
@@ -33,7 +34,9 @@ class EducationalGroupResource extends Resource
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
Forms\Components\TextInput::make('title')->label('Название группы')->required(),
|
||||
Forms\Components\Select::make('faculty_id')->label('Факультет')->required()
|
||||
->options(Faculty::all()->pluck('title', 'id'))
|
||||
->options(Faculty::all()->pluck('title', 'id')),
|
||||
Forms\Components\Select::make('education_form_id')->label('Форма обучения')
|
||||
->options(FormEducation::class)
|
||||
]),
|
||||
]),
|
||||
]);
|
||||
|
||||
@@ -10,15 +10,19 @@ use App\Models\Page;
|
||||
use App\Models\Post;
|
||||
use Filament\Forms;
|
||||
use Filament\Forms\Components\ColorPicker;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\FileUpload;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\ToggleButtons;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Yepsua\Filament\Forms\Components\RangeSlider;
|
||||
|
||||
class MainSliderResource extends Resource
|
||||
{
|
||||
@@ -96,32 +100,75 @@ class MainSliderResource extends Resource
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Section::make('Слайдер')->schema([
|
||||
|
||||
Forms\Components\Section::make('Информация слайда')->schema([
|
||||
Forms\Components\TextInput::make('title')
|
||||
->label('Заголовок слайда')
|
||||
->required(),
|
||||
->label('Заголовок слайда'),
|
||||
Forms\Components\Textarea::make('content')
|
||||
->label('Текст слайда'),
|
||||
FileUpload::make('image')
|
||||
Forms\Components\Grid::make()->schema([
|
||||
ColorPicker::make('color_theme')
|
||||
->label('Цвет текста')
|
||||
->default('#ffffff')
|
||||
->required(),
|
||||
Forms\Components\ToggleButtons::make('settings.text_position')
|
||||
->options([
|
||||
'left' => 'Текст слева',
|
||||
'center' => 'Текст по середине',
|
||||
'right' => 'Текст справа'
|
||||
])
|
||||
->inline()->default('left')->grouped()
|
||||
->label('Позиция текста на слайде'),
|
||||
]),
|
||||
Forms\Components\Grid::make()->schema([
|
||||
Toggle::make('active_button')
|
||||
->label('Использовать кнопку для ссылки (Ссылка будет открываться при нажатии на слайд)')
|
||||
->inline(false)
|
||||
->default(true)
|
||||
->live()
|
||||
->dehydrated(false),
|
||||
Forms\Components\TextInput::make('settings.link_text')
|
||||
->default('Читать')
|
||||
->label('Текст кнопки')
|
||||
->disabled(fn (Forms\Get $get) => !$get('active_button'))
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Section::make('Изображение')->schema([
|
||||
FileUpload::make('image.url')
|
||||
->label('Изображение')
|
||||
->image()
|
||||
->optimize('webp')
|
||||
->resize(50)
|
||||
->disk('public')
|
||||
->directory('images')
|
||||
->imageEditor(),
|
||||
Forms\Components\Grid::make(2)->schema([
|
||||
->imageEditor()
|
||||
->required(),
|
||||
ToggleButtons::make('image.shading')->inline()->grouped()->label('Уровень затемнения изображения')->options([
|
||||
'1' => 'Без затемнения',
|
||||
'0.7' => 'Слабое затемнение',
|
||||
'0.5' => 'Среднее затемнение',
|
||||
'0.3' => 'Сильное затемнение',
|
||||
]),
|
||||
]),
|
||||
Forms\Components\Section::make('Общая часть')->schema([
|
||||
Forms\Components\Grid::make()->schema([
|
||||
DateTimePicker::make('start_time')
|
||||
->label('Слайд начинается с')
|
||||
->native()
|
||||
->displayFormat('d/m/Y')
|
||||
->minDate(Carbon::now()->subDay())
|
||||
->maxDate(Carbon::now()->addWeek()),
|
||||
DateTimePicker::make('end_time')
|
||||
->label('Слайд действует до')
|
||||
->native()
|
||||
->displayFormat('d/m/Y')
|
||||
->minDate(Carbon::now())
|
||||
->maxDate(Carbon::now()->addMonth()),
|
||||
]),
|
||||
Forms\Components\TextInput::make('link')
|
||||
->label('Ссылка кнопки')
|
||||
->required(),
|
||||
Forms\Components\TextInput::make('link_text')
|
||||
->default('Читать')
|
||||
->label('Текст кнопки')
|
||||
->required(),
|
||||
]),
|
||||
ColorPicker::make('color_theme')
|
||||
->label('Цвет текста')
|
||||
->default('#ffffff')
|
||||
->required(),
|
||||
|
||||
Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false),
|
||||
]),
|
||||
|
||||
@@ -44,8 +44,6 @@ class PageResource extends Resource
|
||||
|
||||
protected static ?string $pluralLabel = 'Страницы';
|
||||
|
||||
public static ?string $label = 'Страница';
|
||||
|
||||
|
||||
protected static ?string $navigationGroup = 'Структура приложения';
|
||||
|
||||
|
||||
@@ -59,10 +59,12 @@ class PostResource extends Resource implements HasShieldPermissions
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
// Tables\Columns\TextColumn::make('id')->sortable(),
|
||||
Tables\Columns\TextColumn::make('created_at')->label('Дата создания')->sortable(),
|
||||
Tables\Columns\TextColumn::make('title')->label('Заголовок')->sortable()->searchable(),
|
||||
Tables\Columns\TextColumn::make('status')->label('Статус')->sortable()->badge(),
|
||||
Tables\Columns\TextColumn::make('publish_at')->label('Дата публикации')->sortable(),
|
||||
Tables\Columns\TextColumn::make('author.name')->label('Автор')->sortable()->searchable(),
|
||||
|
||||
])->defaultSort('publish_at', 'desc')
|
||||
->filters([
|
||||
@@ -92,6 +94,8 @@ class PostResource extends Resource implements HasShieldPermissions
|
||||
'index' => Pages\ListPosts::route('/'),
|
||||
'create' => Pages\CreatePost::route('/create'),
|
||||
'edit' => Pages\EditPost::route('/{record}/edit'),
|
||||
'view' => Pages\ViewPost::route('/{record}'),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,29 +2,15 @@
|
||||
|
||||
namespace App\Filament\Resources\PostResource\Pages;
|
||||
|
||||
use App\Dto\MainSliderDTO;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Resources\PostResource;
|
||||
use App\Jobs\CreateVkPost;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Services\VK\VkService;
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
use Filament\Actions;
|
||||
use Filament\Notifications\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use App\Services\Filament\Domain\Posts\PostDataProcessor;
|
||||
use App\Services\Filament\Domain\Posts\PostNotificationService;
|
||||
use App\Services\Filament\Domain\Posts\PostSeoGenerator;
|
||||
use App\Services\Filament\Domain\Posts\PostSliderService;
|
||||
use App\Services\Filament\Domain\Posts\VkPostPublisher;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Messages\BroadcastMessage;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use PhpParser\Node\Expr\AssignOp\Mod;
|
||||
use VK\Client\VKApiClient;
|
||||
use VK\OAuth\Scopes\VKOAuthGroupScope;
|
||||
use VK\OAuth\Scopes\VKOAuthUserScope;
|
||||
use VK\OAuth\VKOAuth;
|
||||
use VK\OAuth\VKOAuthDisplay;
|
||||
use VK\OAuth\VKOAuthResponseType;
|
||||
|
||||
class CreatePost extends CreateRecord
|
||||
{
|
||||
@@ -33,277 +19,68 @@ class CreatePost extends CreateRecord
|
||||
protected array $seoData;
|
||||
protected array $publicationAgreements;
|
||||
|
||||
protected array $slideData;
|
||||
|
||||
protected static array|string $routeMiddleware = ['limit.post'];
|
||||
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$this->publicationAgreements = $data['publication'];
|
||||
unset($data['publication']);
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$data['preview_text'] = $this->setPreviewText($data);
|
||||
$data['publish_at'] = $this->setPublishDateTime($data['publish_setting']);
|
||||
unset($data['publish_setting']);
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
||||
return $data;
|
||||
$this->extractAdditionalData($data);
|
||||
return $this->processPostData($data);
|
||||
}
|
||||
|
||||
protected function extractAdditionalData(array &$data): void
|
||||
{
|
||||
$this->publicationAgreements = $data['publication'] ?? [];
|
||||
$this->slideData = $data['slide'] ?? [];
|
||||
unset($data['slide'], $data['publication']);
|
||||
}
|
||||
|
||||
protected function processPostData(array $data): array
|
||||
{
|
||||
return (new PostDataProcessor())->process($data);
|
||||
}
|
||||
|
||||
protected function afterCreate(): void
|
||||
{
|
||||
$this->record->seo()->create($this->seoData);
|
||||
$this->sendNotify($this->record, auth()->user());
|
||||
$publish_date = ($this->record->publish_at > now()) ? Carbon::parse($this->record->publish_at)->timestamp : null;
|
||||
$this->postToSocialMedia($this->publicationAgreements, $this->record->content, $this->record->title, $publish_date);
|
||||
$this->handleSlides();
|
||||
$this->generateSeo();
|
||||
$this->sendNotifications();
|
||||
$this->publishToVk();
|
||||
}
|
||||
|
||||
private function generateSeo(array $data) : array
|
||||
protected function handleSlides(): void
|
||||
{
|
||||
$title = $data['title'];
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||
if ($rowData === null) {
|
||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
||||
}
|
||||
if ($rowData !== null) {
|
||||
$description = html_entity_decode(strip_tags($rowData['data']['content']));
|
||||
} else {
|
||||
$description = null;
|
||||
} $image = ($data['preview'] !== null) ? $data['preview'] : null;
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
|
||||
'image' => $image,
|
||||
];
|
||||
if (empty($this->slideData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private function setPreviewText(array $data) : string
|
||||
$this->slideData['is_active'] = $this->record->status === PostStatus::PUBLISHED;
|
||||
$this->slideData['start_time'] = $this->record->publish_at;
|
||||
|
||||
$sliderDTO = MainSliderDTO::fromArray($this->slideData);
|
||||
(new PostSliderService($sliderDTO, $this->record->slug))->create();
|
||||
}
|
||||
|
||||
protected function generateSeo(): void
|
||||
{
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||
if ($rowData === null) {
|
||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
||||
}
|
||||
$preview_text = html_entity_decode(strip_tags($rowData['data']['content']));
|
||||
return Str::limit($preview_text, 160);
|
||||
$seoData = (new PostSeoGenerator())->generate([
|
||||
'title' => $this->record->title,
|
||||
'content' => $this->record->content,
|
||||
'preview' => $this->record->preview,
|
||||
]);
|
||||
$this->record->seo()->create($seoData);
|
||||
}
|
||||
|
||||
private function generateSearchData(array $data) : string
|
||||
protected function sendNotifications(): void
|
||||
{
|
||||
$result = "";
|
||||
foreach ($data as $block) {
|
||||
$result .= $this->getDataFromBlocks($block);
|
||||
(new PostNotificationService())->send($this->record);
|
||||
}
|
||||
// Удаляем лишние пробелы и переносы строк
|
||||
$result = preg_replace('/\s+/', ' ', $result);
|
||||
$result = trim($result);
|
||||
|
||||
return strtolower($result);
|
||||
}
|
||||
private function getFirstBlockByName(string $name, array $content) : array|null
|
||||
protected function publishToVk(): void
|
||||
{
|
||||
$data = null;
|
||||
foreach ($content as $block) {
|
||||
$data = ($block['type'] === $name) ? $block : null;
|
||||
break;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getBlockBySeoActiveState(string $name, array $content) : array|null
|
||||
{
|
||||
$data = [];
|
||||
foreach ($content as $block) {
|
||||
if ($block['type'] === $name) {
|
||||
$data[] = $block;
|
||||
}
|
||||
}
|
||||
$block = null;
|
||||
foreach ($data as $item) {
|
||||
if ($item['data']['seo_active'] === true) {
|
||||
$block = $item;
|
||||
}
|
||||
}
|
||||
return $block;
|
||||
}
|
||||
|
||||
private function sendNotify($post, $recipient) : void
|
||||
{
|
||||
Notification::make()
|
||||
->title('Новость на проверку')
|
||||
->body('Новая запись была создана!')
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->label('Проверить')
|
||||
->button()
|
||||
->markAsRead()
|
||||
->url(PostResource::getUrl('edit', ['record' => $post])),
|
||||
|
||||
])->sendToDatabase($recipient);
|
||||
}
|
||||
private function setPublishDateTime(array $data) : Carbon|null
|
||||
{
|
||||
if ($data['publish_after'] === true) {
|
||||
return Carbon::parse($data['publish_at']);
|
||||
}
|
||||
return Carbon::now();
|
||||
}
|
||||
private function calculateReadingTime(string $text): int
|
||||
{
|
||||
|
||||
// Calculate the number of words in the text
|
||||
$wordCount = str_word_count($text,0,"АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя");
|
||||
|
||||
|
||||
|
||||
|
||||
// Calculate the average reading speed in words per minute
|
||||
$wordsPerMinute = 120; // You can adjust this value based on your desired reading speed
|
||||
|
||||
// Calculate the reading time in minutes
|
||||
$readingTime = $wordCount / $wordsPerMinute;
|
||||
|
||||
// Round the reading time to the nearest integer
|
||||
$readingTime = round($readingTime);
|
||||
|
||||
|
||||
return $readingTime;
|
||||
}
|
||||
protected function convertDataToHtml($blocks) {
|
||||
$convertedHtml = "";
|
||||
foreach ($blocks as $block) {
|
||||
switch ($block['type']) {
|
||||
case "header":
|
||||
$convertedHtml .= "<h" . $block['data']['level'] . ">" . $block['data']['text'] . "</h" . $block['data']['level'] . ">";
|
||||
break;
|
||||
case "embded":
|
||||
$convertedHtml .= "<div><iframe width='560' height='315' src='" . $block['data']['embed'] . "' frameborder='0' allow='autoplay; encrypted-media' allowfullscreen></iframe></div>";
|
||||
break;
|
||||
case "paragraph":
|
||||
$convertedHtml .= "<p>" . $block['data']['text'] . "</p>";
|
||||
break;
|
||||
case "delimiter":
|
||||
$convertedHtml .= "<hr />";
|
||||
break;
|
||||
case "image":
|
||||
$convertedHtml .= "<img class='img-fluid' src='" . $block['data']['file']['url'] . "' title='" . $block['data']['caption'] . "' /><br /><em>" . $block['data']['caption'] . "</em>";
|
||||
break;
|
||||
case "list":
|
||||
$convertedHtml .= "<ul>";
|
||||
foreach ($block['data']['items'] as $li) {
|
||||
$convertedHtml .= "<li>" . $li . "</li>";
|
||||
}
|
||||
$convertedHtml .= "</ul>";
|
||||
break;
|
||||
case "table":
|
||||
$convertedHtml .= "<table>";
|
||||
if ($block['data']['withHeadings']) {
|
||||
$convertedHtml .= "<thead><tr>";
|
||||
foreach ($block['data']['content'][0] as $th) {
|
||||
$convertedHtml .= "<th>" . $th . "</th>";
|
||||
}
|
||||
$convertedHtml .= "</tr></thead>";
|
||||
}
|
||||
$convertedHtml .= "<tbody>";
|
||||
foreach ($block['data']['content'] as $row) {
|
||||
$convertedHtml .= "<tr>";
|
||||
foreach ($row as $td) {
|
||||
$convertedHtml .= "<td>" . $td . "</td>";
|
||||
}
|
||||
$convertedHtml .= "</tr>";
|
||||
}
|
||||
$convertedHtml .= "</tbody></table>";
|
||||
break;
|
||||
default:
|
||||
echo "Unknown block type " . $block['type'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $convertedHtml;
|
||||
}
|
||||
private function getDataFromBlocks($block) : string
|
||||
{
|
||||
$data = "";
|
||||
switch ($block['type']) {
|
||||
case 'paragraph':
|
||||
$data .= strip_tags($block['data']['content']) . " ";
|
||||
break;
|
||||
case 'heading':
|
||||
$data .= strip_tags($block['data']['content']) . " ";
|
||||
break;
|
||||
case 'files':
|
||||
foreach ($block['data']['file'] as $file) {
|
||||
$data .= $file['title'] . " ";
|
||||
}
|
||||
break;
|
||||
case 'person':
|
||||
$data .= $block['data']['name'] . " ";
|
||||
break;
|
||||
case 'stepper':
|
||||
$data .= $block['data']['step_name'] . " ";
|
||||
foreach ($block['data']['steps'] as $step) {
|
||||
$data .= $step['title'] . " ";
|
||||
$data .= strip_tags($step['content']) . " ";
|
||||
}
|
||||
break;
|
||||
case 'tabs':
|
||||
foreach ($block['data']['tab'] as $item) {
|
||||
foreach ($item['content'] as $block) {
|
||||
$data .= $this->getDataFromBlocks($block);
|
||||
};
|
||||
};
|
||||
break;
|
||||
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function postToSocialMedia($settings, $content, $title, $publish_date) : void
|
||||
{
|
||||
if ($this->record->status === PostStatus::PUBLISHED) {
|
||||
if ($settings['vk']) {
|
||||
$text = "";
|
||||
foreach ($content as $block) {
|
||||
$text .= $this->generateContentToVK($block);
|
||||
}
|
||||
|
||||
$images = $this->generateImageLinksToVK($this->record->images);
|
||||
|
||||
$post_id = $this->record->id;
|
||||
|
||||
|
||||
|
||||
dispatch(new CreateVkPost($title, $text, $images, $post_id, $publish_date));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function generateContentToVK($block) : string
|
||||
{
|
||||
$data = "";
|
||||
|
||||
switch ($block['type']) {
|
||||
case 'paragraph':
|
||||
// Удаляем все HTML-теги и заменяем закрывающие теги p и h2 на двойной отступ
|
||||
$content = preg_replace('/<\/(p|h2)>/', "\n\n", $block['data']['content']);
|
||||
$data .= html_entity_decode(strip_tags($content));
|
||||
break;
|
||||
|
||||
case 'heading':
|
||||
// Удаляем теги заголовка и добавляем двойной отступ
|
||||
$data .= $block['data']['content'] . "\n\n";
|
||||
break;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
private function generateImageLinksToVK($images)
|
||||
{
|
||||
|
||||
$imageUrls = array_map(function ($file) {
|
||||
return url(Storage::url($file)); // Добавляем домен
|
||||
}, $images);
|
||||
|
||||
|
||||
return $imageUrls; // Возвращаем массив с полными URL изображений
|
||||
(new VkPostPublisher())->publish($this->publicationAgreements, $this->record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,14 +2,17 @@
|
||||
|
||||
namespace App\Filament\Resources\PostResource\Pages;
|
||||
|
||||
use App\Dto\MainSliderDTO;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Filament\Resources\PostResource;
|
||||
use App\Jobs\UpdateVkPost;
|
||||
use App\Services\Filament\Domain\Posts\PostDataProcessor;
|
||||
use App\Services\Filament\Domain\Posts\PostNotificationService;
|
||||
use App\Services\Filament\Domain\Posts\PostSeoGenerator;
|
||||
use App\Services\Filament\Domain\Posts\PostSliderService;
|
||||
use App\Services\Filament\Domain\Posts\VkPostPublisher;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class EditPost extends EditRecord
|
||||
{
|
||||
@@ -18,224 +21,75 @@ class EditPost extends EditRecord
|
||||
protected array $seoData;
|
||||
protected array $publicationAgreements;
|
||||
|
||||
protected array $slideData;
|
||||
|
||||
|
||||
protected function mutateFormDataBeforeSave(array $data): array
|
||||
{
|
||||
$this->seoData = $this->generateSeo($data);
|
||||
$this->publicationAgreements = $data['publication'];
|
||||
unset($data['publication']);
|
||||
unset($data['publish_setting']);
|
||||
$data['preview_text'] = $this->setPreviewText($data);
|
||||
$data['publish_at'] = $this->setPublishDateTime($data['status'], $this->record->publish_at);
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
||||
$this->extractAdditionalData($data);
|
||||
return $this->processPostData($data);
|
||||
}
|
||||
|
||||
return $data;
|
||||
protected function extractAdditionalData(array &$data): void
|
||||
{
|
||||
$this->publicationAgreements = $data['publication'] ?? [];
|
||||
$this->slideData = $data['slide'] ?? [];
|
||||
unset($data['slide'], $data['publication']);
|
||||
}
|
||||
|
||||
protected function processPostData(array $data): array
|
||||
{
|
||||
return (new PostDataProcessor())->process($data);
|
||||
}
|
||||
|
||||
protected function afterSave(): void
|
||||
{
|
||||
$this->record->seo()->update($this->seoData);
|
||||
$publish_date = ($this->record->publish_at > now()) ? Carbon::parse($this->record->publish_at)->timestamp : null;
|
||||
$this->postToSocialMedia($this->publicationAgreements, $this->record->content, $this->record->title, $publish_date);
|
||||
$this->handleSlides();
|
||||
$this->generateSeo();
|
||||
$this->sendNotifications();
|
||||
$this->publishToVk();
|
||||
}
|
||||
|
||||
private function setPreviewText(array $data) : string|null
|
||||
|
||||
protected function handleSlides(): void
|
||||
{
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||
if ($rowData === null) {
|
||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
||||
}
|
||||
if ($rowData !== null) {
|
||||
$preview_text = html_entity_decode(strip_tags($rowData['data']['content']));
|
||||
return Str::limit($preview_text, 160);
|
||||
} else {
|
||||
$preview_text = null;
|
||||
return $preview_text;
|
||||
}
|
||||
if (empty($this->slideData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
private function getBlockBySeoActiveState(string $name, array $content) : array|null
|
||||
$this->slideData['is_active'] = $this->record->status === PostStatus::PUBLISHED;
|
||||
$this->slideData['start_time'] = $this->record->publish_at;
|
||||
|
||||
$sliderDTO = MainSliderDTO::fromArray($this->slideData);
|
||||
(new PostSliderService($sliderDTO, $this->record->slug))->update();
|
||||
}
|
||||
|
||||
protected function generateSeo(): void
|
||||
{
|
||||
$data = [];
|
||||
foreach ($content as $block) {
|
||||
if ($block['type'] === $name) {
|
||||
$data[] = $block;
|
||||
}
|
||||
}
|
||||
$block = null;
|
||||
foreach ($data as $item) {
|
||||
if ($item['data']['seo_active'] === true) {
|
||||
$block = $item;
|
||||
}
|
||||
}
|
||||
return $block;
|
||||
$seoData = (new PostSeoGenerator())->generate([
|
||||
'title' => $this->record->title,
|
||||
'content' => $this->record->content,
|
||||
'preview' => $this->record->preview,
|
||||
]);
|
||||
$this->record->seo()->update($seoData);
|
||||
}
|
||||
|
||||
private function generateSeo(array $data) : array
|
||||
{
|
||||
$title = $data['title'];
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||
if ($rowData === null) {
|
||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
||||
}
|
||||
if ($rowData !== null) {
|
||||
$description = html_entity_decode(strip_tags($rowData['data']['content']));
|
||||
} else {
|
||||
$description = null;
|
||||
}
|
||||
$image = ($this->record->preview !== null) ? $this->record->preview : null;
|
||||
|
||||
return [
|
||||
'title' => $title,
|
||||
'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
|
||||
'image' => $image,
|
||||
];
|
||||
}
|
||||
|
||||
private function setPublishDateTime($status, $publish_at)
|
||||
{
|
||||
if ($publish_at !== null) {
|
||||
return $publish_at;
|
||||
}
|
||||
return PostStatus::tryFrom($status) === PostStatus::PUBLISHED ? Carbon::now() : null;
|
||||
}
|
||||
private function getDataFromBlocks($block) : string
|
||||
{
|
||||
$data = "";
|
||||
switch ($block['type']) {
|
||||
case 'paragraph':
|
||||
$data .= strip_tags($block['data']['content']) . " ";
|
||||
break;
|
||||
case 'heading':
|
||||
$data .= strip_tags($block['data']['content']) . " ";
|
||||
break;
|
||||
case 'files':
|
||||
foreach ($block['data']['file'] as $file) {
|
||||
$data .= $file['title'] . " ";
|
||||
}
|
||||
break;
|
||||
case 'person':
|
||||
$data .= $block['data']['name'] . " ";
|
||||
break;
|
||||
case 'stepper':
|
||||
$data .= $block['data']['step_name'] . " ";
|
||||
foreach ($block['data']['steps'] as $step) {
|
||||
$data .= $step['title'] . " ";
|
||||
$data .= strip_tags($step['content']) . " ";
|
||||
}
|
||||
break;
|
||||
case 'tabs':
|
||||
foreach ($block['data']['tab'] as $item) {
|
||||
foreach ($item['content'] as $block) {
|
||||
$data .= $this->getDataFromBlocks($block);
|
||||
};
|
||||
};
|
||||
break;
|
||||
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function calculateReadingTime(string $text): int
|
||||
{
|
||||
|
||||
// Calculate the number of words in the text
|
||||
$wordCount = str_word_count($text,0,"АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя");
|
||||
|
||||
|
||||
|
||||
|
||||
// Calculate the average reading speed in words per minute
|
||||
$wordsPerMinute = 120; // You can adjust this value based on your desired reading speed
|
||||
|
||||
// Calculate the reading time in minutes
|
||||
$readingTime = $wordCount / $wordsPerMinute;
|
||||
|
||||
// Round the reading time to the nearest integer
|
||||
$readingTime = round($readingTime);
|
||||
|
||||
|
||||
return $readingTime;
|
||||
}
|
||||
|
||||
private function generateSearchData(array $data) : string
|
||||
{
|
||||
$result = "";
|
||||
foreach ($data as $block) {
|
||||
$result .= $this->getDataFromBlocks($block);
|
||||
}
|
||||
// Удаляем лишние пробелы и переносы строк
|
||||
$result = preg_replace('/\s+/', ' ', $result);
|
||||
$result = htmlspecialchars(trim($result));
|
||||
|
||||
return strtolower($result);
|
||||
}
|
||||
private function getFirstBlockByName(string $name, array $content) : array|null
|
||||
{
|
||||
$data = null;
|
||||
foreach ($content as $block) {
|
||||
$data = ($block['type'] === $name) ? $block : null;
|
||||
break;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function generateContentToVK($block) : string
|
||||
{
|
||||
$data = "";
|
||||
|
||||
switch ($block['type']) {
|
||||
case 'paragraph':
|
||||
// Удаляем все HTML-теги и заменяем закрывающие теги p и h2 на двойной отступ
|
||||
$content = preg_replace('/<\/(p|h2)>/', "\n\n", $block['data']['content']);
|
||||
$data .= html_entity_decode(strip_tags($content));
|
||||
break;
|
||||
|
||||
case 'heading':
|
||||
// Удаляем теги заголовка и добавляем двойной отступ
|
||||
$data .= $block['data']['content'] . "\n\n";
|
||||
break;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function postToSocialMedia($settings, $content, $title, $publish_date) : void
|
||||
protected function sendNotifications(): void
|
||||
{
|
||||
// Отправляем уведомления
|
||||
$notificationService = new PostNotificationService();
|
||||
if ($this->record->status === PostStatus::PUBLISHED) {
|
||||
if ($settings['vk']) {
|
||||
$text = "";
|
||||
foreach ($content as $block) {
|
||||
$text .= $this->generateContentToVK($block);
|
||||
}
|
||||
|
||||
$images = $this->generateImageLinksToVK($this->record->images);
|
||||
|
||||
$post_id = $this->record->id;
|
||||
|
||||
|
||||
dispatch(new UpdateVkPost($title, $text, $images, $post_id, $publish_date));
|
||||
}
|
||||
$notificationService->sendSuccessNotification($this->record);
|
||||
} elseif ($this->record->status === PostStatus::REJECTED) {
|
||||
$notificationService->sendDeniedNotification($this->record);
|
||||
}
|
||||
}
|
||||
|
||||
private function generateImageLinksToVK($images)
|
||||
protected function publishToVk(): void
|
||||
{
|
||||
|
||||
$imageUrls = array_map(function ($file) {
|
||||
return url(Storage::url($file)); // Добавляем домен
|
||||
}, $images);
|
||||
|
||||
|
||||
return $imageUrls; // Возвращаем массив с полными URL изображений
|
||||
(new VkPostPublisher())->publish($this->publicationAgreements, $this->record);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -50,7 +50,6 @@ class UserResource extends Resource implements HasShieldPermissions
|
||||
Forms\Components\TextInput::make('password')
|
||||
->label('Пароль')
|
||||
->password()
|
||||
->default(Str::password(15))
|
||||
->required(fn (string $context): bool => $context === 'create')
|
||||
->dehydrated(fn ($state) => filled($state))
|
||||
->maxLength(255),
|
||||
|
||||
@@ -22,11 +22,10 @@ class ClientAcademicJournalController extends Controller
|
||||
public function show(string $slug)
|
||||
{
|
||||
$journal = new ClientAcademicJournalListResource(AcademicJournal::query()->where('slug', '=', $slug)->firstOrFail());
|
||||
$journalIssues = JournalIssue::all()
|
||||
->groupBy('year_publication');
|
||||
$journalIssues = JournalIssue::where('academic_journal_id', $journal->id)
|
||||
->groupBy('year_publication')->get();
|
||||
|
||||
$journals = [];
|
||||
$years = JournalIssue::select('year_publication')->distinct()->get();
|
||||
|
||||
foreach ($journalIssues as $year => $journalGroup) {
|
||||
$journals[] = [
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\FormEducation;
|
||||
use App\Http\Resources\ClientEducationalGroupResource;
|
||||
use App\Http\Resources\ScheduleResource;
|
||||
use App\Models\EducationalGroup;
|
||||
@@ -14,22 +15,71 @@ class ClientScheduleController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
$educationalGroups = collect();
|
||||
|
||||
if (request()->filled('search')) {
|
||||
$educationalGroups = ClientEducationalGroupResource::collection(EducationalGroup::query()
|
||||
->has('schedules')
|
||||
->when(request()->input('search'), function ($query, $search) {
|
||||
$query->whereRaw('LOWER(title) like ?', ["%".strtolower($search)."%"]);
|
||||
})
|
||||
->when(request()->input('favorite'), function ($query, $favorite) {
|
||||
$query->whereHas('schedules', function ($query) use ($favorite) {
|
||||
$query->whereIn('id', $favorite);
|
||||
});
|
||||
})
|
||||
->when(request()->input('form'), function ($query, $form) {
|
||||
$query->where('education_form_id', FormEducation::fromName($form)->value);
|
||||
|
||||
})
|
||||
|
||||
->with('schedules')
|
||||
->with('faculty')
|
||||
->orderBy('title')
|
||||
->get());
|
||||
}
|
||||
$searchRequest = request()->input('search');
|
||||
|
||||
return Inertia::render('Client/Schedules/Index', compact('educationalGroups', 'searchRequest'));
|
||||
$schedulesByFaculty = $educationalGroups->groupBy(function ($group) {
|
||||
return $group->faculty->title; // Предполагаем, что у факультета есть поле 'name'
|
||||
});
|
||||
|
||||
$schedulesByFaculty = $schedulesByFaculty->toArray();
|
||||
|
||||
if ($request->has('favorite') && empty($request->input('favorite'))) {
|
||||
$schedulesByFaculty = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
$forms_education = [];
|
||||
foreach (FormEducation::cases() as $case) {
|
||||
$forms_education[$case->name] = $case->getLabel();
|
||||
}
|
||||
|
||||
$filters = [
|
||||
'direction_filter' => [
|
||||
'type' => 'direction',
|
||||
'value' => request()->input('direction'),
|
||||
'param' => 'direction'
|
||||
],
|
||||
'form_education_filter' => [
|
||||
'type' => 'form',
|
||||
'value' => request()->input('form'),
|
||||
'param' => 'form'
|
||||
],
|
||||
'search_filter' => [
|
||||
'type' => 'search',
|
||||
'value' => $request->input('search'),
|
||||
'param' => 'search'
|
||||
],
|
||||
'favorite_filter' => [
|
||||
'type' => 'favorite',
|
||||
'value' => $request->input('favorite'),
|
||||
'param' => 'favorite'
|
||||
]
|
||||
];
|
||||
|
||||
// Возвращаем данные в представление
|
||||
return Inertia::render('Client/Schedules/Index', compact('educationalGroups', 'filters', 'forms_education', 'schedulesByFaculty'));
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
|
||||
@@ -27,6 +27,7 @@ use App\Services\Vicon\EducationalProgram\EducationalProgramService;
|
||||
use Carbon\Carbon;
|
||||
use DateTime;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
@@ -37,14 +38,32 @@ class MainController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$admissionCampaign = $this->getAdmissionCampaign();
|
||||
$educations = $this->getEducationsData();
|
||||
$sliders = $this->getActiveSliders();
|
||||
$posts = $this->getRecentPosts();
|
||||
$events = $this->getUpcomingEvents();
|
||||
// Кешируем данные на 60 минут (можно изменить время по необходимости)
|
||||
// $admissionCampaign = Cache::remember('admission_campaign', now()->addHour(), function () {
|
||||
// return $this->getAdmissionCampaign();
|
||||
// });
|
||||
|
||||
$educations = Cache::remember('educations_data', now()->addHour(), function () {
|
||||
return $this->getEducationsData();
|
||||
});
|
||||
|
||||
$sliders = Cache::remember('active_sliders', now()->addHour(), function () {
|
||||
return $this->getActiveSliders();
|
||||
});
|
||||
|
||||
$posts = Cache::remember('recent_posts', now()->addHour(), function () {
|
||||
return $this->getRecentPosts();
|
||||
});
|
||||
|
||||
$events = Cache::remember('upcoming_events', now()->addHour(), function () {
|
||||
return $this->getUpcomingEvents();
|
||||
});
|
||||
|
||||
$path = route('index', null, false);
|
||||
$page = Page::where('path', $path)->first();
|
||||
$page = Cache::remember('page_' . $path, now()->addHour(), function () use ($path) {
|
||||
return Page::where('path', $path)->first();
|
||||
});
|
||||
|
||||
$seo = $page->seo ?? null;
|
||||
|
||||
return Inertia::render('Main', compact('posts', 'events', 'sliders', 'educations', 'seo'));
|
||||
@@ -82,11 +101,10 @@ class MainController extends Controller
|
||||
|
||||
private function getActiveSliders()
|
||||
{
|
||||
return ClientMainSliderResource::collection(
|
||||
return (ClientMainSliderResource::collection(
|
||||
MainSlider::where('is_active', true)
|
||||
->orderBy('sort', 'asc')
|
||||
->get()
|
||||
);
|
||||
->get()));
|
||||
}
|
||||
|
||||
private function getRecentPosts()
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http;
|
||||
|
||||
use App\Http\Middleware\AccessCheck;
|
||||
use App\Http\Middleware\InternalRequestOnly;
|
||||
use App\Http\Middleware\LimitPost;
|
||||
use App\Http\Middleware\RateLimitCheckMiddleware;
|
||||
use App\Http\Middleware\RateLimitCounterMiddleware;
|
||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||
@@ -84,6 +85,7 @@ class Kernel extends HttpKernel
|
||||
'rate.limited.check' => RateLimitCheckMiddleware::class,
|
||||
'ensure.browser' => InternalRequestOnly::class,
|
||||
'superadmin' => \App\Http\Middleware\EnsureUserIsSuperadmin::class,
|
||||
'limit.post' => LimitPost::class,
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -12,6 +13,6 @@ class Authenticate extends Middleware
|
||||
*/
|
||||
protected function redirectTo(Request $request): ?string
|
||||
{
|
||||
return $request->expectsJson() ? null : route('login');
|
||||
return $request->expectsJson() ? null : throw new AuthorizationException('Forbidden', 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class LimitPost
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (auth()->user()->receivedInvitation === null) {
|
||||
return $next($request);
|
||||
}
|
||||
if (auth()->user()->receivedInvitation->post_limit > 0) {
|
||||
return $next($request);
|
||||
} else {
|
||||
throw new HttpException(403, 'Лимит постов исчерпан');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,4 +10,10 @@ class MainSlider extends Model
|
||||
use HasFactory;
|
||||
|
||||
protected $guarded = false;
|
||||
|
||||
|
||||
protected $casts = [
|
||||
'settings' => 'array',
|
||||
'image' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -34,6 +34,11 @@ class Post extends Model
|
||||
return $this->belongsTo(Category::class);
|
||||
}
|
||||
|
||||
public function author() : BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
|
||||
public function seo()
|
||||
{
|
||||
|
||||
@@ -94,6 +94,7 @@ class User extends Authenticatable implements FilamentUser
|
||||
{
|
||||
if (config('filament-shield.dashboard_user.enabled', false)) {
|
||||
FilamentShield::createRole(name: config('filament-shield.dashboard_user.name', 'dashboard_user'));
|
||||
FilamentShield::createRole(name: config('', 'editor'));
|
||||
static::created(function (User $user) {
|
||||
$user->assignRole(config('filament-shield.dashboard_user.name', 'dashboard_user'));
|
||||
});
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Models\MainSlider;
|
||||
use App\Services\App\Cache\MainSliderCacheService;
|
||||
|
||||
class MainSliderObserver
|
||||
{
|
||||
|
||||
protected MainSliderCacheService $cacheService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->cacheService = new MainSliderCacheService();
|
||||
}
|
||||
/**
|
||||
* Handle the MainSlider "created" event.
|
||||
*/
|
||||
public function created(MainSlider $mainSlider): void
|
||||
{
|
||||
// Устанавливаем сортировку для новой записи
|
||||
$mainSlider->sort = 1;
|
||||
$mainSlider->save();
|
||||
|
||||
// Обновляем сортировку для всех остальных записей
|
||||
$this->updateSortOrder();
|
||||
|
||||
$this->cacheService->clearAllCacheByModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the MainSlider "updated" event.
|
||||
*/
|
||||
public function updated(MainSlider $mainSlider): void
|
||||
{
|
||||
$this->cacheService->clearAllCacheByModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the MainSlider "deleted" event.
|
||||
*/
|
||||
public function deleted(MainSlider $mainSlider): void
|
||||
{
|
||||
$this->cacheService->clearAllCacheByModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the MainSlider "restored" event.
|
||||
*/
|
||||
public function restored(MainSlider $mainSlider): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the MainSlider "force deleted" event.
|
||||
*/
|
||||
public function forceDeleted(MainSlider $mainSlider): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
protected function updateSortOrder(): void
|
||||
{
|
||||
// Получаем все записи, отсортированные по текущему значению sort
|
||||
$slides = MainSlider::orderBy('sort', 'asc')->get();
|
||||
|
||||
// Обновляем сортировку для каждой записи
|
||||
foreach ($slides as $index => $slide) {
|
||||
$slide->sort = $index + 1; // Начинаем с 1
|
||||
$slide->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class PostObserver
|
||||
*/
|
||||
public function updated(Post $post)
|
||||
{
|
||||
$this->postCacheService->clearCache($post);
|
||||
$this->postCacheService->clearAllCacheByModel();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\ContactWidget;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class ContactWidgetPolicy
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*/
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return $user->can('view_any_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*/
|
||||
public function view(User $user, ContactWidget $contactWidget): bool
|
||||
{
|
||||
return $user->can('view_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*/
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return $user->can('create_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*/
|
||||
public function update(User $user, ContactWidget $contactWidget): bool
|
||||
{
|
||||
return $user->can('update_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*/
|
||||
public function delete(User $user, ContactWidget $contactWidget): bool
|
||||
{
|
||||
return $user->can('delete_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can bulk delete.
|
||||
*/
|
||||
public function deleteAny(User $user): bool
|
||||
{
|
||||
return $user->can('delete_any_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete.
|
||||
*/
|
||||
public function forceDelete(User $user, ContactWidget $contactWidget): bool
|
||||
{
|
||||
return $user->can('force_delete_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently bulk delete.
|
||||
*/
|
||||
public function forceDeleteAny(User $user): bool
|
||||
{
|
||||
return $user->can('force_delete_any_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore.
|
||||
*/
|
||||
public function restore(User $user, ContactWidget $contactWidget): bool
|
||||
{
|
||||
return $user->can('restore_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can bulk restore.
|
||||
*/
|
||||
public function restoreAny(User $user): bool
|
||||
{
|
||||
return $user->can('restore_any_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can replicate.
|
||||
*/
|
||||
public function replicate(User $user, ContactWidget $contactWidget): bool
|
||||
{
|
||||
return $user->can('replicate_contact::widget');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can reorder.
|
||||
*/
|
||||
public function reorder(User $user): bool
|
||||
{
|
||||
return $user->can('reorder_contact::widget');
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,20 @@
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\MainSection;
|
||||
use App\Models\MainSlider;
|
||||
use App\Models\Page;
|
||||
use App\Models\Post;
|
||||
use App\Models\SubSection;
|
||||
use App\Observers\MainSectionObserver;
|
||||
use App\Observers\MainSliderObserver;
|
||||
use App\Observers\PageObserver;
|
||||
use App\Observers\PostObserver;
|
||||
use App\Observers\SubSectionObserver;
|
||||
use App\Services\App\Cache\MainSliderCacheService;
|
||||
use Carbon\Carbon;
|
||||
use Filament\Facades\Filament;
|
||||
use Filament\Support\Facades\FilamentView;
|
||||
use Filament\Tables\View\TablesRenderHook;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\Vite;
|
||||
@@ -34,16 +39,29 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
setlocale(LC_TIME, 'ru_RU.UTF-8');
|
||||
Page::observe(PageObserver::class);
|
||||
Post::observe(PostObserver::class);
|
||||
MainSection::observe(MainSectionObserver::class);
|
||||
SubSection::observe(SubSectionObserver::class);
|
||||
Carbon::setLocale(config('app.locale'));
|
||||
self::setObserversByModel();
|
||||
self::setLocaleTime();
|
||||
Model::preventLazyLoading(!app()->isProduction());
|
||||
// URL::forceScheme('https');
|
||||
self::registerFilamentNavigationGroups();
|
||||
$this->loadViewsFrom(__DIR__.'/path/to/views', 'checkpoint');
|
||||
|
||||
FilamentView::registerRenderHook(TablesRenderHook::TOOLBAR_REORDER_TRIGGER_AFTER, function () {
|
||||
(new MainSliderCacheService())->clearAllCacheByModel();
|
||||
});
|
||||
}
|
||||
|
||||
private static function setObserversByModel() : void {
|
||||
Page::observe(PageObserver::class);
|
||||
Post::observe(PostObserver::class);
|
||||
MainSection::observe(MainSectionObserver::class);
|
||||
SubSection::observe(SubSectionObserver::class);
|
||||
MainSlider::observe(MainSliderObserver::class);
|
||||
}
|
||||
|
||||
private static function setLocaleTime() : void {
|
||||
setlocale(LC_TIME, 'ru_RU.UTF-8');
|
||||
Carbon::setLocale(config('app.locale'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class AdminPanelProvider extends PanelProvider
|
||||
->default()
|
||||
->id('admin')
|
||||
->path('admin')
|
||||
->registration()
|
||||
// ->registration()
|
||||
->login()
|
||||
->databaseNotifications()
|
||||
->databaseNotificationsPolling('5s')
|
||||
|
||||
@@ -29,6 +29,8 @@ class DashboardPanelProvider extends PanelProvider
|
||||
'primary' => Color::Amber,
|
||||
])
|
||||
->login()
|
||||
->databaseNotifications()
|
||||
->databaseNotificationsPolling('5s')
|
||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||
->discoverPages(in: app_path('Filament/Dashboard/Pages'), for: 'App\\Filament\\Dashboard\\Pages')
|
||||
->pages([
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\App\Cache;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class MainSliderCacheService extends AbstractCacheService implements CacheInterface
|
||||
{
|
||||
/**
|
||||
* Очищает кеш, связанный с постом.
|
||||
*
|
||||
* @param mixed $entity Пост или связанная сущность
|
||||
* @return void
|
||||
*/
|
||||
public function clearCache($entity): void
|
||||
{
|
||||
}
|
||||
|
||||
public function clearAllCacheByModel(): void
|
||||
{
|
||||
$this->clearCacheByPrefix('active_sliders*');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Получает кешированные данные по ключу.
|
||||
*
|
||||
* @param string $key Ключ кеша
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCachedData(string $key)
|
||||
{
|
||||
return Cache::get($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Кеширует данные по ключу.
|
||||
*
|
||||
* @param string $key Ключ кеша
|
||||
* @param mixed $data Данные для кеширования
|
||||
* @param int $ttl Время жизни кеша в секундах
|
||||
* @return void
|
||||
*/
|
||||
public function cacheData(string $key, $data, int $ttl = 3600): void
|
||||
{
|
||||
Cache::put($key, $data, $ttl);
|
||||
}
|
||||
}
|
||||
@@ -18,14 +18,18 @@ class PostCacheService extends AbstractCacheService implements CacheInterface
|
||||
$cacheKeyBySlug = md5($entity->slug);
|
||||
$cacheKeyById = md5($entity->id);
|
||||
|
||||
Cache::forget($cacheKeyBySlug);
|
||||
Cache::forget($cacheKeyById);
|
||||
Cache::forget('post_' .$cacheKeyBySlug);
|
||||
Cache::forget('post_' .$cacheKeyById);
|
||||
|
||||
$this->clearCacheByPrefix('recent_posts*');
|
||||
|
||||
}
|
||||
|
||||
public function clearAllCacheByModel(): void
|
||||
{
|
||||
$this->clearCacheByPrefix('post_*');
|
||||
$this->clearCacheByPrefix('posts_*');
|
||||
$this->clearCacheByPrefix('recent_posts*');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Filament\Domain\Posts;
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PostDataProcessor
|
||||
{
|
||||
/**
|
||||
* Обрабатывает данные перед созданием поста.
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function process(array $data): array
|
||||
{
|
||||
// Удаляем ненужные данные
|
||||
unset($data['publication']);
|
||||
|
||||
// Устанавливаем текст для предпросмотра
|
||||
$data['preview_text'] = $this->setPreviewText($data);
|
||||
|
||||
// Устанавливаем время публикации
|
||||
$data['publish_at'] = $this->setPublishDateTime($data['publish_setting'], $data['status']);
|
||||
unset($data['publish_setting']);
|
||||
|
||||
// Генерируем данные для поиска
|
||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
||||
|
||||
// Рассчитываем время чтения
|
||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
||||
|
||||
// Устанавливаем ID текущего пользователя
|
||||
$data['user_id'] = auth()->id();
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает текст для предпросмотра.
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
private function setPreviewText(array $data): string
|
||||
{
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||
if ($rowData === null) {
|
||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
||||
}
|
||||
|
||||
$previewText = $rowData ? html_entity_decode(strip_tags($rowData['data']['content'])) : '';
|
||||
return Str::limit($previewText, 160);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает время публикации.
|
||||
*
|
||||
* @param array $publishSetting
|
||||
* @param string $status
|
||||
* @return Carbon|null
|
||||
*/
|
||||
private function setPublishDateTime(array $publishSetting, string|PostStatus $status): ?Carbon
|
||||
{
|
||||
if ($publishSetting['publish_after'] === true) {
|
||||
return Carbon::parse($publishSetting['publish_at']);
|
||||
}
|
||||
|
||||
if ($status === PostStatus::PUBLISHED) {
|
||||
return Carbon::now();
|
||||
}
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует данные для поиска.
|
||||
*
|
||||
* @param array $content
|
||||
* @return string
|
||||
*/
|
||||
private function generateSearchData(array $content): string
|
||||
{
|
||||
$result = "";
|
||||
foreach ($content as $block) {
|
||||
$result .= $this->getDataFromBlocks($block);
|
||||
}
|
||||
|
||||
// Удаляем лишние пробелы и переносы строк
|
||||
$result = preg_replace('/\s+/', ' ', $result);
|
||||
$result = trim($result);
|
||||
|
||||
return strtolower($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает данные из блоков контента.
|
||||
*
|
||||
* @param array $block
|
||||
* @return string
|
||||
*/
|
||||
private function getDataFromBlocks(array $block): string
|
||||
{
|
||||
$data = "";
|
||||
switch ($block['type']) {
|
||||
case 'paragraph':
|
||||
$data .= strip_tags($block['data']['content']) . " ";
|
||||
break;
|
||||
case 'heading':
|
||||
$data .= strip_tags($block['data']['content']) . " ";
|
||||
break;
|
||||
case 'files':
|
||||
foreach ($block['data']['file'] as $file) {
|
||||
$data .= $file['title'] . " ";
|
||||
}
|
||||
break;
|
||||
case 'person':
|
||||
$data .= $block['data']['name'] . " ";
|
||||
break;
|
||||
case 'stepper':
|
||||
$data .= $block['data']['step_name'] . " ";
|
||||
foreach ($block['data']['steps'] as $step) {
|
||||
$data .= $step['title'] . " ";
|
||||
$data .= strip_tags($step['content']) . " ";
|
||||
}
|
||||
break;
|
||||
case 'tabs':
|
||||
foreach ($block['data']['tab'] as $item) {
|
||||
foreach ($item['content'] as $block) {
|
||||
$data .= $this->getDataFromBlocks($block);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает время чтения.
|
||||
*
|
||||
* @param string $text
|
||||
* @return int
|
||||
*/
|
||||
private function calculateReadingTime(string $text): int
|
||||
{
|
||||
$wordCount = str_word_count($text, 0, "АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя");
|
||||
$wordsPerMinute = 120; // Средняя скорость чтения
|
||||
return max(1, round($wordCount / $wordsPerMinute));
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит первый блок по имени.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $content
|
||||
* @return array|null
|
||||
*/
|
||||
private function getFirstBlockByName(string $name, array $content): ?array
|
||||
{
|
||||
foreach ($content as $block) {
|
||||
if ($block['type'] === $name) {
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит блок по SEO-активности.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $content
|
||||
* @return array|null
|
||||
*/
|
||||
private function getBlockBySeoActiveState(string $name, array $content): ?array
|
||||
{
|
||||
foreach ($content as $block) {
|
||||
if ($block['type'] === $name && ($block['data']['seo_active'] ?? false)) {
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Filament\Domain\Posts;
|
||||
|
||||
use App\Filament\Resources\PostResource;
|
||||
use App\Models\Post;
|
||||
use App\Models\Role;
|
||||
use App\Models\User;
|
||||
use Filament\Notifications\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
|
||||
class PostNotificationService
|
||||
{
|
||||
/**
|
||||
* Отправляет уведомления редакторам о новом посте.
|
||||
*
|
||||
* @param Post $post
|
||||
* @return void
|
||||
*/
|
||||
public function send(Post $post): void
|
||||
{
|
||||
// Находим всех пользователей с ролью "editor"
|
||||
$editors = Role::findByName('editor')->users;
|
||||
|
||||
// Отправляем уведомление каждому редактору
|
||||
foreach ($editors as $editor) {
|
||||
Notification::make()
|
||||
->title('Новость на проверку')
|
||||
->body('Новость "' . $post->title . '" нуждается в проверке')
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->label('Проверить')
|
||||
->button()
|
||||
->markAsRead()
|
||||
->url(PostResource::getUrl('edit', ['record' => $post])),
|
||||
])
|
||||
->sendToDatabase($editor); // Отправляем уведомление конкретному редактору
|
||||
}
|
||||
}
|
||||
|
||||
public function sendSuccessNotification(Post $post): void
|
||||
{
|
||||
$user = User::find($post->user_id);
|
||||
|
||||
Notification::make()
|
||||
->title('Ваша новость опубликована')
|
||||
->body('Новость "' . $post->title . '" опубликована')
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->label('Смотреть')
|
||||
->button()
|
||||
->markAsRead()
|
||||
->url(route('client.post.show', $post->slug)),
|
||||
])
|
||||
->sendToDatabase($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет уведомление об отклонении.
|
||||
*
|
||||
* @param Post $post
|
||||
* @return void
|
||||
*/
|
||||
public function sendDeniedNotification(Post $post): void
|
||||
{
|
||||
$user = User::find($post->user_id);
|
||||
|
||||
Notification::make()
|
||||
->title('Ваша новость отклонена :(')
|
||||
->body('Новость "' . $post->title . '" была отклонена')
|
||||
->actions([
|
||||
Action::make('view')
|
||||
->label('Смотреть')
|
||||
->button()
|
||||
->markAsRead()
|
||||
->url(PostResource::getUrl('edit', ['record' => $post])),
|
||||
])
|
||||
->sendToDatabase($user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Filament\Domain\Posts;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class PostSeoGenerator
|
||||
{
|
||||
/**
|
||||
* Генерирует SEO-данные для поста.
|
||||
*
|
||||
* @param array $data
|
||||
* @return array
|
||||
*/
|
||||
public function generate(array $data): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->extractSeoTitle($data),
|
||||
'description' => $this->extractSeoDescription($data['content']),
|
||||
'image' => $this->extractSeoImage($data),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает SEO-заголовок.
|
||||
*
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
private function extractSeoTitle(array $data): string
|
||||
{
|
||||
return $data['title'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает SEO-описание.
|
||||
*
|
||||
* @param array $content
|
||||
* @return string
|
||||
*/
|
||||
private function extractSeoDescription(array $content): string
|
||||
{
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $content);
|
||||
if ($rowData === null) {
|
||||
$rowData = $this->getFirstBlockByName('paragraph', $content);
|
||||
}
|
||||
|
||||
$description = $rowData ? html_entity_decode(strip_tags($rowData['data']['content'])) : '';
|
||||
return Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160);
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлекает SEO-изображение.
|
||||
*
|
||||
* @param array $data
|
||||
* @return string|null
|
||||
*/
|
||||
private function extractSeoImage(array $data): ?string
|
||||
{
|
||||
return $data['preview'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит первый блок по имени.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $content
|
||||
* @return array|null
|
||||
*/
|
||||
private function getFirstBlockByName(string $name, array $content): ?array
|
||||
{
|
||||
foreach ($content as $block) {
|
||||
if ($block['type'] === $name) {
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит блок по SEO-активности.
|
||||
*
|
||||
* @param string $name
|
||||
* @param array $content
|
||||
* @return array|null
|
||||
*/
|
||||
private function getBlockBySeoActiveState(string $name, array $content): ?array
|
||||
{
|
||||
foreach ($content as $block) {
|
||||
if ($block['type'] === $name && ($block['data']['seo_active'] ?? false)) {
|
||||
return $block;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function setPreviewText(array $data): ?string
|
||||
{
|
||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
||||
if ($rowData === null) {
|
||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
||||
}
|
||||
|
||||
if ($rowData !== null) {
|
||||
$previewText = html_entity_decode(strip_tags($rowData['data']['content']));
|
||||
return Str::limit($previewText, 160);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Filament\Domain\Posts;
|
||||
|
||||
use App\Dto\MainSliderDTO;
|
||||
use App\Models\MainSlider;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PostSliderService
|
||||
{
|
||||
public function __construct(
|
||||
readonly private MainSliderDTO $dto,
|
||||
readonly private string $postSlug,
|
||||
){}
|
||||
|
||||
public function create(): void
|
||||
{
|
||||
try {
|
||||
MainSlider::create([
|
||||
'title' => $this->dto->title,
|
||||
'content' => $this->dto->content,
|
||||
'image' => $this->dto->image,
|
||||
'link' => parse_url(route('client.post.show', $this->postSlug), PHP_URL_PATH),
|
||||
'link_text' => $this->dto->link_text,
|
||||
'is_active' => $this->dto->is_active,
|
||||
'color_theme' => $this->dto->color_theme,
|
||||
'start_time' => $this->dto->start_time,
|
||||
'end_time' => $this->dto->end_time,
|
||||
]);
|
||||
|
||||
Log::info('MainSlider created successfully', ['postSlug' => $this->postSlug]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to create MainSlider', ['error' => $e->getMessage()]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function update(): void
|
||||
{
|
||||
try {
|
||||
MainSlider::update([
|
||||
'title' => $this->dto->title,
|
||||
'content' => $this->dto->content,
|
||||
'image' => $this->dto->image,
|
||||
'link' => parse_url(route('client.post.show', $this->postSlug), PHP_URL_PATH),
|
||||
'link_text' => $this->dto->link_text,
|
||||
'is_active' => $this->dto->is_active,
|
||||
'color_theme' => $this->dto->color_theme,
|
||||
'start_time' => $this->dto->start_time,
|
||||
'end_time' => $this->dto->end_time,
|
||||
]);
|
||||
|
||||
Log::info('MainSlider updated successfully', ['postSlug' => $this->postSlug]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to update MainSlider', ['error' => $e->getMessage()]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Filament\Domain\Posts;
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use App\Jobs\CreateVkPost;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class VkPostPublisher
|
||||
{
|
||||
/**
|
||||
* Публикует пост в социальных сетях.
|
||||
*
|
||||
* @param array $settings
|
||||
* @param Post $post
|
||||
* @return void
|
||||
*/
|
||||
public function publish(array $settings, Post $post): void
|
||||
{
|
||||
if ($post->status === PostStatus::PUBLISHED) {
|
||||
if ($settings['vk']) {
|
||||
$text = $this->generateContentForVk($post->content);
|
||||
$images = $this->generateImageLinksForVk($post->images);
|
||||
$publishDate = $post->publish_at > now() ? $post->publish_at->timestamp : null;
|
||||
|
||||
dispatch(new CreateVkPost($post->title, $text, $images, $post->id, $publishDate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует текстовый контент для ВКонтакте.
|
||||
*
|
||||
* @param array $content
|
||||
* @return string
|
||||
*/
|
||||
private function generateContentForVk(array $content): string
|
||||
{
|
||||
$text = '';
|
||||
foreach ($content as $block) {
|
||||
switch ($block['type']) {
|
||||
case 'paragraph':
|
||||
$text .= strip_tags($block['data']['content']) . "\n\n";
|
||||
break;
|
||||
case 'heading':
|
||||
$text .= strip_tags($block['data']['content']) . "\n\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
return trim($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует ссылки на изображения для ВКонтакте.
|
||||
*
|
||||
* @param array $images
|
||||
* @return array
|
||||
*/
|
||||
private function generateImageLinksForVk(array $images): array
|
||||
{
|
||||
return array_map(function ($file) {
|
||||
return Storage::url($file); // Генерируем полный URL для изображения
|
||||
}, $images);
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -31,7 +31,8 @@
|
||||
"symfony/filesystem": "^6.3",
|
||||
"tightenco/ziggy": "^1.0",
|
||||
"vkcom/vk-php-sdk": "^5.131",
|
||||
"xvladqt/faker-lorem-flickr": "^1.0"
|
||||
"xvladqt/faker-lorem-flickr": "^1.0",
|
||||
"yepsua/filament-range-field": "^0.3.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.9.1",
|
||||
|
||||
Generated
+70
-1
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "25ebd7fca281d22d4022ea15464d54f2",
|
||||
"content-hash": "0e0616783641aeb05f872887cfa59e66",
|
||||
"packages": [
|
||||
{
|
||||
"name": "anourvalar/eloquent-serialize",
|
||||
@@ -10915,6 +10915,75 @@
|
||||
"source": "https://github.com/xvladxtremal/Faker-LoremFlickr/tree/v1.0.0"
|
||||
},
|
||||
"time": "2021-01-18T02:03:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "yepsua/filament-range-field",
|
||||
"version": "v0.3.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/yepsua/filament-range-field.git",
|
||||
"reference": "fc29d84819960b3ad12354c5473649d7989f1883"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/yepsua/filament-range-field/zipball/fc29d84819960b3ad12354c5473649d7989f1883",
|
||||
"reference": "fc29d84819960b3ad12354c5473649d7989f1883",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"filament/filament": "^3.0",
|
||||
"illuminate/contracts": "^8.0|^9.0|^10.0",
|
||||
"php": "^8.0",
|
||||
"spatie/laravel-package-tools": "^1.9.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"nunomaduro/collision": "^6.0",
|
||||
"nunomaduro/larastan": "^2.0.1",
|
||||
"orchestra/testbench": "^7.0",
|
||||
"pestphp/pest": "^1.21",
|
||||
"pestphp/pest-plugin-laravel": "^1.1",
|
||||
"phpstan/extension-installer": "^1.1",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"spatie/laravel-ray": "^1.26"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Yepsua\\Filament\\FilamentRangeFieldServiceProvider"
|
||||
]
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Yepsua\\Filament\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Omar Yepez",
|
||||
"email": "oyepez003@gmail.com",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "The missing range/slider field for the Filament forms.",
|
||||
"homepage": "https://github.com/yepsua/filament-range-field",
|
||||
"keywords": [
|
||||
"filament-range-field",
|
||||
"laravel",
|
||||
"yepsua"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/yepsua/filament-range-field/issues",
|
||||
"source": "https://github.com/yepsua/filament-range-field/tree/v0.3.4"
|
||||
},
|
||||
"time": "2023-12-29T06:06:23+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
||||
@@ -28,13 +28,17 @@ return [
|
||||
'plugins' => 'advlist paste autoresize codesample directionality emoticons fullscreen hr image imagetools link lists media table toc wordcount',
|
||||
'toolbar' => ' bold italic | numlist bullist | blockquote table hr | link | fullscreen',
|
||||
'upload_directory' => null,
|
||||
'browser_spellcheck' => true,
|
||||
'custom_configs' => [
|
||||
'table_advtab' => false,
|
||||
'table_row_advtab' => false,
|
||||
'table_cell_advtab' => false,
|
||||
'contextmenu' => '',
|
||||
'table_resize_bars' => false,
|
||||
'paste_as_text' => true
|
||||
'paste_as_text' => true,
|
||||
'browser_spellcheck' => true, // Включение проверки орфографии
|
||||
|
||||
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up() : void
|
||||
{
|
||||
Schema::table('educational_groups', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('education_form_id')->nullable()->after('faculty_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down() : void
|
||||
{
|
||||
Schema::table('educational_groups', function (Blueprint $table) {
|
||||
$table->dropColumn('education_form_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up() : void
|
||||
{
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->dropColumn('is_zaoch');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down() : void
|
||||
{
|
||||
Schema::table('schedules', function (Blueprint $table) {
|
||||
$table->boolean('is_zaoch')->default(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up() : void
|
||||
{
|
||||
Schema::table('main_sliders', function (Blueprint $table) {
|
||||
$table->timestamp('start_time')->nullable()->after('is_active'); // Дата и время начала действия слайда
|
||||
$table->timestamp('end_time')->nullable()->after('start_time'); // Дата и время окончания действия слайда
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down() : void
|
||||
{
|
||||
Schema::table('main_sliders', function (Blueprint $table) {
|
||||
$table->dropColumn(['start_time', 'end_time']); // Удаление колонок при откате миграции
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up() : void
|
||||
{
|
||||
Schema::table('main_sliders', function (Blueprint $table) {
|
||||
$table->string('title')->nullable()->change();
|
||||
$table->text('content')->nullable()->change();
|
||||
$table->dropColumn('link_text');
|
||||
$table->text('settings')->nullable()->after('link');
|
||||
$table->text('image')->nullable()->change();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down() : void
|
||||
{
|
||||
Schema::table('main_sliders', function (Blueprint $table) {
|
||||
$table->string('title')->nullable(false)->change();
|
||||
$table->text('content')->nullable(false)->change();
|
||||
$table->string('link_text')->nullable(false);
|
||||
$table->dropColumn('settings');
|
||||
$table->string('image')->nullable()->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
Generated
+9
@@ -22,6 +22,7 @@
|
||||
"flowbite": "^2.5.2",
|
||||
"fslightbox": "^3.4.1",
|
||||
"fslightbox-vue": "^2.1.3",
|
||||
"js-cookie": "^3.0.5",
|
||||
"preline": "^1.9.0",
|
||||
"slugify": "^1.6.6",
|
||||
"vue3-yandex-smartcaptcha": "^1.0.0"
|
||||
@@ -2022,6 +2023,14 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/laravel-vite-plugin": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.8.1.tgz",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"flowbite": "^2.5.2",
|
||||
"fslightbox": "^3.4.1",
|
||||
"fslightbox-vue": "^2.1.3",
|
||||
"js-cookie": "^3.0.5",
|
||||
"preline": "^1.9.0",
|
||||
"slugify": "^1.6.6",
|
||||
"vue3-yandex-smartcaptcha": "^1.0.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="w-full h-[67px] fixed pointer-events-none" id="visor"></div>
|
||||
<div class="w-full h-[67px] fixed pointer-events-none bvi-no-styles" id="visor"></div>
|
||||
|
||||
<nav class="order-last hidden w-56 shrink-0 lg:block">
|
||||
<div v-if="headerNavs.length > 0" class="sticky top-[100px] h-[calc(100vh-121px)]">
|
||||
|
||||
@@ -45,8 +45,18 @@ export default {
|
||||
methods: {
|
||||
filter: debounce(function () {
|
||||
let url = new URL(window.location.href);
|
||||
// Создаем массив для хранения всех ключей, которые нужно удалить
|
||||
const keysToDelete = [];
|
||||
|
||||
// Перебираем все параметры и добавляем ключи, начинающиеся с 'category', в массив
|
||||
for (const [key] of url.searchParams) {
|
||||
if (key.startsWith('direction')) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
// Удаляем все ключи из массива
|
||||
keysToDelete.forEach(key => url.searchParams.delete(key));
|
||||
url.searchParams.delete('page');
|
||||
url.searchParams.delete('direction[]');
|
||||
let newUrl = url.toString();
|
||||
this.$inertia.visit(newUrl, {
|
||||
method: 'get',
|
||||
@@ -58,9 +68,18 @@ export default {
|
||||
}, 500),
|
||||
clearFilter() {
|
||||
let url = new URL(window.location.href);
|
||||
url.searchParams.delete('direction[]');
|
||||
// Создаем массив для хранения всех ключей, которые нужно удалить
|
||||
const keysToDelete = [];
|
||||
|
||||
// Перебираем все параметры и добавляем ключи, начинающиеся с 'category', в массив
|
||||
for (const [key] of url.searchParams) {
|
||||
if (key.startsWith('direction')) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
// Удаляем все ключи из массива
|
||||
keysToDelete.forEach(key => url.searchParams.delete(key));
|
||||
this.direction = [];
|
||||
this.searchTerm = ''; // Сбросить поле поиска
|
||||
let newUrl = url.toString();
|
||||
this.$inertia.visit(newUrl, {
|
||||
method: 'get',
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<!--Фильтр-->
|
||||
|
||||
|
||||
<button type="button"
|
||||
aria-haspopup="dialog" aria-expanded="false" aria-controls="hs-offcanvas-example" data-hs-overlay="#hs-offcanvas-example"
|
||||
class="flex w-full py-2 px-4 items-center gap-x-2 text-xs font-medium rounded-lg border border-gray-200 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-primaryBlue focus:text-white disabled:opacity-50 disabled:pointer-events-none">
|
||||
<BaseIcon class="shrink-0 size-4" name="settings" />
|
||||
<span>Фильтры</span>
|
||||
</button>
|
||||
|
||||
|
||||
<!--Конец Фильтра-->
|
||||
<div id="hs-offcanvas-example" class="hs-overlay hs-overlay-open:translate-x-0 hidden -translate-x-full fixed top-0 start-0 transition-all duration-300 transform h-full max-w-xs w-full z-[80] bg-white border-e overflow-auto" role="dialog" tabindex="-1" aria-labelledby="hs-offcanvas-example-label">
|
||||
<div class="flex justify-between items-center py-3 px-4 border-b">
|
||||
<h3 id="hs-offcanvas-example-label" class="font-bold text-gray-800">
|
||||
Фильтры
|
||||
</h3>
|
||||
<button type="button" class="size-8 inline-flex justify-center items-center gap-x-2 rounded-full border border-transparent bg-gray-100 text-gray-800 hover:bg-gray-200 focus:outline-none focus:bg-gray-200 disabled:opacity-50 disabled:pointer-events-none" aria-label="Close" data-hs-overlay="#hs-offcanvas-example">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 6 6 18"></path>
|
||||
<path d="m6 6 12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<div class="hs-accordion-group">
|
||||
<div class="hs-accordion" :id="'id' + formEdu_filter.type">
|
||||
<button class="hs-accordion-toggle hs-accordion-active:text-blue-600 py-3 inline-flex items-center gap-x-3 w-full font-semibold text-start text-gray-800 hover:text-gray-500 focus:outline-none focus:text-gray-500 rounded-lg disabled:opacity-50 disabled:pointer-events-none" aria-expanded="false" aria-controls="hs-basic-with-arrow-collapse-two">
|
||||
<svg class="hs-accordion-active:hidden block size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 9 6 6 6-6"></path>
|
||||
</svg>
|
||||
<svg class="hs-accordion-active:block hidden size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m18 15-6-6-6 6"></path>
|
||||
</svg>
|
||||
Формат обучения
|
||||
</button>
|
||||
<div id="hs-basic-with-arrow-collapse-two" class="hs-accordion-content hidden w-full overflow-hidden transition-[height] duration-300" role="region" :aria-labelledby="'id' + formEdu_filter.type">
|
||||
<FormEducationalFilter :forms="forms_educational" :formEdu_filter="formEdu_filter" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseIcon from "@/Components/BaseComponents/BaseIcon.vue";
|
||||
import CategoryFilter from "@/Components/BuilderUi/Events/Filters/CategoryFilter.vue";
|
||||
import TagFilter from "@/Components/BuilderUi/Events/Filters/TagFilter.vue";
|
||||
import SortingByFilter from "@/Components/BuilderUi/Events/Filters/SortingByFilter.vue";
|
||||
import FormEducationalFilter from "@/Components/BuilderUi/Programs/Filters/FormEducationalFilter.vue";
|
||||
import BudgetFilter from "@/Components/BuilderUi/Programs/Filters/BudgetFilter.vue";
|
||||
import DirectionFilter from "@/Components/BuilderUi/Programs/Filters/DirectionFilter.vue";
|
||||
|
||||
export default {
|
||||
name: "ClientScheduleFilter",
|
||||
components: {
|
||||
DirectionFilter,
|
||||
BudgetFilter, FormEducationalFilter, SortingByFilter, TagFilter, CategoryFilter, BaseIcon},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
|
||||
props: {
|
||||
faculty_filter: {
|
||||
type: Object,
|
||||
},
|
||||
formEdu_filter: {
|
||||
type: Object
|
||||
},
|
||||
forms_educational: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -1,32 +1,34 @@
|
||||
<template>
|
||||
<div ref="sliderRef" class="relative z-0 min-h-[calc(100vh)] items-center">
|
||||
<div class="absolute -z-10 h-full w-full before:absolute before:z-10 before:h-full before:w-full before:bg-black/30">
|
||||
<a :href="(!slider.settings.link_text) ? slider.link : '#'" v-for="(slider, index) in slidersCarousel.data" :class="`brightness-[${slider.image.shading}]`"
|
||||
class="absolute -z-10 h-full w-full before:absolute before:z-10 before:h-full before:w-full ">
|
||||
<img
|
||||
alt="Thumbnail"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
data-nimg="fill"
|
||||
class="object-cover brightness-[0.7] transition-opacity duration-1000 absolute inset-0 h-full w-full"
|
||||
class="object-cover transition-opacity duration-1000 absolute inset-0 h-full w-full"
|
||||
sizes="100vw"
|
||||
v-for="(slider, index) in slidersCarousel.data"
|
||||
:key="index"
|
||||
:src="'/storage/' + slider.image"
|
||||
:src="'/storage/' + slider.image.url"
|
||||
:class="{ 'opacity-1': currentIndex === index, 'opacity-0': currentIndex !== index }"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="currentIndex === index" v-for="(item, index) in slidersCarousel.data" :key="index" class="mx-auto max-w-screen-md px-5 pt-[150px] pb-0">
|
||||
<h1 class="text-brand-primary mb-3 mt-2 text-3xl font-semibold tracking-tight text-white lg:text-5xl lg:leading-tight">
|
||||
</a>
|
||||
<div v-show="currentIndex === index" v-for="(item, index) in slidersCarousel.data" :key="index" class="mx-auto max-w-screen-md px-5 pt-[150px] pb-0 bvi-no-styles">
|
||||
<h1 v-if="item.title" :class="`text-${item.settings.text_position}`" class="text-brand-primary mb-3 mt-2 text-3xl font-semibold tracking-tight text-white lg:text-5xl lg:leading-tight">
|
||||
{{ item.title }}
|
||||
</h1>
|
||||
<div class="mt-8 flex space-x-3 text-gray-500 mb-8">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center">
|
||||
<div class="flex gap-3">
|
||||
<p class="text-gray-100 line-clamp-3"><a href="/author/erika-oliver">{{ item.content }}</a></p>
|
||||
<div v-if="item.content" class="mt-8 space-x-3 text-gray-500 mb-8">
|
||||
<div class="gap-3 md:flex-row md:items-center">
|
||||
<div class="gap-3">
|
||||
<p :class="`text-${item.settings.text_position}`" class="text-gray-100 line-clamp-3">
|
||||
{{ item.content }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a :href="item.link" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-white text-white hover:border-white/70 hover:text-white/70 disabled:opacity-50 disabled:pointer-events-none">
|
||||
{{ item.link_text }}
|
||||
<a v-if="item.settings.link_text" :href="item.link" class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-semibold rounded-lg border border-white text-white hover:border-white/70 hover:text-white/70 disabled:opacity-50 disabled:pointer-events-none">
|
||||
{{ item.settings.link_text }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-if="slidersCarousel.data.length >= 2" class="mx-auto max-w-screen-md px-5">
|
||||
|
||||
@@ -69,8 +69,6 @@
|
||||
<DirectionFilter :direction_filter="direction_filter" :direction_studies="direction_studies" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,10 +69,7 @@ export default {
|
||||
// stroke_width: 1.5,
|
||||
// stroke: 'currentColor',
|
||||
},
|
||||
|
||||
|
||||
|
||||
home: {
|
||||
home: {
|
||||
path: '<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/>',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
@@ -81,4 +78,16 @@ home: {
|
||||
},
|
||||
|
||||
|
||||
settings: {
|
||||
path: '<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"/>',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
stroke_width: 1.5,
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -60,9 +60,7 @@
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<a href="#" class="hover:opacity-70 py-3 className">
|
||||
<a @click="this.removeCookieBvi()" href="#" class="hover:opacity-70 py-3 open-bvi ">
|
||||
<svg :class="!underSliderHeader ? 'text-white' : 'text-black'" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6 duration-300 md:block hidden">
|
||||
@@ -167,6 +165,11 @@ export default {
|
||||
//
|
||||
// return hasActiveInPages || hasActiveInSubSections;
|
||||
},
|
||||
removeCookieBvi() {
|
||||
if (this.getCookie('bvi_panelActive') === 'true') {
|
||||
this.deleteCookiesWithPrefix('bvi')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
@@ -36,72 +36,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="navbar-collapse-with-animation"
|
||||
class="hs-collapse hidden overflow-hidden transition-all duration-300 basis-full grow lg:block">
|
||||
<div
|
||||
class="overflow-hidden overflow-y-auto max-h-[75vh] [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-thumb]:bg-gray-300">
|
||||
<div
|
||||
class="flex flex-col gap-x-0 mt-5 md:flex-row md:items-center md:justify-end md:gap-x-7 md:mt-0 md:ps-7 md:divide-y-0 md:divide-solid">
|
||||
<template v-for="section in this.sections.data" :key="section.id">
|
||||
<div
|
||||
class="hs-dropdown [--strategy:static] md:[--strategy:absolute] [--adaptive:none] md:[--trigger:hover] py-3 md:py-6">
|
||||
<button type="button"
|
||||
class="active:text-blue-600 flex items-center w-full text-black hover:text-gray-300 font-medium">
|
||||
{{ section.title }}
|
||||
|
||||
</button>
|
||||
<div
|
||||
class="hs-dropdown-menu transition-opacity duration-150 md:duration-500 hs-dropdown-open:opacity-100 opacity-0 w-full hidden z-10 top-full start-0 min-w-[15rem] bg-white md:shadow-2xl rounded-lg py-2 md:p-4 before:absolute before:-top-5 before:start-0 before:w-full before:h-5">
|
||||
<div class="grid px-5 grid-cols-1 md:grid-cols-10">
|
||||
|
||||
<template v-for="subsection in section.subSections" :key="subsection.id">
|
||||
<div class="md:col-span-3">
|
||||
<div class="flex flex-col py-6 px-3 md:px-6">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center mb-2 gap-x-2">
|
||||
<span class="text-xs font-bold uppercase text-gray-800">{{
|
||||
subsection.title
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<template v-for="page in subsection.pages" :key="page.id">
|
||||
<a :class="{'text-secondDarkBlue hover:text-gray-800 font-semibold ' : isSameRoute(page.path), 'text-gray-800 hover:text-gray-500' : !isSameRoute(page.path) }"
|
||||
class="flex items-center gap-x-2"
|
||||
:href="(page.is_url) ? page.path : route('page.view', page.path) + '/'">
|
||||
<div class="grow">
|
||||
<p>{{ page.title }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<a href="#" class="hover:opacity-70 py-3 className">
|
||||
<BaseIcon name="eye" class="w-6 h-6 duration-300 md:block hidden" />
|
||||
<span class="md:hidden">Режим для слабовидящих</span>
|
||||
</a>
|
||||
|
||||
|
||||
<Link :href="route('client.schedule')" class="hover:opacity-70 py-3">
|
||||
<BaseIcon name="schedule" class="w-6 h-6 text-black md:block hidden" />
|
||||
<span class="md:hidden text-black">Расписание</span>
|
||||
</Link>
|
||||
|
||||
<a class="hover:opacity-70 py-3 cursor-pointer" data-hs-overlay="#hs-full-screen-modal-below-md">
|
||||
<BaseIcon name="search" class="w-6 h-6 text-black md:block hidden" />
|
||||
<span class="md:hidden text-black">Поиск</span>
|
||||
</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DesktopNavBar v-if="sections" :sections="sections" />
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -127,6 +63,7 @@ import BaseIcon from "@/Components/BaseComponents/BaseIcon.vue";
|
||||
import MobileNavbar from "@/Navbars/MobileNavbar.vue";
|
||||
import SearchModal from "@/Components/Modals/SearchModal.vue";
|
||||
import {defineAsyncComponent} from "vue";
|
||||
import DesktopNavBar from "@/Navbars/DesktopNavBar.vue";
|
||||
|
||||
|
||||
export default {
|
||||
@@ -141,6 +78,7 @@ export default {
|
||||
return {}
|
||||
},
|
||||
components: {
|
||||
DesktopNavBar,
|
||||
SearchModal,
|
||||
MobileNavbar,
|
||||
BaseIcon,
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
||||
<MobileNavbar v-if="sections" :sections="sections" />
|
||||
|
||||
|
||||
@@ -63,6 +64,7 @@ import SearchModal from "@/Components/Modals/SearchModal.vue";
|
||||
import DesktopNavBar from "@/Navbars/DesktopNavBar.vue";
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
name: 'MainPageNavBar',
|
||||
components: {
|
||||
@@ -87,6 +89,7 @@ export default {
|
||||
headerFilter: false,
|
||||
underSliderHeader: this.sliderRef,
|
||||
bvi: null,
|
||||
isActiveBvi: null,
|
||||
logos: {
|
||||
default: '/logos/white_ntspi_logo.svg',
|
||||
alternate: '/logos/ntspi-logo.svg',
|
||||
@@ -123,6 +126,19 @@ export default {
|
||||
|
||||
},
|
||||
|
||||
iniBvi() {
|
||||
if (this.getCookie('bvi_panelActive') === null) {
|
||||
this.bvi = new isvek.Bvi({
|
||||
target: '.open-bvi',
|
||||
fontSize: 24,
|
||||
theme: 'black',
|
||||
speech: false,
|
||||
reload: true,
|
||||
panelHide: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
handleScroll() {
|
||||
if (typeof this.sliderRef === 'object') {
|
||||
const mainSlider = this.sliderRef;
|
||||
@@ -133,30 +149,19 @@ export default {
|
||||
this.headerFilter = true
|
||||
}
|
||||
},
|
||||
getCookie(name) {
|
||||
let cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
let cookie = cookies[i].trim();
|
||||
if (cookie.startsWith(name + '=')) {
|
||||
return cookie.substring(name.length + 1);
|
||||
}
|
||||
}
|
||||
return null; // Если cookie не найден
|
||||
}
|
||||
|
||||
|
||||
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
// if (this.getCookie('bvi_panelActive') === null) {
|
||||
// this.bvi = new isvek.Bvi({
|
||||
// target: '.className',
|
||||
// fontSize: 24,
|
||||
// theme: 'black',
|
||||
// speech: false,
|
||||
// reload: true,
|
||||
// });
|
||||
// }
|
||||
|
||||
|
||||
if (this.getCookie('bvi_panelActive') === null) {
|
||||
this.iniBvi()
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
|
||||
@@ -106,8 +106,11 @@ export default {
|
||||
<!-- Icon Block -->
|
||||
<div class="text-center">
|
||||
<div class="flex justify-center items-center size-12 bg-gray-50 border border-gray-200 rounded-full mx-auto">
|
||||
<svg class="flex-shrink-0 size-5 text-gray-600" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="10" height="14" x="3" y="8" rx="2"/><path d="M5 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2h-2.4"/><path d="M8 18h.01"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-semibold text-gray-800">Направление подготовки</h3>
|
||||
<p class="mt-1 text-gray-600">{{ program.data.directionStudy.name }} {{ program.data.directionStudy.code }}</p>
|
||||
@@ -118,7 +121,9 @@ export default {
|
||||
<!-- Icon Block -->
|
||||
<div class="text-center">
|
||||
<div class="flex justify-center items-center size-12 bg-gray-50 border border-gray-200 rounded-full mx-auto">
|
||||
<svg class="flex-shrink-0 size-5 text-gray-600" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-semibold text-gray-800">Срок обучения</h3>
|
||||
@@ -148,7 +153,9 @@ export default {
|
||||
<!-- Icon Block -->
|
||||
<div class="text-center">
|
||||
<div class="flex justify-center items-center size-12 bg-gray-50 border border-gray-200 rounded-full mx-auto">
|
||||
<svg class="flex-shrink-0 size-5 text-gray-600" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4c0-1.1.9-2 2-2h8a2 2 0 0 1 2 2v5Z"/><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<h3 class="text-lg font-semibold text-gray-800">Количество мест на прием</h3>
|
||||
|
||||
@@ -5,23 +5,34 @@ import { debounce } from "lodash";
|
||||
import { Link } from "@inertiajs/vue3";
|
||||
import MainPageNavBar from "@/Navbars/MainPageNavbar.vue";
|
||||
import BaseIcon from "@/Components/BaseComponents/BaseIcon.vue";
|
||||
import ClientScheduleFilter from "@/Components/BuilderUi/Schedules/ClientScheduleFilter.vue";
|
||||
|
||||
export default {
|
||||
name: "Index",
|
||||
data() {
|
||||
return {
|
||||
searchInput: this.searchRequest,
|
||||
favoriteGroups: JSON.parse(localStorage.getItem('favoriteGroups')) || [],
|
||||
searchInput: this.filters.search_filter.value,
|
||||
favoriteGroups: JSON.parse(localStorage.getItem('favoriteGroups')),
|
||||
showFavorites: false,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
components: {BaseIcon, MainPageNavBar, ClientFooterDown, MainNavbar, Link},
|
||||
props: [
|
||||
'educationalGroups',
|
||||
'mainSections',
|
||||
'searchRequest',
|
||||
'navigation',
|
||||
],
|
||||
components: {ClientScheduleFilter, BaseIcon, MainPageNavBar, ClientFooterDown, MainNavbar, Link},
|
||||
|
||||
props: {
|
||||
navigation: {
|
||||
type: Object
|
||||
},
|
||||
filters: {
|
||||
type: Object
|
||||
},
|
||||
forms_education: {
|
||||
type: Object
|
||||
},
|
||||
schedulesByFaculty: {
|
||||
type: Object
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search: debounce(function () {
|
||||
this.$inertia.reload({
|
||||
@@ -34,7 +45,7 @@ export default {
|
||||
});
|
||||
}, 300),
|
||||
toggleFavorite(group) {
|
||||
const index = this.favoriteGroups.findIndex(g => g.id === group.id);
|
||||
const index = this.favoriteGroups.findIndex(g => g === group);
|
||||
if (index === -1) {
|
||||
this.favoriteGroups.push(group);
|
||||
} else {
|
||||
@@ -43,18 +54,65 @@ export default {
|
||||
localStorage.setItem('favoriteGroups', JSON.stringify(this.favoriteGroups));
|
||||
},
|
||||
isFavorite(group) {
|
||||
return this.favoriteGroups.some(g => g.id === group.id);
|
||||
return this.favoriteGroups.some(g => g === group);
|
||||
},
|
||||
toggleShowFavorites() {
|
||||
this.showFavorites = !this.showFavorites;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
filteredGroups() {
|
||||
if (this.showFavorites) {
|
||||
return this.educationalGroups.data.filter(group => this.favoriteGroups.some(g => g.id === group.id));
|
||||
this.loading = true; // Включаем состояние загрузки
|
||||
|
||||
if (this.filters.favorite_filter.value === null) {
|
||||
this.filterFavorite(() => {
|
||||
this.loading = false; // Выключаем состояние загрузки после завершения
|
||||
});
|
||||
} else {
|
||||
this.clearFilterFavorite(() => {
|
||||
this.loading = false; // Выключаем состояние загрузки после завершения
|
||||
});
|
||||
}
|
||||
return this.educationalGroups.data;
|
||||
},
|
||||
|
||||
filterFavorite: debounce(function (callback) {
|
||||
let url = new URL(window.location.href);
|
||||
// Создаем массив для хранения всех ключей, которые нужно удалить
|
||||
const keysToDelete = [];
|
||||
|
||||
// Перебираем все параметры и добавляем ключи, начинающиеся с 'category', в массив
|
||||
for (const [key] of url.searchParams) {
|
||||
if (key.startsWith('favorite')) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
// Удаляем все ключи из массива
|
||||
keysToDelete.forEach(key => url.searchParams.delete(key));
|
||||
let newUrl = url.toString();
|
||||
this.$inertia.visit(newUrl, {
|
||||
method: 'get',
|
||||
preserveState: true,
|
||||
data: {
|
||||
favorite: (this.favoriteGroups.length !== 0) ? this.favoriteGroups : "",
|
||||
},
|
||||
onFinish: callback, // Вызываем колбэк после завершения запроса
|
||||
});
|
||||
}, 500),
|
||||
clearFilterFavorite(callback) {
|
||||
let url = new URL(window.location.href);
|
||||
// Создаем массив для хранения всех ключей, которые нужно удалить
|
||||
const keysToDelete = [];
|
||||
|
||||
// Перебираем все параметры и добавляем ключи, начинающиеся с 'category', в массив
|
||||
for (const [key] of url.searchParams) {
|
||||
if (key.startsWith('favorite')) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
// Удаляем все ключи из массива
|
||||
keysToDelete.forEach(key => url.searchParams.delete(key));
|
||||
let newUrl = url.toString();
|
||||
this.$inertia.visit(newUrl, {
|
||||
method: 'get',
|
||||
preserveState: true,
|
||||
onFinish: callback, // Вызываем колбэк после завершения запроса
|
||||
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -69,14 +127,13 @@ export default {
|
||||
<article class="w-full min-w-0 mt-4 px-1 md:px-6">
|
||||
<div class="relative overflow-hidden">
|
||||
<div class="max-w-[85rem] mx-auto sm:px-6 lg:px-8 py-10 sm:pb-12 sm:py-5">
|
||||
<div class="text-center">
|
||||
<h1 class="text-2xl sm:text-4xl font-bold text-gray-800 dark:text-gray-200">
|
||||
<div class="">
|
||||
<h1 class="text-2xl sm:text-4xl font-bold text-gray-800 text-center">
|
||||
Расписание занятий
|
||||
</h1>
|
||||
|
||||
|
||||
<div class="text-center">
|
||||
<p class="mt-3 text-gray-600 dark:text-gray-400">
|
||||
<div class="">
|
||||
<p class="mt-3 text-gray-600 text-center">
|
||||
Просто введите название группы
|
||||
</p>
|
||||
</div>
|
||||
@@ -100,7 +157,7 @@ export default {
|
||||
id="hs-search-article-1"
|
||||
class="py-2.5 px-4 block w-full border-transparent rounded-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-100"
|
||||
placeholder="Поиск"
|
||||
:disabled="showFavorites"
|
||||
:disabled="filters.favorite_filter.value !== null"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,24 +165,20 @@ export default {
|
||||
</form>
|
||||
<div class="">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<button @click="toggleShowFavorites" type="button"
|
||||
:class="showFavorites ? 'bg-primaryBlue text-white hover:bg-secondDarkBlue' : 'text-gray-700 hover:bg-gray-100'"
|
||||
class="flex w-full py-2 px-4 items-center gap-x-2 text-xs font-medium rounded-lg border border-gray-200 focus:outline-none disabled:opacity-50 disabled:pointer-events-none">
|
||||
<button
|
||||
@click="toggleShowFavorites"
|
||||
type="button"
|
||||
:disabled="favoriteGroups.length === 0 || loading"
|
||||
:class="
|
||||
filters.favorite_filter.value !== null ? 'bg-primaryBlue text-white hover:bg-secondDarkBlue': 'text-gray-700 hover:bg-gray-100',
|
||||
loading ? 'animate-pulse' : ''
|
||||
"
|
||||
class="flex w-full py-2 px-4 items-center gap-x-2 text-xs font-medium rounded-lg border border-gray-200 focus:outline-none disabled:opacity-50 disabled:pointer-events-none"
|
||||
>
|
||||
<BaseIcon name="heart" class="shrink-0 size-4"/>
|
||||
<span>Избранные расписания</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="flex w-full py-2 px-4 items-center gap-x-2 text-xs font-medium rounded-lg border border-gray-200 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-primaryBlue focus:text-white disabled:opacity-50 disabled:pointer-events-none">
|
||||
<svg class="shrink-0 size-4" xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<line x1="19" x2="19" y1="8" y2="14"/>
|
||||
<line x1="22" x2="16" y1="11" y2="11"/>
|
||||
</svg>
|
||||
<span>Follow</span>
|
||||
<span>Избранное</span>
|
||||
</button>
|
||||
<ClientScheduleFilter :forms_educational="this.forms_education" :form-edu_filter="this.filters.form_education_filter" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,18 +186,20 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto max-w-2xl hs-accordion-group grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
<div v-if="!showFavorites">
|
||||
<div class="mx-auto max-w-2xl hs-accordion-group grid gap-3">
|
||||
<div class="space-y-4">
|
||||
<transition-group name="fade">
|
||||
<template v-for="educationalGroup in filteredGroups" :key="educationalGroup.id">
|
||||
<div
|
||||
class="hs-accordion hs-accordion-active:border-gray-200 bg-white border-b dark:hs-accordion-active:border-gray-700 dark:bg-gray-800 dark:border-transparent"
|
||||
id="hs-active-bordered-heading-one">
|
||||
<div class="flex">
|
||||
<button @click="toggleFavorite(educationalGroup)">
|
||||
<template v-for="(faculty, title) in schedulesByFaculty">
|
||||
<div class="">
|
||||
<h2 class="text-center text-gray-800 font-medium">{{ title }}</h2>
|
||||
<hr class="mt-2 mb-4">
|
||||
<template v-for="educationalGroup in faculty" :key="educationalGroup.data.id">
|
||||
<div class="hs-accordion hs-accordion-active:border-gray-200 bg-white border border-transparent rounded-xl" id="hs-active-bordered-heading-one">
|
||||
<div class="flex py-4 px-5 gap-x-3">
|
||||
<button @click="toggleFavorite(educationalGroup.data.id)">
|
||||
<transition name="heart" mode="out-in">
|
||||
<BaseIcon
|
||||
v-if="isFavorite(educationalGroup)"
|
||||
v-if="isFavorite(educationalGroup.data.id)"
|
||||
name="heart_filled"
|
||||
class="shrink-0 size-4 text-red-500"
|
||||
key="filled"
|
||||
@@ -158,33 +213,21 @@ export default {
|
||||
</transition>
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="hs-accordion-toggle hs-accordion-active:text-blue-600 inline-flex justify-between items-center gap-x-3 w-full font-semibold text-start text-gray-800 py-4 px-5 hover:text-gray-500 disabled:opacity-50 disabled:pointer-events-none dark:hs-accordion-active:text-blue-500 dark:text-gray-200 dark:hover:text-gray-400 dark:focus:outline-none dark:focus:text-gray-400"
|
||||
aria-controls="hs-basic-active-bordered-collapse-one">
|
||||
|
||||
<div class="flex items-center gap-x-2"><span>{{ educationalGroup.title }}</span> <span
|
||||
class="font-light text-sm uppercase text-gray-500">{{ educationalGroup.faculty }}</span></div>
|
||||
<svg class="hs-accordion-active:hidden block w-3.5 h-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M12 5v14"/>
|
||||
<button class="hs-accordion-toggle hs-accordion-active:text-blue-600 inline-flex justify-between items-center gap-x-3 w-full font-semibold text-start text-gray-800 hover:text-gray-500 disabled:opacity-50 disabled:pointer-events-none" aria-expanded="false" aria-controls="hs-basic-active-bordered-collapse-one">
|
||||
<span class="flex items-center gap-x-2">{{ educationalGroup.data.title }}</span>
|
||||
<svg class="hs-accordion-active:hidden block size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="M12 5v14"></path>
|
||||
</svg>
|
||||
<svg class="hs-accordion-active:block hidden w-3.5 h-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"/>
|
||||
<svg class="hs-accordion-active:block hidden size-3.5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="hs-basic-active-bordered-collapse-one"
|
||||
class="hs-accordion-content hidden w-full overflow-hidden transition-[height] duration-300"
|
||||
aria-labelledby="hs-active-bordered-heading-one">
|
||||
|
||||
<div id="hs-basic-active-bordered-collapse-one" class="hs-accordion-content hidden w-full overflow-hidden transition-[height] duration-300" role="region" aria-labelledby="hs-active-bordered-heading-one">
|
||||
<div class="pb-4 px-5 grid gap-3 grid-cols-1">
|
||||
<template v-for="schedule in educationalGroup.schedules" :key="schedule.id">
|
||||
<template v-for="schedule in educationalGroup.data.schedules" :key="schedule.id">
|
||||
<template v-for="file in schedule.file">
|
||||
<a :href="'storage/' + file.path" target="_blank"
|
||||
class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-500 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600">
|
||||
@@ -196,68 +239,6 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</transition-group>
|
||||
</div>
|
||||
<div v-else>
|
||||
<transition-group name="fade">
|
||||
<template v-for="educationalGroup in favoriteGroups" :key="educationalGroup.id">
|
||||
<div
|
||||
class="hs-accordion hs-accordion-active:border-gray-200 bg-white border-b dark:hs-accordion-active:border-gray-700 dark:bg-gray-800 dark:border-transparent"
|
||||
id="hs-active-bordered-heading-one">
|
||||
<div class="flex">
|
||||
<button @click="toggleFavorite(educationalGroup)">
|
||||
<transition name="heart" mode="out-in">
|
||||
<BaseIcon
|
||||
v-if="isFavorite(educationalGroup)"
|
||||
name="heart_filled"
|
||||
class="shrink-0 size-4 text-red-500"
|
||||
key="filled"
|
||||
/>
|
||||
<BaseIcon
|
||||
v-else
|
||||
name="heart"
|
||||
class="shrink-0 size-4"
|
||||
key="outline"
|
||||
/>
|
||||
</transition>
|
||||
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="hs-accordion-toggle hs-accordion-active:text-blue-600 inline-flex justify-between items-center gap-x-3 w-full font-semibold text-start text-gray-800 py-4 px-5 hover:text-gray-500 disabled:opacity-50 disabled:pointer-events-none dark:hs-accordion-active:text-blue-500 dark:text-gray-200 dark:hover:text-gray-400 dark:focus:outline-none dark:focus:text-gray-400"
|
||||
aria-controls="hs-basic-active-bordered-collapse-one">
|
||||
|
||||
<div class="flex items-center gap-x-2"><span>{{ educationalGroup.title }}</span> <span
|
||||
class="font-light text-sm uppercase text-gray-500">{{ educationalGroup.faculty }}</span></div>
|
||||
<svg class="hs-accordion-active:hidden block w-3.5 h-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"/>
|
||||
<path d="M12 5v14"/>
|
||||
</svg>
|
||||
<svg class="hs-accordion-active:block hidden w-3.5 h-3.5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="hs-basic-active-bordered-collapse-one"
|
||||
class="hs-accordion-content hidden w-full overflow-hidden transition-[height] duration-300"
|
||||
aria-labelledby="hs-active-bordered-heading-one">
|
||||
<div class="pb-4 px-5 grid gap-3 grid-cols-1">
|
||||
<template v-for="schedule in educationalGroup.schedules" :key="schedule.id">
|
||||
<template v-for="file in schedule.file">
|
||||
<a :href="'storage/' + file.path" target="_blank"
|
||||
class="py-3 px-4 inline-flex items-center gap-x-2 text-sm font-medium rounded-lg border border-gray-200 bg-white text-gray-500 shadow-sm hover:bg-gray-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:focus:outline-none dark:focus:ring-1 dark:focus:ring-gray-600">
|
||||
{{ file.title }}
|
||||
</a>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</transition-group>
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
<MainPageNavBar :sections="$page.props.navigation" :slider-ref="sliderRef" />
|
||||
<ClientMainSlider @slider-mounted="setSliderRef" :slidersCarousel="sliders" />
|
||||
<section class="max-w-screen-xl w-full mx-auto px-4 py-3 pb-10">
|
||||
<h2 class="text-brand-primary my-6 md:mb-[50px] md:mt-[80px] text-2xl font-semibold tracking-tight text-black lg:text-[32px] lg:leading-tight">Последние новости</h2>
|
||||
<div class="grid gap-10 pb-10 md:grid-cols-2 lg:gap-10 xl:grid-cols-3">
|
||||
<h2 class="text-brand-primary my-6 md:mb-[50px] md:mt-[80px] text-2xl font-semibold tracking-tight text-black lg:text-[32px] lg:leading-tight bvi-show">Последние новости</h2>
|
||||
<div class="grid gap-10 pb-10 md:grid-cols-2 lg:gap-10 xl:grid-cols-3 bvi-no-styles">
|
||||
<template v-for="post in posts.data" :key="post.id">
|
||||
<ClientPost :post="post" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="flex justify-center">
|
||||
<a :href="route('client.post.index')" class="group mt-3 inline-flex items-center gap-x-1 text-sm font-semibold text-primaryBlue">
|
||||
Все новости
|
||||
@@ -184,6 +185,7 @@ import BaseMetaHead from "@/Components/BaseComponents/BaseMetaHead.vue";
|
||||
import PageResourceList from "@/Components/BuilderUi/Pages/Blocks/PageResourceList.vue";
|
||||
import AppHead from "@/Components/AppHead.vue";
|
||||
import ContactSectionBlock from "@/Components/BaseComponents/BaseBuilderUi/Blocks/Contacts/ContactSectionBlock.vue";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
|
||||
|
||||
@@ -238,6 +240,7 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
Cookies,
|
||||
setSliderRef(ref) {
|
||||
this.sliderRef = ref;
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</Head>
|
||||
<MainPageNavBar class="border-b" :sections="$page.props.navigation"></MainPageNavBar>
|
||||
|
||||
<div class="w-full h-[67px] fixed pointer-events-none" id="visor"></div>
|
||||
<div class="w-full h-[67px] fixed pointer-events-none bvi-no-styles" id="visor"></div>
|
||||
<div class="flex flex-col h-screen">
|
||||
<main class="flex-grow">
|
||||
<div class="relative mx-auto mt-[67px] max-w-screen-xl px-4 py-10 md:flex md:flex-row md:py-10">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createInertiaApp } from '@inertiajs/vue3';
|
||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
|
||||
import {linksReform} from "@/mixins/LinksReform.js";
|
||||
import YSmartCaptcha from 'vue3-yandex-smartcaptcha'
|
||||
import cookieMixin from "@/mixins/cookieMixin.js";
|
||||
|
||||
// const appName = import.meta.env.VITE_APP_NAME || 'НТГСПИ';
|
||||
|
||||
@@ -20,6 +21,7 @@ createInertiaApp({
|
||||
return createSSRApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.mixin(linksReform)
|
||||
.mixin(cookieMixin)
|
||||
.use(ZiggyVue)
|
||||
.use(YSmartCaptcha, {
|
||||
siteKey: "ysc1_jxb2IcslgBuHgHbmmKY1F7Nvb8zYHuAeBgjoaTka6886f893",
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
export default {
|
||||
methods: {
|
||||
// Устанавливаем куки
|
||||
setCookie(name, value, days) {
|
||||
let expires = "";
|
||||
if (days) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); // Устанавливаем срок действия куки
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/"; // Устанавливаем куки
|
||||
},
|
||||
|
||||
// Получаем куки
|
||||
getCookie(name) {
|
||||
const nameEQ = name + "="; // Форматируем имя куки
|
||||
const ca = document.cookie.split(';'); // Разбиваем куки по точке с запятой
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === ' ') c = c.substring(1, c.length); // Убираем пробелы
|
||||
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); // Если найдено, возвращаем значение
|
||||
}
|
||||
return null; // Если кука не найдена
|
||||
},
|
||||
|
||||
// Удаляем куки
|
||||
deleteCookie(name) {
|
||||
// Получаем текущий домен
|
||||
const domain = window.location.hostname.includes('.') ? '.' + window.location.hostname : window.location.hostname;
|
||||
document.cookie = name + '=; Max-Age=-99999999; path=/; domain=' + domain; // Устанавливаем срок действия в прошлом
|
||||
},
|
||||
|
||||
deleteCookiesWithPrefix(prefix) {
|
||||
const cookies = document.cookie.split(';'); // Получаем все куки
|
||||
for (let cookie of cookies) {
|
||||
const cookieName = cookie.split('=')[0].trim(); // Получаем имя куки
|
||||
if (cookieName.startsWith(prefix)) { // Проверяем, начинается ли имя куки с заданного префикса
|
||||
this.deleteCookie(cookieName); // Удаляем куку
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -30,11 +30,20 @@
|
||||
z-index: 10000000!important;
|
||||
}
|
||||
|
||||
.bvi-active {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ce-header {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
body .bvi-body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
*, html {
|
||||
scroll-padding: 6rem;
|
||||
scroll-behavior: smooth !important;
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\ClientWidgetPageReferenceListController;
|
||||
use App\Http\Controllers\ClientWidgetPostController;
|
||||
use App\Http\Controllers\IconController;
|
||||
use App\Http\Controllers\SearchController;
|
||||
use App\Http\Controllers\UpdateEduDataApiController;
|
||||
use App\Http\Controllers\VkAuthController;
|
||||
use App\Http\Controllers\VkPostController;
|
||||
use App\Models\AdmissionCampaign;
|
||||
@@ -25,6 +26,9 @@ Route::get('/getAcademicYear', function () {
|
||||
})->name('academic.year');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Route::middleware('ensure.browser')->group(function () {
|
||||
Route::get('/getNavigation', [NavigateController::class, 'index'])->name('client.main.navigate');
|
||||
|
||||
@@ -50,6 +54,8 @@ Route::middleware('ensure.browser')->group(function () {
|
||||
});
|
||||
|
||||
Route::middleware(['auth', 'superadmin'])->group(function () {
|
||||
Route::get('/get-data', [UpdateEduDataApiController::class, 'index']);
|
||||
|
||||
Route::get('/login/vk', [VkAuthService::class, 'redirectToProvider'])->name('vk.login');
|
||||
Route::get('/login/vk/callback', [VkAuthService::class, 'handleProviderCallback'])->name('vk.callback');
|
||||
Route::get('/vk-get-token', [VkAuthService::class, 'getToken'])->name('vk.getToken');
|
||||
|
||||
@@ -86,8 +86,6 @@ Route::middleware('access-check')->group(function () {
|
||||
Route::get('/divisions', [ClientDivisionController::class, 'index'])->name('client.division.index');
|
||||
Route::get('/divisions/{slug}', [ClientDivisionController::class, 'show'])->name('client.division.show');
|
||||
|
||||
Route::get('/get-data', [UpdateEduDataApiController::class, 'index']);
|
||||
|
||||
Route::get('{path}', [PageController::class, 'render'])->where('path', '[0-9,a-z,/,-]+')->name('page.view');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user