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 App\Models\Post;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
use Filament\Forms\Components\Builder;
|
use Filament\Forms\Components\Builder;
|
||||||
|
use Filament\Forms\Components\ColorPicker;
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
use Filament\Forms\Components\Grid;
|
use Filament\Forms\Components\Grid;
|
||||||
@@ -97,7 +98,7 @@ class PostForm
|
|||||||
]),
|
]),
|
||||||
Tabs\Tab::make('Содержание новости')
|
Tabs\Tab::make('Содержание новости')
|
||||||
->schema([
|
->schema([
|
||||||
ContentBuilderItem::getItem('content')
|
ContentBuilderItem::getItem('content')->required(),
|
||||||
]),
|
]),
|
||||||
Tabs\Tab::make('Изображения')
|
Tabs\Tab::make('Изображения')
|
||||||
->schema([
|
->schema([
|
||||||
@@ -118,6 +119,51 @@ class PostForm
|
|||||||
->multiple()
|
->multiple()
|
||||||
->directory('images'),
|
->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\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
|
use Filament\Tables\Columns\TextInputColumn;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
@@ -29,7 +30,7 @@ class AcceptedInvitationResource extends Resource implements HasShieldPermission
|
|||||||
{
|
{
|
||||||
return $form
|
return $form
|
||||||
->schema([
|
->schema([
|
||||||
//
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +38,8 @@ class AcceptedInvitationResource extends Resource implements HasShieldPermission
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->columns([
|
||||||
//
|
Tables\Columns\TextColumn::make('receiver.name')->label('Приглашенный пользователь'),
|
||||||
|
TextInputColumn::make('post_limit')->label('Лимит постов')->default(0)->rules(['required', 'max:10', 'integer'])
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -104,4 +104,16 @@ class ContactWidgetResource extends Resource
|
|||||||
'edit' => Pages\EditContactWidget::route('/{record}/edit'),
|
'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;
|
namespace App\Filament\Resources;
|
||||||
|
|
||||||
|
use App\Enums\FormEducation;
|
||||||
use App\Filament\Resources\EducationalGroupResource\Pages;
|
use App\Filament\Resources\EducationalGroupResource\Pages;
|
||||||
use App\Filament\Resources\EducationalGroupResource\RelationManagers;
|
use App\Filament\Resources\EducationalGroupResource\RelationManagers;
|
||||||
use App\Models\EducationalGroup;
|
use App\Models\EducationalGroup;
|
||||||
@@ -33,7 +34,9 @@ class EducationalGroupResource extends Resource
|
|||||||
Forms\Components\Grid::make(2)->schema([
|
Forms\Components\Grid::make(2)->schema([
|
||||||
Forms\Components\TextInput::make('title')->label('Название группы')->required(),
|
Forms\Components\TextInput::make('title')->label('Название группы')->required(),
|
||||||
Forms\Components\Select::make('faculty_id')->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 App\Models\Post;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
use Filament\Forms\Components\ColorPicker;
|
use Filament\Forms\Components\ColorPicker;
|
||||||
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
use Filament\Forms\Components\FileUpload;
|
use Filament\Forms\Components\FileUpload;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
|
use Filament\Forms\Components\ToggleButtons;
|
||||||
use Filament\Forms\Form;
|
use Filament\Forms\Form;
|
||||||
use Filament\Resources\Resource;
|
use Filament\Resources\Resource;
|
||||||
use Filament\Tables;
|
use Filament\Tables;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Yepsua\Filament\Forms\Components\RangeSlider;
|
||||||
|
|
||||||
class MainSliderResource extends Resource
|
class MainSliderResource extends Resource
|
||||||
{
|
{
|
||||||
@@ -96,32 +100,75 @@ class MainSliderResource extends Resource
|
|||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
Forms\Components\Section::make('Слайдер')->schema([
|
Forms\Components\Section::make('Слайдер')->schema([
|
||||||
|
|
||||||
|
Forms\Components\Section::make('Информация слайда')->schema([
|
||||||
Forms\Components\TextInput::make('title')
|
Forms\Components\TextInput::make('title')
|
||||||
->label('Заголовок слайда')
|
->label('Заголовок слайда'),
|
||||||
->required(),
|
|
||||||
Forms\Components\Textarea::make('content')
|
Forms\Components\Textarea::make('content')
|
||||||
->label('Текст слайда'),
|
->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('Изображение')
|
->label('Изображение')
|
||||||
->image()
|
->image()
|
||||||
->optimize('webp')
|
->optimize('webp')
|
||||||
->resize(50)
|
->resize(50)
|
||||||
->disk('public')
|
->disk('public')
|
||||||
->directory('images')
|
->directory('images')
|
||||||
->imageEditor(),
|
->imageEditor()
|
||||||
Forms\Components\Grid::make(2)->schema([
|
->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')
|
Forms\Components\TextInput::make('link')
|
||||||
->label('Ссылка кнопки')
|
->label('Ссылка кнопки')
|
||||||
->required(),
|
->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),
|
Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false),
|
||||||
]),
|
]),
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ class PageResource extends Resource
|
|||||||
|
|
||||||
protected static ?string $pluralLabel = 'Страницы';
|
protected static ?string $pluralLabel = 'Страницы';
|
||||||
|
|
||||||
public static ?string $label = 'Страница';
|
|
||||||
|
|
||||||
|
|
||||||
protected static ?string $navigationGroup = 'Структура приложения';
|
protected static ?string $navigationGroup = 'Структура приложения';
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,12 @@ class PostResource extends Resource implements HasShieldPermissions
|
|||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
->columns([
|
->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('title')->label('Заголовок')->sortable()->searchable(),
|
||||||
Tables\Columns\TextColumn::make('status')->label('Статус')->sortable()->badge(),
|
Tables\Columns\TextColumn::make('status')->label('Статус')->sortable()->badge(),
|
||||||
Tables\Columns\TextColumn::make('publish_at')->label('Дата публикации')->sortable(),
|
Tables\Columns\TextColumn::make('publish_at')->label('Дата публикации')->sortable(),
|
||||||
|
Tables\Columns\TextColumn::make('author.name')->label('Автор')->sortable()->searchable(),
|
||||||
|
|
||||||
])->defaultSort('publish_at', 'desc')
|
])->defaultSort('publish_at', 'desc')
|
||||||
->filters([
|
->filters([
|
||||||
@@ -92,6 +94,8 @@ class PostResource extends Resource implements HasShieldPermissions
|
|||||||
'index' => Pages\ListPosts::route('/'),
|
'index' => Pages\ListPosts::route('/'),
|
||||||
'create' => Pages\CreatePost::route('/create'),
|
'create' => Pages\CreatePost::route('/create'),
|
||||||
'edit' => Pages\EditPost::route('/{record}/edit'),
|
'edit' => Pages\EditPost::route('/{record}/edit'),
|
||||||
|
'view' => Pages\ViewPost::route('/{record}'),
|
||||||
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,29 +2,15 @@
|
|||||||
|
|
||||||
namespace App\Filament\Resources\PostResource\Pages;
|
namespace App\Filament\Resources\PostResource\Pages;
|
||||||
|
|
||||||
|
use App\Dto\MainSliderDTO;
|
||||||
use App\Enums\PostStatus;
|
use App\Enums\PostStatus;
|
||||||
use App\Filament\Resources\PostResource;
|
use App\Filament\Resources\PostResource;
|
||||||
use App\Jobs\CreateVkPost;
|
use App\Services\Filament\Domain\Posts\PostDataProcessor;
|
||||||
use App\Models\Post;
|
use App\Services\Filament\Domain\Posts\PostNotificationService;
|
||||||
use App\Models\User;
|
use App\Services\Filament\Domain\Posts\PostSeoGenerator;
|
||||||
use App\Services\VK\VkService;
|
use App\Services\Filament\Domain\Posts\PostSliderService;
|
||||||
use Carbon\Carbon;
|
use App\Services\Filament\Domain\Posts\VkPostPublisher;
|
||||||
use Closure;
|
|
||||||
use Filament\Actions;
|
|
||||||
use Filament\Notifications\Actions\Action;
|
|
||||||
use Filament\Notifications\Notification;
|
|
||||||
use Filament\Resources\Pages\CreateRecord;
|
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
|
class CreatePost extends CreateRecord
|
||||||
{
|
{
|
||||||
@@ -33,277 +19,68 @@ class CreatePost extends CreateRecord
|
|||||||
protected array $seoData;
|
protected array $seoData;
|
||||||
protected array $publicationAgreements;
|
protected array $publicationAgreements;
|
||||||
|
|
||||||
|
protected array $slideData;
|
||||||
|
|
||||||
|
protected static array|string $routeMiddleware = ['limit.post'];
|
||||||
|
|
||||||
|
|
||||||
protected function mutateFormDataBeforeCreate(array $data): array
|
protected function mutateFormDataBeforeCreate(array $data): array
|
||||||
{
|
{
|
||||||
$this->publicationAgreements = $data['publication'];
|
$this->extractAdditionalData($data);
|
||||||
unset($data['publication']);
|
return $this->processPostData($data);
|
||||||
$this->seoData = $this->generateSeo($data);
|
}
|
||||||
$data['preview_text'] = $this->setPreviewText($data);
|
|
||||||
$data['publish_at'] = $this->setPublishDateTime($data['publish_setting']);
|
protected function extractAdditionalData(array &$data): void
|
||||||
unset($data['publish_setting']);
|
{
|
||||||
$data['search_data'] = $this->generateSearchData($data['content']);
|
$this->publicationAgreements = $data['publication'] ?? [];
|
||||||
$data['reading_time'] = $this->calculateReadingTime($data['search_data']);
|
$this->slideData = $data['slide'] ?? [];
|
||||||
return $data;
|
unset($data['slide'], $data['publication']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function processPostData(array $data): array
|
||||||
|
{
|
||||||
|
return (new PostDataProcessor())->process($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function afterCreate(): void
|
protected function afterCreate(): void
|
||||||
{
|
{
|
||||||
$this->record->seo()->create($this->seoData);
|
$this->handleSlides();
|
||||||
$this->sendNotify($this->record, auth()->user());
|
$this->generateSeo();
|
||||||
$publish_date = ($this->record->publish_at > now()) ? Carbon::parse($this->record->publish_at)->timestamp : null;
|
$this->sendNotifications();
|
||||||
$this->postToSocialMedia($this->publicationAgreements, $this->record->content, $this->record->title, $publish_date);
|
$this->publishToVk();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateSeo(array $data) : array
|
protected function handleSlides(): void
|
||||||
{
|
{
|
||||||
$title = $data['title'];
|
if (empty($this->slideData)) {
|
||||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
return;
|
||||||
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,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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']);
|
$seoData = (new PostSeoGenerator())->generate([
|
||||||
if ($rowData === null) {
|
'title' => $this->record->title,
|
||||||
$rowData = $this->getFirstBlockByName('paragraph', $data['content']);
|
'content' => $this->record->content,
|
||||||
}
|
'preview' => $this->record->preview,
|
||||||
$preview_text = html_entity_decode(strip_tags($rowData['data']['content']));
|
]);
|
||||||
return Str::limit($preview_text, 160);
|
$this->record->seo()->create($seoData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateSearchData(array $data) : string
|
protected function sendNotifications(): void
|
||||||
{
|
{
|
||||||
$result = "";
|
(new PostNotificationService())->send($this->record);
|
||||||
foreach ($data as $block) {
|
|
||||||
$result .= $this->getDataFromBlocks($block);
|
|
||||||
}
|
}
|
||||||
// Удаляем лишние пробелы и переносы строк
|
|
||||||
$result = preg_replace('/\s+/', ' ', $result);
|
|
||||||
$result = trim($result);
|
|
||||||
|
|
||||||
return strtolower($result);
|
protected function publishToVk(): void
|
||||||
}
|
|
||||||
private function getFirstBlockByName(string $name, array $content) : array|null
|
|
||||||
{
|
{
|
||||||
$data = null;
|
(new VkPostPublisher())->publish($this->publicationAgreements, $this->record);
|
||||||
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 изображений
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
|
|
||||||
namespace App\Filament\Resources\PostResource\Pages;
|
namespace App\Filament\Resources\PostResource\Pages;
|
||||||
|
|
||||||
|
use App\Dto\MainSliderDTO;
|
||||||
use App\Enums\PostStatus;
|
use App\Enums\PostStatus;
|
||||||
use App\Filament\Resources\PostResource;
|
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 Carbon\Carbon;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Resources\Pages\EditRecord;
|
use Filament\Resources\Pages\EditRecord;
|
||||||
use Illuminate\Support\Facades\Storage;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class EditPost extends EditRecord
|
class EditPost extends EditRecord
|
||||||
{
|
{
|
||||||
@@ -18,224 +21,75 @@ class EditPost extends EditRecord
|
|||||||
protected array $seoData;
|
protected array $seoData;
|
||||||
protected array $publicationAgreements;
|
protected array $publicationAgreements;
|
||||||
|
|
||||||
|
protected array $slideData;
|
||||||
|
|
||||||
|
|
||||||
protected function mutateFormDataBeforeSave(array $data): array
|
protected function mutateFormDataBeforeSave(array $data): array
|
||||||
{
|
{
|
||||||
$this->seoData = $this->generateSeo($data);
|
$this->extractAdditionalData($data);
|
||||||
$this->publicationAgreements = $data['publication'];
|
return $this->processPostData($data);
|
||||||
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']);
|
|
||||||
|
|
||||||
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
|
protected function afterSave(): void
|
||||||
{
|
{
|
||||||
$this->record->seo()->update($this->seoData);
|
$this->handleSlides();
|
||||||
$publish_date = ($this->record->publish_at > now()) ? Carbon::parse($this->record->publish_at)->timestamp : null;
|
$this->generateSeo();
|
||||||
$this->postToSocialMedia($this->publicationAgreements, $this->record->content, $this->record->title, $publish_date);
|
$this->sendNotifications();
|
||||||
|
$this->publishToVk();
|
||||||
}
|
}
|
||||||
|
|
||||||
private function setPreviewText(array $data) : string|null
|
|
||||||
|
protected function handleSlides(): void
|
||||||
{
|
{
|
||||||
$rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
|
if (empty($this->slideData)) {
|
||||||
if ($rowData === null) {
|
return;
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = [];
|
$seoData = (new PostSeoGenerator())->generate([
|
||||||
foreach ($content as $block) {
|
'title' => $this->record->title,
|
||||||
if ($block['type'] === $name) {
|
'content' => $this->record->content,
|
||||||
$data[] = $block;
|
'preview' => $this->record->preview,
|
||||||
}
|
]);
|
||||||
}
|
$this->record->seo()->update($seoData);
|
||||||
$block = null;
|
|
||||||
foreach ($data as $item) {
|
|
||||||
if ($item['data']['seo_active'] === true) {
|
|
||||||
$block = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateSeo(array $data) : array
|
protected function sendNotifications(): 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 = ($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
|
|
||||||
{
|
{
|
||||||
|
// Отправляем уведомления
|
||||||
|
$notificationService = new PostNotificationService();
|
||||||
if ($this->record->status === PostStatus::PUBLISHED) {
|
if ($this->record->status === PostStatus::PUBLISHED) {
|
||||||
if ($settings['vk']) {
|
$notificationService->sendSuccessNotification($this->record);
|
||||||
$text = "";
|
} elseif ($this->record->status === PostStatus::REJECTED) {
|
||||||
foreach ($content as $block) {
|
$notificationService->sendDeniedNotification($this->record);
|
||||||
$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));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function generateImageLinksToVK($images)
|
protected function publishToVk(): void
|
||||||
{
|
{
|
||||||
|
(new VkPostPublisher())->publish($this->publicationAgreements, $this->record);
|
||||||
$imageUrls = array_map(function ($file) {
|
|
||||||
return url(Storage::url($file)); // Добавляем домен
|
|
||||||
}, $images);
|
|
||||||
|
|
||||||
|
|
||||||
return $imageUrls; // Возвращаем массив с полными URL изображений
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ class UserResource extends Resource implements HasShieldPermissions
|
|||||||
Forms\Components\TextInput::make('password')
|
Forms\Components\TextInput::make('password')
|
||||||
->label('Пароль')
|
->label('Пароль')
|
||||||
->password()
|
->password()
|
||||||
->default(Str::password(15))
|
|
||||||
->required(fn (string $context): bool => $context === 'create')
|
->required(fn (string $context): bool => $context === 'create')
|
||||||
->dehydrated(fn ($state) => filled($state))
|
->dehydrated(fn ($state) => filled($state))
|
||||||
->maxLength(255),
|
->maxLength(255),
|
||||||
|
|||||||
@@ -22,11 +22,10 @@ class ClientAcademicJournalController extends Controller
|
|||||||
public function show(string $slug)
|
public function show(string $slug)
|
||||||
{
|
{
|
||||||
$journal = new ClientAcademicJournalListResource(AcademicJournal::query()->where('slug', '=', $slug)->firstOrFail());
|
$journal = new ClientAcademicJournalListResource(AcademicJournal::query()->where('slug', '=', $slug)->firstOrFail());
|
||||||
$journalIssues = JournalIssue::all()
|
$journalIssues = JournalIssue::where('academic_journal_id', $journal->id)
|
||||||
->groupBy('year_publication');
|
->groupBy('year_publication')->get();
|
||||||
|
|
||||||
$journals = [];
|
$journals = [];
|
||||||
$years = JournalIssue::select('year_publication')->distinct()->get();
|
|
||||||
|
|
||||||
foreach ($journalIssues as $year => $journalGroup) {
|
foreach ($journalIssues as $year => $journalGroup) {
|
||||||
$journals[] = [
|
$journals[] = [
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\FormEducation;
|
||||||
use App\Http\Resources\ClientEducationalGroupResource;
|
use App\Http\Resources\ClientEducationalGroupResource;
|
||||||
use App\Http\Resources\ScheduleResource;
|
use App\Http\Resources\ScheduleResource;
|
||||||
use App\Models\EducationalGroup;
|
use App\Models\EducationalGroup;
|
||||||
@@ -14,22 +15,71 @@ class ClientScheduleController extends Controller
|
|||||||
{
|
{
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
$educationalGroups = collect();
|
|
||||||
|
|
||||||
if (request()->filled('search')) {
|
|
||||||
$educationalGroups = ClientEducationalGroupResource::collection(EducationalGroup::query()
|
$educationalGroups = ClientEducationalGroupResource::collection(EducationalGroup::query()
|
||||||
->has('schedules')
|
->has('schedules')
|
||||||
->when(request()->input('search'), function ($query, $search) {
|
->when(request()->input('search'), function ($query, $search) {
|
||||||
$query->whereRaw('LOWER(title) like ?', ["%".strtolower($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('schedules')
|
||||||
->with('faculty')
|
->with('faculty')
|
||||||
->orderBy('title')
|
->orderBy('title')
|
||||||
->get());
|
->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)
|
public function show($id)
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ use App\Services\Vicon\EducationalProgram\EducationalProgramService;
|
|||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
@@ -37,14 +38,32 @@ class MainController extends Controller
|
|||||||
{
|
{
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$admissionCampaign = $this->getAdmissionCampaign();
|
// Кешируем данные на 60 минут (можно изменить время по необходимости)
|
||||||
$educations = $this->getEducationsData();
|
// $admissionCampaign = Cache::remember('admission_campaign', now()->addHour(), function () {
|
||||||
$sliders = $this->getActiveSliders();
|
// return $this->getAdmissionCampaign();
|
||||||
$posts = $this->getRecentPosts();
|
// });
|
||||||
$events = $this->getUpcomingEvents();
|
|
||||||
|
$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);
|
$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;
|
$seo = $page->seo ?? null;
|
||||||
|
|
||||||
return Inertia::render('Main', compact('posts', 'events', 'sliders', 'educations', 'seo'));
|
return Inertia::render('Main', compact('posts', 'events', 'sliders', 'educations', 'seo'));
|
||||||
@@ -82,11 +101,10 @@ class MainController extends Controller
|
|||||||
|
|
||||||
private function getActiveSliders()
|
private function getActiveSliders()
|
||||||
{
|
{
|
||||||
return ClientMainSliderResource::collection(
|
return (ClientMainSliderResource::collection(
|
||||||
MainSlider::where('is_active', true)
|
MainSlider::where('is_active', true)
|
||||||
->orderBy('sort', 'asc')
|
->orderBy('sort', 'asc')
|
||||||
->get()
|
->get()));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getRecentPosts()
|
private function getRecentPosts()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Http;
|
|||||||
|
|
||||||
use App\Http\Middleware\AccessCheck;
|
use App\Http\Middleware\AccessCheck;
|
||||||
use App\Http\Middleware\InternalRequestOnly;
|
use App\Http\Middleware\InternalRequestOnly;
|
||||||
|
use App\Http\Middleware\LimitPost;
|
||||||
use App\Http\Middleware\RateLimitCheckMiddleware;
|
use App\Http\Middleware\RateLimitCheckMiddleware;
|
||||||
use App\Http\Middleware\RateLimitCounterMiddleware;
|
use App\Http\Middleware\RateLimitCounterMiddleware;
|
||||||
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
use Illuminate\Foundation\Http\Kernel as HttpKernel;
|
||||||
@@ -84,6 +85,7 @@ class Kernel extends HttpKernel
|
|||||||
'rate.limited.check' => RateLimitCheckMiddleware::class,
|
'rate.limited.check' => RateLimitCheckMiddleware::class,
|
||||||
'ensure.browser' => InternalRequestOnly::class,
|
'ensure.browser' => InternalRequestOnly::class,
|
||||||
'superadmin' => \App\Http\Middleware\EnsureUserIsSuperadmin::class,
|
'superadmin' => \App\Http\Middleware\EnsureUserIsSuperadmin::class,
|
||||||
|
'limit.post' => LimitPost::class,
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -12,6 +13,6 @@ class Authenticate extends Middleware
|
|||||||
*/
|
*/
|
||||||
protected function redirectTo(Request $request): ?string
|
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;
|
use HasFactory;
|
||||||
|
|
||||||
protected $guarded = false;
|
protected $guarded = false;
|
||||||
|
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'settings' => 'array',
|
||||||
|
'image' => 'array',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ class Post extends Model
|
|||||||
return $this->belongsTo(Category::class);
|
return $this->belongsTo(Category::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function author() : BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public function seo()
|
public function seo()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class User extends Authenticatable implements FilamentUser
|
|||||||
{
|
{
|
||||||
if (config('filament-shield.dashboard_user.enabled', false)) {
|
if (config('filament-shield.dashboard_user.enabled', false)) {
|
||||||
FilamentShield::createRole(name: config('filament-shield.dashboard_user.name', 'dashboard_user'));
|
FilamentShield::createRole(name: config('filament-shield.dashboard_user.name', 'dashboard_user'));
|
||||||
|
FilamentShield::createRole(name: config('', 'editor'));
|
||||||
static::created(function (User $user) {
|
static::created(function (User $user) {
|
||||||
$user->assignRole(config('filament-shield.dashboard_user.name', 'dashboard_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)
|
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;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Models\MainSection;
|
use App\Models\MainSection;
|
||||||
|
use App\Models\MainSlider;
|
||||||
use App\Models\Page;
|
use App\Models\Page;
|
||||||
use App\Models\Post;
|
use App\Models\Post;
|
||||||
use App\Models\SubSection;
|
use App\Models\SubSection;
|
||||||
use App\Observers\MainSectionObserver;
|
use App\Observers\MainSectionObserver;
|
||||||
|
use App\Observers\MainSliderObserver;
|
||||||
use App\Observers\PageObserver;
|
use App\Observers\PageObserver;
|
||||||
use App\Observers\PostObserver;
|
use App\Observers\PostObserver;
|
||||||
use App\Observers\SubSectionObserver;
|
use App\Observers\SubSectionObserver;
|
||||||
|
use App\Services\App\Cache\MainSliderCacheService;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
|
use Filament\Support\Facades\FilamentView;
|
||||||
|
use Filament\Tables\View\TablesRenderHook;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Facades\URL;
|
use Illuminate\Support\Facades\URL;
|
||||||
use Illuminate\Support\Facades\Vite;
|
use Illuminate\Support\Facades\Vite;
|
||||||
@@ -34,16 +39,29 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
setlocale(LC_TIME, 'ru_RU.UTF-8');
|
self::setObserversByModel();
|
||||||
Page::observe(PageObserver::class);
|
self::setLocaleTime();
|
||||||
Post::observe(PostObserver::class);
|
|
||||||
MainSection::observe(MainSectionObserver::class);
|
|
||||||
SubSection::observe(SubSectionObserver::class);
|
|
||||||
Carbon::setLocale(config('app.locale'));
|
|
||||||
Model::preventLazyLoading(!app()->isProduction());
|
Model::preventLazyLoading(!app()->isProduction());
|
||||||
// URL::forceScheme('https');
|
// URL::forceScheme('https');
|
||||||
self::registerFilamentNavigationGroups();
|
self::registerFilamentNavigationGroups();
|
||||||
$this->loadViewsFrom(__DIR__.'/path/to/views', 'checkpoint');
|
$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()
|
->default()
|
||||||
->id('admin')
|
->id('admin')
|
||||||
->path('admin')
|
->path('admin')
|
||||||
->registration()
|
// ->registration()
|
||||||
->login()
|
->login()
|
||||||
->databaseNotifications()
|
->databaseNotifications()
|
||||||
->databaseNotificationsPolling('5s')
|
->databaseNotificationsPolling('5s')
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ class DashboardPanelProvider extends PanelProvider
|
|||||||
'primary' => Color::Amber,
|
'primary' => Color::Amber,
|
||||||
])
|
])
|
||||||
->login()
|
->login()
|
||||||
|
->databaseNotifications()
|
||||||
|
->databaseNotificationsPolling('5s')
|
||||||
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
|
||||||
->discoverPages(in: app_path('Filament/Dashboard/Pages'), for: 'App\\Filament\\Dashboard\\Pages')
|
->discoverPages(in: app_path('Filament/Dashboard/Pages'), for: 'App\\Filament\\Dashboard\\Pages')
|
||||||
->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);
|
$cacheKeyBySlug = md5($entity->slug);
|
||||||
$cacheKeyById = md5($entity->id);
|
$cacheKeyById = md5($entity->id);
|
||||||
|
|
||||||
Cache::forget($cacheKeyBySlug);
|
Cache::forget('post_' .$cacheKeyBySlug);
|
||||||
Cache::forget($cacheKeyById);
|
Cache::forget('post_' .$cacheKeyById);
|
||||||
|
|
||||||
|
$this->clearCacheByPrefix('recent_posts*');
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clearAllCacheByModel(): void
|
public function clearAllCacheByModel(): void
|
||||||
{
|
{
|
||||||
$this->clearCacheByPrefix('post_*');
|
$this->clearCacheByPrefix('post_*');
|
||||||
$this->clearCacheByPrefix('posts_*');
|
$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",
|
"symfony/filesystem": "^6.3",
|
||||||
"tightenco/ziggy": "^1.0",
|
"tightenco/ziggy": "^1.0",
|
||||||
"vkcom/vk-php-sdk": "^5.131",
|
"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": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.9.1",
|
"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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "25ebd7fca281d22d4022ea15464d54f2",
|
"content-hash": "0e0616783641aeb05f872887cfa59e66",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "anourvalar/eloquent-serialize",
|
"name": "anourvalar/eloquent-serialize",
|
||||||
@@ -10915,6 +10915,75 @@
|
|||||||
"source": "https://github.com/xvladxtremal/Faker-LoremFlickr/tree/v1.0.0"
|
"source": "https://github.com/xvladxtremal/Faker-LoremFlickr/tree/v1.0.0"
|
||||||
},
|
},
|
||||||
"time": "2021-01-18T02:03:35+00:00"
|
"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": [
|
"packages-dev": [
|
||||||
|
|||||||
@@ -28,13 +28,17 @@ return [
|
|||||||
'plugins' => 'advlist paste autoresize codesample directionality emoticons fullscreen hr image imagetools link lists media table toc wordcount',
|
'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',
|
'toolbar' => ' bold italic | numlist bullist | blockquote table hr | link | fullscreen',
|
||||||
'upload_directory' => null,
|
'upload_directory' => null,
|
||||||
|
'browser_spellcheck' => true,
|
||||||
'custom_configs' => [
|
'custom_configs' => [
|
||||||
'table_advtab' => false,
|
'table_advtab' => false,
|
||||||
'table_row_advtab' => false,
|
'table_row_advtab' => false,
|
||||||
'table_cell_advtab' => false,
|
'table_cell_advtab' => false,
|
||||||
'contextmenu' => '',
|
'contextmenu' => '',
|
||||||
'table_resize_bars' => false,
|
'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",
|
"flowbite": "^2.5.2",
|
||||||
"fslightbox": "^3.4.1",
|
"fslightbox": "^3.4.1",
|
||||||
"fslightbox-vue": "^2.1.3",
|
"fslightbox-vue": "^2.1.3",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"preline": "^1.9.0",
|
"preline": "^1.9.0",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"vue3-yandex-smartcaptcha": "^1.0.0"
|
"vue3-yandex-smartcaptcha": "^1.0.0"
|
||||||
@@ -2022,6 +2023,14 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"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": {
|
"node_modules/laravel-vite-plugin": {
|
||||||
"version": "0.8.1",
|
"version": "0.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.8.1.tgz",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"flowbite": "^2.5.2",
|
"flowbite": "^2.5.2",
|
||||||
"fslightbox": "^3.4.1",
|
"fslightbox": "^3.4.1",
|
||||||
"fslightbox-vue": "^2.1.3",
|
"fslightbox-vue": "^2.1.3",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"preline": "^1.9.0",
|
"preline": "^1.9.0",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"vue3-yandex-smartcaptcha": "^1.0.0"
|
"vue3-yandex-smartcaptcha": "^1.0.0"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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">
|
<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)]">
|
<div v-if="headerNavs.length > 0" class="sticky top-[100px] h-[calc(100vh-121px)]">
|
||||||
|
|||||||
@@ -45,8 +45,18 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
filter: debounce(function () {
|
filter: debounce(function () {
|
||||||
let url = new URL(window.location.href);
|
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('page');
|
||||||
url.searchParams.delete('direction[]');
|
|
||||||
let newUrl = url.toString();
|
let newUrl = url.toString();
|
||||||
this.$inertia.visit(newUrl, {
|
this.$inertia.visit(newUrl, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@@ -58,9 +68,18 @@ export default {
|
|||||||
}, 500),
|
}, 500),
|
||||||
clearFilter() {
|
clearFilter() {
|
||||||
let url = new URL(window.location.href);
|
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.direction = [];
|
||||||
this.searchTerm = ''; // Сбросить поле поиска
|
|
||||||
let newUrl = url.toString();
|
let newUrl = url.toString();
|
||||||
this.$inertia.visit(newUrl, {
|
this.$inertia.visit(newUrl, {
|
||||||
method: 'get',
|
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>
|
<template>
|
||||||
<div ref="sliderRef" class="relative z-0 min-h-[calc(100vh)] items-center">
|
<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
|
<img
|
||||||
alt="Thumbnail"
|
alt="Thumbnail"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
data-nimg="fill"
|
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"
|
sizes="100vw"
|
||||||
v-for="(slider, index) in slidersCarousel.data"
|
|
||||||
:key="index"
|
:key="index"
|
||||||
:src="'/storage/' + slider.image"
|
:src="'/storage/' + slider.image.url"
|
||||||
:class="{ 'opacity-1': currentIndex === index, 'opacity-0': currentIndex !== index }"
|
:class="{ 'opacity-1': currentIndex === index, 'opacity-0': currentIndex !== index }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<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 class="text-brand-primary mb-3 mt-2 text-3xl font-semibold tracking-tight text-white lg:text-5xl lg:leading-tight">
|
<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 }}
|
{{ item.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="mt-8 flex space-x-3 text-gray-500 mb-8">
|
<div v-if="item.content" class="mt-8 space-x-3 text-gray-500 mb-8">
|
||||||
<div class="flex flex-col gap-3 md:flex-row md:items-center">
|
<div class="gap-3 md:flex-row md:items-center">
|
||||||
<div class="flex gap-3">
|
<div class="gap-3">
|
||||||
<p class="text-gray-100 line-clamp-3"><a href="/author/erika-oliver">{{ item.content }}</a></p>
|
<p :class="`text-${item.settings.text_position}`" class="text-gray-100 line-clamp-3">
|
||||||
|
{{ item.content }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<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.link_text }}
|
{{ item.settings.link_text }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="slidersCarousel.data.length >= 2" class="mx-auto max-w-screen-md px-5">
|
<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" />
|
<DirectionFilter :direction_filter="direction_filter" :direction_studies="direction_studies" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,9 +69,6 @@ export default {
|
|||||||
// stroke_width: 1.5,
|
// stroke_width: 1.5,
|
||||||
// stroke: 'currentColor',
|
// 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"/>',
|
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',
|
viewBox: '0 0 24 24',
|
||||||
@@ -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>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<a @click="this.removeCookieBvi()" href="#" class="hover:opacity-70 py-3 open-bvi ">
|
||||||
|
|
||||||
<a href="#" class="hover:opacity-70 py-3 className">
|
|
||||||
<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"
|
<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"
|
stroke="currentColor"
|
||||||
class="w-6 h-6 duration-300 md:block hidden">
|
class="w-6 h-6 duration-300 md:block hidden">
|
||||||
@@ -167,6 +165,11 @@ export default {
|
|||||||
//
|
//
|
||||||
// return hasActiveInPages || hasActiveInSubSections;
|
// return hasActiveInPages || hasActiveInSubSections;
|
||||||
},
|
},
|
||||||
|
removeCookieBvi() {
|
||||||
|
if (this.getCookie('bvi_panelActive') === 'true') {
|
||||||
|
this.deleteCookiesWithPrefix('bvi')
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -36,72 +36,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="navbar-collapse-with-animation"
|
<div>
|
||||||
class="hs-collapse hidden overflow-hidden transition-all duration-300 basis-full grow lg:block">
|
<DesktopNavBar v-if="sections" :sections="sections" />
|
||||||
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -127,6 +63,7 @@ import BaseIcon from "@/Components/BaseComponents/BaseIcon.vue";
|
|||||||
import MobileNavbar from "@/Navbars/MobileNavbar.vue";
|
import MobileNavbar from "@/Navbars/MobileNavbar.vue";
|
||||||
import SearchModal from "@/Components/Modals/SearchModal.vue";
|
import SearchModal from "@/Components/Modals/SearchModal.vue";
|
||||||
import {defineAsyncComponent} from "vue";
|
import {defineAsyncComponent} from "vue";
|
||||||
|
import DesktopNavBar from "@/Navbars/DesktopNavBar.vue";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -141,6 +78,7 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
DesktopNavBar,
|
||||||
SearchModal,
|
SearchModal,
|
||||||
MobileNavbar,
|
MobileNavbar,
|
||||||
BaseIcon,
|
BaseIcon,
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
<MobileNavbar v-if="sections" :sections="sections" />
|
<MobileNavbar v-if="sections" :sections="sections" />
|
||||||
|
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ import SearchModal from "@/Components/Modals/SearchModal.vue";
|
|||||||
import DesktopNavBar from "@/Navbars/DesktopNavBar.vue";
|
import DesktopNavBar from "@/Navbars/DesktopNavBar.vue";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MainPageNavBar',
|
name: 'MainPageNavBar',
|
||||||
components: {
|
components: {
|
||||||
@@ -87,6 +89,7 @@ export default {
|
|||||||
headerFilter: false,
|
headerFilter: false,
|
||||||
underSliderHeader: this.sliderRef,
|
underSliderHeader: this.sliderRef,
|
||||||
bvi: null,
|
bvi: null,
|
||||||
|
isActiveBvi: null,
|
||||||
logos: {
|
logos: {
|
||||||
default: '/logos/white_ntspi_logo.svg',
|
default: '/logos/white_ntspi_logo.svg',
|
||||||
alternate: '/logos/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() {
|
handleScroll() {
|
||||||
if (typeof this.sliderRef === 'object') {
|
if (typeof this.sliderRef === 'object') {
|
||||||
const mainSlider = this.sliderRef;
|
const mainSlider = this.sliderRef;
|
||||||
@@ -133,30 +149,19 @@ export default {
|
|||||||
this.headerFilter = true
|
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() {
|
mounted() {
|
||||||
window.addEventListener('scroll', this.handleScroll)
|
window.addEventListener('scroll', this.handleScroll)
|
||||||
// if (this.getCookie('bvi_panelActive') === null) {
|
|
||||||
// this.bvi = new isvek.Bvi({
|
|
||||||
// target: '.className',
|
if (this.getCookie('bvi_panelActive') === null) {
|
||||||
// fontSize: 24,
|
this.iniBvi()
|
||||||
// theme: 'black',
|
}
|
||||||
// speech: false,
|
|
||||||
// reload: true,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('scroll', this.handleScroll)
|
window.removeEventListener('scroll', this.handleScroll)
|
||||||
|
|||||||
@@ -106,8 +106,11 @@ export default {
|
|||||||
<!-- Icon Block -->
|
<!-- Icon Block -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="flex justify-center items-center size-12 bg-gray-50 border border-gray-200 rounded-full mx-auto">
|
<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>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<h3 class="text-lg font-semibold text-gray-800">Направление подготовки</h3>
|
<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>
|
<p class="mt-1 text-gray-600">{{ program.data.directionStudy.name }} {{ program.data.directionStudy.code }}</p>
|
||||||
@@ -118,7 +121,9 @@ export default {
|
|||||||
<!-- Icon Block -->
|
<!-- Icon Block -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="flex justify-center items-center size-12 bg-gray-50 border border-gray-200 rounded-full mx-auto">
|
<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>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<h3 class="text-lg font-semibold text-gray-800">Срок обучения</h3>
|
<h3 class="text-lg font-semibold text-gray-800">Срок обучения</h3>
|
||||||
@@ -148,7 +153,9 @@ export default {
|
|||||||
<!-- Icon Block -->
|
<!-- Icon Block -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<div class="flex justify-center items-center size-12 bg-gray-50 border border-gray-200 rounded-full mx-auto">
|
<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>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<h3 class="text-lg font-semibold text-gray-800">Количество мест на прием</h3>
|
<h3 class="text-lg font-semibold text-gray-800">Количество мест на прием</h3>
|
||||||
|
|||||||
@@ -5,23 +5,34 @@ import { debounce } from "lodash";
|
|||||||
import { Link } from "@inertiajs/vue3";
|
import { Link } from "@inertiajs/vue3";
|
||||||
import MainPageNavBar from "@/Navbars/MainPageNavbar.vue";
|
import MainPageNavBar from "@/Navbars/MainPageNavbar.vue";
|
||||||
import BaseIcon from "@/Components/BaseComponents/BaseIcon.vue";
|
import BaseIcon from "@/Components/BaseComponents/BaseIcon.vue";
|
||||||
|
import ClientScheduleFilter from "@/Components/BuilderUi/Schedules/ClientScheduleFilter.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Index",
|
name: "Index",
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
searchInput: this.searchRequest,
|
searchInput: this.filters.search_filter.value,
|
||||||
favoriteGroups: JSON.parse(localStorage.getItem('favoriteGroups')) || [],
|
favoriteGroups: JSON.parse(localStorage.getItem('favoriteGroups')),
|
||||||
showFavorites: false,
|
showFavorites: false,
|
||||||
|
loading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {BaseIcon, MainPageNavBar, ClientFooterDown, MainNavbar, Link},
|
components: {ClientScheduleFilter, BaseIcon, MainPageNavBar, ClientFooterDown, MainNavbar, Link},
|
||||||
props: [
|
|
||||||
'educationalGroups',
|
props: {
|
||||||
'mainSections',
|
navigation: {
|
||||||
'searchRequest',
|
type: Object
|
||||||
'navigation',
|
},
|
||||||
],
|
filters: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
forms_education: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
schedulesByFaculty: {
|
||||||
|
type: Object
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
search: debounce(function () {
|
search: debounce(function () {
|
||||||
this.$inertia.reload({
|
this.$inertia.reload({
|
||||||
@@ -34,7 +45,7 @@ export default {
|
|||||||
});
|
});
|
||||||
}, 300),
|
}, 300),
|
||||||
toggleFavorite(group) {
|
toggleFavorite(group) {
|
||||||
const index = this.favoriteGroups.findIndex(g => g.id === group.id);
|
const index = this.favoriteGroups.findIndex(g => g === group);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
this.favoriteGroups.push(group);
|
this.favoriteGroups.push(group);
|
||||||
} else {
|
} else {
|
||||||
@@ -43,18 +54,65 @@ export default {
|
|||||||
localStorage.setItem('favoriteGroups', JSON.stringify(this.favoriteGroups));
|
localStorage.setItem('favoriteGroups', JSON.stringify(this.favoriteGroups));
|
||||||
},
|
},
|
||||||
isFavorite(group) {
|
isFavorite(group) {
|
||||||
return this.favoriteGroups.some(g => g.id === group.id);
|
return this.favoriteGroups.some(g => g === group);
|
||||||
},
|
},
|
||||||
toggleShowFavorites() {
|
toggleShowFavorites() {
|
||||||
this.showFavorites = !this.showFavorites;
|
this.loading = true; // Включаем состояние загрузки
|
||||||
},
|
|
||||||
},
|
if (this.filters.favorite_filter.value === null) {
|
||||||
computed: {
|
this.filterFavorite(() => {
|
||||||
filteredGroups() {
|
this.loading = false; // Выключаем состояние загрузки после завершения
|
||||||
if (this.showFavorites) {
|
});
|
||||||
return this.educationalGroups.data.filter(group => this.favoriteGroups.some(g => g.id === group.id));
|
} 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">
|
<article class="w-full min-w-0 mt-4 px-1 md:px-6">
|
||||||
<div class="relative overflow-hidden">
|
<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="max-w-[85rem] mx-auto sm:px-6 lg:px-8 py-10 sm:pb-12 sm:py-5">
|
||||||
<div class="text-center">
|
<div class="">
|
||||||
<h1 class="text-2xl sm:text-4xl font-bold text-gray-800 dark:text-gray-200">
|
<h1 class="text-2xl sm:text-4xl font-bold text-gray-800 text-center">
|
||||||
Расписание занятий
|
Расписание занятий
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
<div class="text-center">
|
<p class="mt-3 text-gray-600 text-center">
|
||||||
<p class="mt-3 text-gray-600 dark:text-gray-400">
|
|
||||||
Просто введите название группы
|
Просто введите название группы
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,7 +157,7 @@ export default {
|
|||||||
id="hs-search-article-1"
|
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"
|
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="Поиск"
|
placeholder="Поиск"
|
||||||
:disabled="showFavorites"
|
:disabled="filters.favorite_filter.value !== null"
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,24 +165,20 @@ export default {
|
|||||||
</form>
|
</form>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<button @click="toggleShowFavorites" type="button"
|
<button
|
||||||
:class="showFavorites ? 'bg-primaryBlue text-white hover:bg-secondDarkBlue' : 'text-gray-700 hover:bg-gray-100'"
|
@click="toggleShowFavorites"
|
||||||
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">
|
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"/>
|
<BaseIcon name="heart" class="shrink-0 size-4"/>
|
||||||
<span>Избранные расписания</span>
|
<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>
|
|
||||||
</button>
|
</button>
|
||||||
|
<ClientScheduleFilter :forms_educational="this.forms_education" :form-edu_filter="this.filters.form_education_filter" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,18 +186,20 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-auto max-w-2xl hs-accordion-group grid grid-cols-1 lg:grid-cols-2 gap-3">
|
<div class="mx-auto max-w-2xl hs-accordion-group grid gap-3">
|
||||||
<div v-if="!showFavorites">
|
<div class="space-y-4">
|
||||||
<transition-group name="fade">
|
<transition-group name="fade">
|
||||||
<template v-for="educationalGroup in filteredGroups" :key="educationalGroup.id">
|
<template v-for="(faculty, title) in schedulesByFaculty">
|
||||||
<div
|
<div class="">
|
||||||
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"
|
<h2 class="text-center text-gray-800 font-medium">{{ title }}</h2>
|
||||||
id="hs-active-bordered-heading-one">
|
<hr class="mt-2 mb-4">
|
||||||
<div class="flex">
|
<template v-for="educationalGroup in faculty" :key="educationalGroup.data.id">
|
||||||
<button @click="toggleFavorite(educationalGroup)">
|
<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">
|
<transition name="heart" mode="out-in">
|
||||||
<BaseIcon
|
<BaseIcon
|
||||||
v-if="isFavorite(educationalGroup)"
|
v-if="isFavorite(educationalGroup.data.id)"
|
||||||
name="heart_filled"
|
name="heart_filled"
|
||||||
class="shrink-0 size-4 text-red-500"
|
class="shrink-0 size-4 text-red-500"
|
||||||
key="filled"
|
key="filled"
|
||||||
@@ -158,33 +213,21 @@ export default {
|
|||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
</button>
|
</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 hover:text-gray-500 disabled:opacity-50 disabled:pointer-events-none" aria-expanded="false" aria-controls="hs-basic-active-bordered-collapse-one">
|
||||||
<button
|
<span class="flex items-center gap-x-2">{{ educationalGroup.data.title }}</span>
|
||||||
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"
|
<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">
|
||||||
aria-controls="hs-basic-active-bordered-collapse-one">
|
<path d="M5 12h14"></path>
|
||||||
|
<path d="M12 5v14"></path>
|
||||||
<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>
|
||||||
<svg class="hs-accordion-active:block hidden w-3.5 h-3.5"
|
<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">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<path d="M5 12h14"></path>
|
||||||
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="hs-basic-active-bordered-collapse-one"
|
|
||||||
class="hs-accordion-content hidden w-full overflow-hidden transition-[height] duration-300"
|
<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">
|
||||||
aria-labelledby="hs-active-bordered-heading-one">
|
|
||||||
<div class="pb-4 px-5 grid gap-3 grid-cols-1">
|
<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">
|
<template v-for="file in schedule.file">
|
||||||
<a :href="'storage/' + file.path" target="_blank"
|
<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">
|
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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
|
|||||||
@@ -4,14 +4,15 @@
|
|||||||
<MainPageNavBar :sections="$page.props.navigation" :slider-ref="sliderRef" />
|
<MainPageNavBar :sections="$page.props.navigation" :slider-ref="sliderRef" />
|
||||||
<ClientMainSlider @slider-mounted="setSliderRef" :slidersCarousel="sliders" />
|
<ClientMainSlider @slider-mounted="setSliderRef" :slidersCarousel="sliders" />
|
||||||
<section class="max-w-screen-xl w-full mx-auto px-4 py-3 pb-10">
|
<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>
|
<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">
|
<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">
|
<template v-for="post in posts.data" :key="post.id">
|
||||||
<ClientPost :post="post" />
|
<ClientPost :post="post" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<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">
|
<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 PageResourceList from "@/Components/BuilderUi/Pages/Blocks/PageResourceList.vue";
|
||||||
import AppHead from "@/Components/AppHead.vue";
|
import AppHead from "@/Components/AppHead.vue";
|
||||||
import ContactSectionBlock from "@/Components/BaseComponents/BaseBuilderUi/Blocks/Contacts/ContactSectionBlock.vue";
|
import ContactSectionBlock from "@/Components/BaseComponents/BaseBuilderUi/Blocks/Contacts/ContactSectionBlock.vue";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -238,6 +240,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
Cookies,
|
||||||
setSliderRef(ref) {
|
setSliderRef(ref) {
|
||||||
this.sliderRef = ref;
|
this.sliderRef = ref;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</Head>
|
</Head>
|
||||||
<MainPageNavBar class="border-b" :sections="$page.props.navigation"></MainPageNavBar>
|
<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">
|
<div class="flex flex-col h-screen">
|
||||||
<main class="flex-grow">
|
<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">
|
<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 { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
|
||||||
import {linksReform} from "@/mixins/LinksReform.js";
|
import {linksReform} from "@/mixins/LinksReform.js";
|
||||||
import YSmartCaptcha from 'vue3-yandex-smartcaptcha'
|
import YSmartCaptcha from 'vue3-yandex-smartcaptcha'
|
||||||
|
import cookieMixin from "@/mixins/cookieMixin.js";
|
||||||
|
|
||||||
// const appName = import.meta.env.VITE_APP_NAME || 'НТГСПИ';
|
// const appName = import.meta.env.VITE_APP_NAME || 'НТГСПИ';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ createInertiaApp({
|
|||||||
return createSSRApp({ render: () => h(App, props) })
|
return createSSRApp({ render: () => h(App, props) })
|
||||||
.use(plugin)
|
.use(plugin)
|
||||||
.mixin(linksReform)
|
.mixin(linksReform)
|
||||||
|
.mixin(cookieMixin)
|
||||||
.use(ZiggyVue)
|
.use(ZiggyVue)
|
||||||
.use(YSmartCaptcha, {
|
.use(YSmartCaptcha, {
|
||||||
siteKey: "ysc1_jxb2IcslgBuHgHbmmKY1F7Nvb8zYHuAeBgjoaTka6886f893",
|
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;
|
z-index: 10000000!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bvi-active {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ce-header {
|
.ce-header {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body .bvi-body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
*, html {
|
*, html {
|
||||||
scroll-padding: 6rem;
|
scroll-padding: 6rem;
|
||||||
scroll-behavior: smooth !important;
|
scroll-behavior: smooth !important;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Http\Controllers\ClientWidgetPageReferenceListController;
|
|||||||
use App\Http\Controllers\ClientWidgetPostController;
|
use App\Http\Controllers\ClientWidgetPostController;
|
||||||
use App\Http\Controllers\IconController;
|
use App\Http\Controllers\IconController;
|
||||||
use App\Http\Controllers\SearchController;
|
use App\Http\Controllers\SearchController;
|
||||||
|
use App\Http\Controllers\UpdateEduDataApiController;
|
||||||
use App\Http\Controllers\VkAuthController;
|
use App\Http\Controllers\VkAuthController;
|
||||||
use App\Http\Controllers\VkPostController;
|
use App\Http\Controllers\VkPostController;
|
||||||
use App\Models\AdmissionCampaign;
|
use App\Models\AdmissionCampaign;
|
||||||
@@ -25,6 +26,9 @@ Route::get('/getAcademicYear', function () {
|
|||||||
})->name('academic.year');
|
})->name('academic.year');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Route::middleware('ensure.browser')->group(function () {
|
Route::middleware('ensure.browser')->group(function () {
|
||||||
Route::get('/getNavigation', [NavigateController::class, 'index'])->name('client.main.navigate');
|
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::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', [VkAuthService::class, 'redirectToProvider'])->name('vk.login');
|
||||||
Route::get('/login/vk/callback', [VkAuthService::class, 'handleProviderCallback'])->name('vk.callback');
|
Route::get('/login/vk/callback', [VkAuthService::class, 'handleProviderCallback'])->name('vk.callback');
|
||||||
Route::get('/vk-get-token', [VkAuthService::class, 'getToken'])->name('vk.getToken');
|
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', [ClientDivisionController::class, 'index'])->name('client.division.index');
|
||||||
Route::get('/divisions/{slug}', [ClientDivisionController::class, 'show'])->name('client.division.show');
|
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');
|
Route::get('{path}', [PageController::class, 'render'])->where('path', '[0-9,a-z,/,-]+')->name('page.view');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user