diff --git a/app/Dto/MainSliderDTO.php b/app/Dto/MainSliderDTO.php
new file mode 100644
index 0000000..dd585d2
--- /dev/null
+++ b/app/Dto/MainSliderDTO.php
@@ -0,0 +1,58 @@
+ $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(),
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Filament/Components/Forms/PostForm.php b/app/Filament/Components/Forms/PostForm.php
index 1748395..962c4aa 100644
--- a/app/Filament/Components/Forms/PostForm.php
+++ b/app/Filament/Components/Forms/PostForm.php
@@ -13,6 +13,7 @@ use App\Models\PageReferenceList;
use App\Models\Post;
use Filament\Forms;
use Filament\Forms\Components\Builder;
+use Filament\Forms\Components\ColorPicker;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Grid;
@@ -97,7 +98,7 @@ class PostForm
]),
Tabs\Tab::make('Содержание новости')
->schema([
- ContentBuilderItem::getItem('content')
+ ContentBuilderItem::getItem('content')->required(),
]),
Tabs\Tab::make('Изображения')
->schema([
@@ -118,6 +119,51 @@ class PostForm
->multiple()
->directory('images'),
]),
+ Tabs\Tab::make('Добавление новости в слайдер')
+ ->schema([
+ Toggle::make('is_slider_enabled')
+ ->label('Добавить новый слайд')
+ ->live()
+ ->dehydrated(false)
+ ->default(false),
+ Section::make()
+ ->schema([
+ Forms\Components\TextInput::make('slide.title')
+ ->label('Заголовок слайда'),
+ Forms\Components\Textarea::make('slide.content')
+ ->label('Текст слайда'),
+ FileUpload::make('slide.image')
+ ->label('Изображение')
+ ->image()
+ ->optimize('webp')
+ ->resize(50)
+ ->disk('public')
+ ->directory('images')
+ ->imageEditor()
+ ->required(),
+ Grid::make(2)->schema([
+ Toggle::make('disable_link_text')
+ ->label('Отключить текст кнопки (ссылка будет открываться при нажатии на слайд)')
+ ->live()
+ ->inline(false)
+ ->dehydrated(false)
+ ->default(false),
+ Forms\Components\TextInput::make('slide.link_text')
+ ->default('Читать')
+ ->label('Текст кнопки')
+ ->disabled(fn (Forms\Get $get) => $get('disable_link_text')),
+ ]),
+
+ DateTimePicker::make('slide.end_time')
+ ->label('Слайд действует до')
+ ->native()
+ ->displayFormat('d/m/Y')
+ ->minDate(Carbon::now())
+ ->maxDate(Carbon::now()->addWeek()),
+ ])
+ ->disabled(fn (Forms\Get $get) => !$get('is_slider_enabled')) // Отключаем секцию, если Toggle выключен
+ ->hidden(fn (Forms\Get $get) => !$get('is_slider_enabled')), // Скрываем секцию, если Toggle выключен
+ ])->hidden(fn (string $context): bool => $context === 'edit'),
]),
])
]);
diff --git a/app/Filament/Resources/AcceptedInvitationResource.php b/app/Filament/Resources/AcceptedInvitationResource.php
index 76ebdc1..12471ed 100644
--- a/app/Filament/Resources/AcceptedInvitationResource.php
+++ b/app/Filament/Resources/AcceptedInvitationResource.php
@@ -10,6 +10,7 @@ use Filament\Forms;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
+use Filament\Tables\Columns\TextInputColumn;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
@@ -29,7 +30,7 @@ class AcceptedInvitationResource extends Resource implements HasShieldPermission
{
return $form
->schema([
- //
+
]);
}
@@ -37,7 +38,8 @@ class AcceptedInvitationResource extends Resource implements HasShieldPermission
{
return $table
->columns([
- //
+ Tables\Columns\TextColumn::make('receiver.name')->label('Приглашенный пользователь'),
+ TextInputColumn::make('post_limit')->label('Лимит постов')->default(0)->rules(['required', 'max:10', 'integer'])
])
->filters([
//
diff --git a/app/Filament/Resources/ContactWidgetResource.php b/app/Filament/Resources/ContactWidgetResource.php
index 607c2ff..4b06a7c 100644
--- a/app/Filament/Resources/ContactWidgetResource.php
+++ b/app/Filament/Resources/ContactWidgetResource.php
@@ -104,4 +104,16 @@ class ContactWidgetResource extends Resource
'edit' => Pages\EditContactWidget::route('/{record}/edit'),
];
}
+
+ public static function getPermissionPrefixes(): array
+ {
+ return [
+ 'view',
+ 'view_any',
+ 'create',
+ 'update',
+ 'delete',
+ 'delete_any',
+ ];
+ }
}
diff --git a/app/Filament/Resources/EducationalGroupResource.php b/app/Filament/Resources/EducationalGroupResource.php
index 718b83b..069f70c 100644
--- a/app/Filament/Resources/EducationalGroupResource.php
+++ b/app/Filament/Resources/EducationalGroupResource.php
@@ -2,6 +2,7 @@
namespace App\Filament\Resources;
+use App\Enums\FormEducation;
use App\Filament\Resources\EducationalGroupResource\Pages;
use App\Filament\Resources\EducationalGroupResource\RelationManagers;
use App\Models\EducationalGroup;
@@ -33,7 +34,9 @@ class EducationalGroupResource extends Resource
Forms\Components\Grid::make(2)->schema([
Forms\Components\TextInput::make('title')->label('Название группы')->required(),
Forms\Components\Select::make('faculty_id')->label('Факультет')->required()
- ->options(Faculty::all()->pluck('title', 'id'))
+ ->options(Faculty::all()->pluck('title', 'id')),
+ Forms\Components\Select::make('education_form_id')->label('Форма обучения')
+ ->options(FormEducation::class)
]),
]),
]);
diff --git a/app/Filament/Resources/MainSliderResource.php b/app/Filament/Resources/MainSliderResource.php
index cd97794..05cc612 100644
--- a/app/Filament/Resources/MainSliderResource.php
+++ b/app/Filament/Resources/MainSliderResource.php
@@ -10,15 +10,19 @@ use App\Models\Page;
use App\Models\Post;
use Filament\Forms;
use Filament\Forms\Components\ColorPicker;
+use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Toggle;
+use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Form;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletingScope;
+use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
+use Yepsua\Filament\Forms\Components\RangeSlider;
class MainSliderResource extends Resource
{
@@ -96,32 +100,75 @@ class MainSliderResource extends Resource
]),
]),
Forms\Components\Section::make('Слайдер')->schema([
- Forms\Components\TextInput::make('title')
- ->label('Заголовок слайда')
- ->required(),
- Forms\Components\Textarea::make('content')
- ->label('Текст слайда'),
- FileUpload::make('image')
- ->label('Изображение')
- ->image()
- ->optimize('webp')
- ->resize(50)
- ->disk('public')
- ->directory('images')
- ->imageEditor(),
- Forms\Components\Grid::make(2)->schema([
+
+ Forms\Components\Section::make('Информация слайда')->schema([
+ Forms\Components\TextInput::make('title')
+ ->label('Заголовок слайда'),
+ Forms\Components\Textarea::make('content')
+ ->label('Текст слайда'),
+ Forms\Components\Grid::make()->schema([
+ ColorPicker::make('color_theme')
+ ->label('Цвет текста')
+ ->default('#ffffff')
+ ->required(),
+ Forms\Components\ToggleButtons::make('settings.text_position')
+ ->options([
+ 'left' => 'Текст слева',
+ 'center' => 'Текст по середине',
+ 'right' => 'Текст справа'
+ ])
+ ->inline()->default('left')->grouped()
+ ->label('Позиция текста на слайде'),
+ ]),
+ Forms\Components\Grid::make()->schema([
+ Toggle::make('active_button')
+ ->label('Использовать кнопку для ссылки (Ссылка будет открываться при нажатии на слайд)')
+ ->inline(false)
+ ->default(true)
+ ->live()
+ ->dehydrated(false),
+ Forms\Components\TextInput::make('settings.link_text')
+ ->default('Читать')
+ ->label('Текст кнопки')
+ ->disabled(fn (Forms\Get $get) => !$get('active_button'))
+ ]),
+ ]),
+ Forms\Components\Section::make('Изображение')->schema([
+ FileUpload::make('image.url')
+ ->label('Изображение')
+ ->image()
+ ->optimize('webp')
+ ->resize(50)
+ ->disk('public')
+ ->directory('images')
+ ->imageEditor()
+ ->required(),
+ ToggleButtons::make('image.shading')->inline()->grouped()->label('Уровень затемнения изображения')->options([
+ '1' => 'Без затемнения',
+ '0.7' => 'Слабое затемнение',
+ '0.5' => 'Среднее затемнение',
+ '0.3' => 'Сильное затемнение',
+ ]),
+ ]),
+ Forms\Components\Section::make('Общая часть')->schema([
+ Forms\Components\Grid::make()->schema([
+ DateTimePicker::make('start_time')
+ ->label('Слайд начинается с')
+ ->native()
+ ->displayFormat('d/m/Y')
+ ->minDate(Carbon::now()->subDay())
+ ->maxDate(Carbon::now()->addWeek()),
+ DateTimePicker::make('end_time')
+ ->label('Слайд действует до')
+ ->native()
+ ->displayFormat('d/m/Y')
+ ->minDate(Carbon::now())
+ ->maxDate(Carbon::now()->addMonth()),
+ ]),
Forms\Components\TextInput::make('link')
->label('Ссылка кнопки')
->required(),
- Forms\Components\TextInput::make('link_text')
- ->default('Читать')
- ->label('Текст кнопки')
- ->required(),
]),
- ColorPicker::make('color_theme')
- ->label('Цвет текста')
- ->default('#ffffff')
- ->required(),
Toggle::make('is_active')->default(true)->label('Активный слайдер')->inline(false),
]),
diff --git a/app/Filament/Resources/PageResource.php b/app/Filament/Resources/PageResource.php
index a781110..46fbff1 100644
--- a/app/Filament/Resources/PageResource.php
+++ b/app/Filament/Resources/PageResource.php
@@ -44,8 +44,6 @@ class PageResource extends Resource
protected static ?string $pluralLabel = 'Страницы';
- public static ?string $label = 'Страница';
-
protected static ?string $navigationGroup = 'Структура приложения';
diff --git a/app/Filament/Resources/PostResource.php b/app/Filament/Resources/PostResource.php
index 8181a30..8935126 100644
--- a/app/Filament/Resources/PostResource.php
+++ b/app/Filament/Resources/PostResource.php
@@ -59,10 +59,12 @@ class PostResource extends Resource implements HasShieldPermissions
{
return $table
->columns([
- Tables\Columns\TextColumn::make('id')->sortable(),
+// Tables\Columns\TextColumn::make('id')->sortable(),
+ Tables\Columns\TextColumn::make('created_at')->label('Дата создания')->sortable(),
Tables\Columns\TextColumn::make('title')->label('Заголовок')->sortable()->searchable(),
Tables\Columns\TextColumn::make('status')->label('Статус')->sortable()->badge(),
Tables\Columns\TextColumn::make('publish_at')->label('Дата публикации')->sortable(),
+ Tables\Columns\TextColumn::make('author.name')->label('Автор')->sortable()->searchable(),
])->defaultSort('publish_at', 'desc')
->filters([
@@ -92,6 +94,8 @@ class PostResource extends Resource implements HasShieldPermissions
'index' => Pages\ListPosts::route('/'),
'create' => Pages\CreatePost::route('/create'),
'edit' => Pages\EditPost::route('/{record}/edit'),
+ 'view' => Pages\ViewPost::route('/{record}'),
+
];
}
diff --git a/app/Filament/Resources/PostResource/Pages/CreatePost.php b/app/Filament/Resources/PostResource/Pages/CreatePost.php
index b91a618..5de74b6 100644
--- a/app/Filament/Resources/PostResource/Pages/CreatePost.php
+++ b/app/Filament/Resources/PostResource/Pages/CreatePost.php
@@ -2,29 +2,15 @@
namespace App\Filament\Resources\PostResource\Pages;
+use App\Dto\MainSliderDTO;
use App\Enums\PostStatus;
use App\Filament\Resources\PostResource;
-use App\Jobs\CreateVkPost;
-use App\Models\Post;
-use App\Models\User;
-use App\Services\VK\VkService;
-use Carbon\Carbon;
-use Closure;
-use Filament\Actions;
-use Filament\Notifications\Actions\Action;
-use Filament\Notifications\Notification;
+use App\Services\Filament\Domain\Posts\PostDataProcessor;
+use App\Services\Filament\Domain\Posts\PostNotificationService;
+use App\Services\Filament\Domain\Posts\PostSeoGenerator;
+use App\Services\Filament\Domain\Posts\PostSliderService;
+use App\Services\Filament\Domain\Posts\VkPostPublisher;
use Filament\Resources\Pages\CreateRecord;
-use Illuminate\Database\Eloquent\Model;
-use Illuminate\Notifications\Messages\BroadcastMessage;
-use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
-use PhpParser\Node\Expr\AssignOp\Mod;
-use VK\Client\VKApiClient;
-use VK\OAuth\Scopes\VKOAuthGroupScope;
-use VK\OAuth\Scopes\VKOAuthUserScope;
-use VK\OAuth\VKOAuth;
-use VK\OAuth\VKOAuthDisplay;
-use VK\OAuth\VKOAuthResponseType;
class CreatePost extends CreateRecord
{
@@ -33,277 +19,68 @@ class CreatePost extends CreateRecord
protected array $seoData;
protected array $publicationAgreements;
+ protected array $slideData;
+
+ protected static array|string $routeMiddleware = ['limit.post'];
+
protected function mutateFormDataBeforeCreate(array $data): array
{
- $this->publicationAgreements = $data['publication'];
- unset($data['publication']);
- $this->seoData = $this->generateSeo($data);
- $data['preview_text'] = $this->setPreviewText($data);
- $data['publish_at'] = $this->setPublishDateTime($data['publish_setting']);
- unset($data['publish_setting']);
- $data['search_data'] = $this->generateSearchData($data['content']);
- $data['reading_time'] = $this->calculateReadingTime($data['search_data']);
- return $data;
+ $this->extractAdditionalData($data);
+ return $this->processPostData($data);
+ }
+
+ protected function extractAdditionalData(array &$data): void
+ {
+ $this->publicationAgreements = $data['publication'] ?? [];
+ $this->slideData = $data['slide'] ?? [];
+ unset($data['slide'], $data['publication']);
+ }
+
+ protected function processPostData(array $data): array
+ {
+ return (new PostDataProcessor())->process($data);
}
protected function afterCreate(): void
{
- $this->record->seo()->create($this->seoData);
- $this->sendNotify($this->record, auth()->user());
- $publish_date = ($this->record->publish_at > now()) ? Carbon::parse($this->record->publish_at)->timestamp : null;
- $this->postToSocialMedia($this->publicationAgreements, $this->record->content, $this->record->title, $publish_date);
+ $this->handleSlides();
+ $this->generateSeo();
+ $this->sendNotifications();
+ $this->publishToVk();
}
- private function generateSeo(array $data) : array
+ protected function handleSlides(): void
{
- $title = $data['title'];
- $rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
- if ($rowData === null) {
- $rowData = $this->getFirstBlockByName('paragraph', $data['content']);
- }
- if ($rowData !== null) {
- $description = html_entity_decode(strip_tags($rowData['data']['content']));
- } else {
- $description = null;
- } $image = ($data['preview'] !== null) ? $data['preview'] : null;
-
- return [
- 'title' => $title,
- 'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
- 'image' => $image,
- ];
- }
-
- private function setPreviewText(array $data) : string
- {
- $rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
- if ($rowData === null) {
- $rowData = $this->getFirstBlockByName('paragraph', $data['content']);
- }
- $preview_text = html_entity_decode(strip_tags($rowData['data']['content']));
- return Str::limit($preview_text, 160);
- }
-
- private function generateSearchData(array $data) : string
- {
- $result = "";
- foreach ($data as $block) {
- $result .= $this->getDataFromBlocks($block);
- }
- // Удаляем лишние пробелы и переносы строк
- $result = preg_replace('/\s+/', ' ', $result);
- $result = 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 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 .= "" . $block['data']['text'] . "";
- break;
- case "embded":
- $convertedHtml .= "
";
- break;
- case "paragraph":
- $convertedHtml .= "" . $block['data']['text'] . "
";
- break;
- case "delimiter":
- $convertedHtml .= "
";
- break;
- case "image":
- $convertedHtml .= "
" . $block['data']['caption'] . "";
- break;
- case "list":
- $convertedHtml .= "";
- foreach ($block['data']['items'] as $li) {
- $convertedHtml .= "- " . $li . "
";
- }
- $convertedHtml .= "
";
- break;
- case "table":
- $convertedHtml .= "";
- if ($block['data']['withHeadings']) {
- $convertedHtml .= "";
- foreach ($block['data']['content'][0] as $th) {
- $convertedHtml .= "| " . $th . " | ";
- }
- $convertedHtml .= "
";
- }
- $convertedHtml .= "";
- foreach ($block['data']['content'] as $row) {
- $convertedHtml .= "";
- foreach ($row as $td) {
- $convertedHtml .= "| " . $td . " | ";
- }
- $convertedHtml .= "
";
- }
- $convertedHtml .= "
";
- 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;
+ if (empty($this->slideData)) {
+ return;
}
- return $data;
+ $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();
}
- private function generateImageLinksToVK($images)
+
+ protected function generateSeo(): void
{
+ $seoData = (new PostSeoGenerator())->generate([
+ 'title' => $this->record->title,
+ 'content' => $this->record->content,
+ 'preview' => $this->record->preview,
+ ]);
+ $this->record->seo()->create($seoData);
+ }
- $imageUrls = array_map(function ($file) {
- return url(Storage::url($file)); // Добавляем домен
- }, $images);
+ protected function sendNotifications(): void
+ {
+ (new PostNotificationService())->send($this->record);
+ }
-
- return $imageUrls; // Возвращаем массив с полными URL изображений
+ protected function publishToVk(): void
+ {
+ (new VkPostPublisher())->publish($this->publicationAgreements, $this->record);
}
diff --git a/app/Filament/Resources/PostResource/Pages/EditPost.php b/app/Filament/Resources/PostResource/Pages/EditPost.php
index 930e4c8..e316761 100644
--- a/app/Filament/Resources/PostResource/Pages/EditPost.php
+++ b/app/Filament/Resources/PostResource/Pages/EditPost.php
@@ -2,14 +2,17 @@
namespace App\Filament\Resources\PostResource\Pages;
+use App\Dto\MainSliderDTO;
use App\Enums\PostStatus;
use App\Filament\Resources\PostResource;
-use App\Jobs\UpdateVkPost;
+use App\Services\Filament\Domain\Posts\PostDataProcessor;
+use App\Services\Filament\Domain\Posts\PostNotificationService;
+use App\Services\Filament\Domain\Posts\PostSeoGenerator;
+use App\Services\Filament\Domain\Posts\PostSliderService;
+use App\Services\Filament\Domain\Posts\VkPostPublisher;
use Carbon\Carbon;
use Filament\Actions;
use Filament\Resources\Pages\EditRecord;
-use Illuminate\Support\Facades\Storage;
-use Illuminate\Support\Str;
class EditPost extends EditRecord
{
@@ -18,224 +21,75 @@ class EditPost extends EditRecord
protected array $seoData;
protected array $publicationAgreements;
+ protected array $slideData;
+
protected function mutateFormDataBeforeSave(array $data): array
{
- $this->seoData = $this->generateSeo($data);
- $this->publicationAgreements = $data['publication'];
- unset($data['publication']);
- unset($data['publish_setting']);
- $data['preview_text'] = $this->setPreviewText($data);
- $data['publish_at'] = $this->setPublishDateTime($data['status'], $this->record->publish_at);
- $data['search_data'] = $this->generateSearchData($data['content']);
- $data['reading_time'] = $this->calculateReadingTime($data['search_data']);
+ $this->extractAdditionalData($data);
+ return $this->processPostData($data);
+ }
- return $data;
+ protected function extractAdditionalData(array &$data): void
+ {
+ $this->publicationAgreements = $data['publication'] ?? [];
+ $this->slideData = $data['slide'] ?? [];
+ unset($data['slide'], $data['publication']);
+ }
+
+ protected function processPostData(array $data): array
+ {
+ return (new PostDataProcessor())->process($data);
}
protected function afterSave(): void
{
- $this->record->seo()->update($this->seoData);
- $publish_date = ($this->record->publish_at > now()) ? Carbon::parse($this->record->publish_at)->timestamp : null;
- $this->postToSocialMedia($this->publicationAgreements, $this->record->content, $this->record->title, $publish_date);
+ $this->handleSlides();
+ $this->generateSeo();
+ $this->sendNotifications();
+ $this->publishToVk();
}
- private function setPreviewText(array $data) : string|null
+
+ protected function handleSlides(): void
{
- $rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
- if ($rowData === null) {
- $rowData = $this->getFirstBlockByName('paragraph', $data['content']);
- }
- if ($rowData !== null) {
- $preview_text = html_entity_decode(strip_tags($rowData['data']['content']));
- return Str::limit($preview_text, 160);
- } else {
- $preview_text = null;
- return $preview_text;
+ if (empty($this->slideData)) {
+ return;
}
+
+ $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();
}
- private function getBlockBySeoActiveState(string $name, array $content) : array|null
+ protected function generateSeo(): void
{
- $data = [];
- foreach ($content as $block) {
- if ($block['type'] === $name) {
- $data[] = $block;
- }
- }
- $block = null;
- foreach ($data as $item) {
- if ($item['data']['seo_active'] === true) {
- $block = $item;
- }
- }
- return $block;
+ $seoData = (new PostSeoGenerator())->generate([
+ 'title' => $this->record->title,
+ 'content' => $this->record->content,
+ 'preview' => $this->record->preview,
+ ]);
+ $this->record->seo()->update($seoData);
}
- private function generateSeo(array $data) : array
- {
- $title = $data['title'];
- $rowData = $this->getBlockBySeoActiveState('paragraph', $data['content']);
- if ($rowData === null) {
- $rowData = $this->getFirstBlockByName('paragraph', $data['content']);
- }
- if ($rowData !== null) {
- $description = html_entity_decode(strip_tags($rowData['data']['content']));
- } else {
- $description = null;
- }
- $image = ($this->record->preview !== null) ? $this->record->preview : null;
-
- return [
- 'title' => $title,
- 'description' => Str::limit(htmlspecialchars($description, ENT_QUOTES, 'UTF-8'), 160),
- 'image' => $image,
- ];
- }
-
- private function setPublishDateTime($status, $publish_at)
- {
- if ($publish_at !== null) {
- return $publish_at;
- }
- return PostStatus::tryFrom($status) === PostStatus::PUBLISHED ? Carbon::now() : null;
- }
- private function getDataFromBlocks($block) : string
- {
- $data = "";
- switch ($block['type']) {
- case 'paragraph':
- $data .= strip_tags($block['data']['content']) . " ";
- break;
- case 'heading':
- $data .= strip_tags($block['data']['content']) . " ";
- break;
- case 'files':
- foreach ($block['data']['file'] as $file) {
- $data .= $file['title'] . " ";
- }
- break;
- case 'person':
- $data .= $block['data']['name'] . " ";
- break;
- case 'stepper':
- $data .= $block['data']['step_name'] . " ";
- foreach ($block['data']['steps'] as $step) {
- $data .= $step['title'] . " ";
- $data .= strip_tags($step['content']) . " ";
- }
- break;
- case 'tabs':
- foreach ($block['data']['tab'] as $item) {
- foreach ($item['content'] as $block) {
- $data .= $this->getDataFromBlocks($block);
- };
- };
- break;
-
- }
- return $data;
- }
-
- private function calculateReadingTime(string $text): int
- {
-
- // Calculate the number of words in the text
- $wordCount = str_word_count($text,0,"АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя");
-
-
-
-
- // Calculate the average reading speed in words per minute
- $wordsPerMinute = 120; // You can adjust this value based on your desired reading speed
-
- // Calculate the reading time in minutes
- $readingTime = $wordCount / $wordsPerMinute;
-
- // Round the reading time to the nearest integer
- $readingTime = round($readingTime);
-
-
- return $readingTime;
- }
-
- private function generateSearchData(array $data) : string
- {
- $result = "";
- foreach ($data as $block) {
- $result .= $this->getDataFromBlocks($block);
- }
- // Удаляем лишние пробелы и переносы строк
- $result = preg_replace('/\s+/', ' ', $result);
- $result = htmlspecialchars(trim($result));
-
- return strtolower($result);
- }
- private function getFirstBlockByName(string $name, array $content) : array|null
- {
- $data = null;
- foreach ($content as $block) {
- $data = ($block['type'] === $name) ? $block : null;
- break;
- }
- return $data;
- }
-
- private function generateContentToVK($block) : string
- {
- $data = "";
-
- switch ($block['type']) {
- case 'paragraph':
- // Удаляем все HTML-теги и заменяем закрывающие теги p и h2 на двойной отступ
- $content = preg_replace('/<\/(p|h2)>/', "\n\n", $block['data']['content']);
- $data .= html_entity_decode(strip_tags($content));
- break;
-
- case 'heading':
- // Удаляем теги заголовка и добавляем двойной отступ
- $data .= $block['data']['content'] . "\n\n";
- break;
- }
-
- return $data;
- }
-
- private function postToSocialMedia($settings, $content, $title, $publish_date) : void
+ protected function sendNotifications(): void
{
+ // Отправляем уведомления
+ $notificationService = new PostNotificationService();
if ($this->record->status === PostStatus::PUBLISHED) {
- if ($settings['vk']) {
- $text = "";
- foreach ($content as $block) {
- $text .= $this->generateContentToVK($block);
- }
-
- $images = $this->generateImageLinksToVK($this->record->images);
-
- $post_id = $this->record->id;
-
-
- dispatch(new UpdateVkPost($title, $text, $images, $post_id, $publish_date));
- }
+ $notificationService->sendSuccessNotification($this->record);
+ } elseif ($this->record->status === PostStatus::REJECTED) {
+ $notificationService->sendDeniedNotification($this->record);
}
}
- private function generateImageLinksToVK($images)
+ protected function publishToVk(): void
{
-
- $imageUrls = array_map(function ($file) {
- return url(Storage::url($file)); // Добавляем домен
- }, $images);
-
-
- return $imageUrls; // Возвращаем массив с полными URL изображений
+ (new VkPostPublisher())->publish($this->publicationAgreements, $this->record);
}
-
-
-
-
-
protected function getHeaderActions(): array
{
return [
diff --git a/app/Filament/Resources/UserResource.php b/app/Filament/Resources/UserResource.php
index 3400404..3a169e0 100644
--- a/app/Filament/Resources/UserResource.php
+++ b/app/Filament/Resources/UserResource.php
@@ -50,7 +50,6 @@ class UserResource extends Resource implements HasShieldPermissions
Forms\Components\TextInput::make('password')
->label('Пароль')
->password()
- ->default(Str::password(15))
->required(fn (string $context): bool => $context === 'create')
->dehydrated(fn ($state) => filled($state))
->maxLength(255),
diff --git a/app/Http/Controllers/ClientAcademicJournalController.php b/app/Http/Controllers/ClientAcademicJournalController.php
index 396cefb..12658eb 100644
--- a/app/Http/Controllers/ClientAcademicJournalController.php
+++ b/app/Http/Controllers/ClientAcademicJournalController.php
@@ -22,11 +22,10 @@ class ClientAcademicJournalController extends Controller
public function show(string $slug)
{
$journal = new ClientAcademicJournalListResource(AcademicJournal::query()->where('slug', '=', $slug)->firstOrFail());
- $journalIssues = JournalIssue::all()
- ->groupBy('year_publication');
+ $journalIssues = JournalIssue::where('academic_journal_id', $journal->id)
+ ->groupBy('year_publication')->get();
$journals = [];
- $years = JournalIssue::select('year_publication')->distinct()->get();
foreach ($journalIssues as $year => $journalGroup) {
$journals[] = [
diff --git a/app/Http/Controllers/ClientScheduleController.php b/app/Http/Controllers/ClientScheduleController.php
index 8e262dc..52ce791 100644
--- a/app/Http/Controllers/ClientScheduleController.php
+++ b/app/Http/Controllers/ClientScheduleController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers;
+use App\Enums\FormEducation;
use App\Http\Resources\ClientEducationalGroupResource;
use App\Http\Resources\ScheduleResource;
use App\Models\EducationalGroup;
@@ -14,22 +15,71 @@ class ClientScheduleController extends Controller
{
public function index(Request $request)
{
- $educationalGroups = collect();
+ $educationalGroups = ClientEducationalGroupResource::collection(EducationalGroup::query()
+ ->has('schedules')
+ ->when(request()->input('search'), function ($query, $search) {
+ $query->whereRaw('LOWER(title) like ?', ["%".strtolower($search)."%"]);
+ })
+ ->when(request()->input('favorite'), function ($query, $favorite) {
+ $query->whereHas('schedules', function ($query) use ($favorite) {
+ $query->whereIn('id', $favorite);
+ });
+ })
+ ->when(request()->input('form'), function ($query, $form) {
+ $query->where('education_form_id', FormEducation::fromName($form)->value);
- if (request()->filled('search')) {
- $educationalGroups = ClientEducationalGroupResource::collection(EducationalGroup::query()
- ->has('schedules')
- ->when(request()->input('search'), function ($query, $search) {
- $query->whereRaw('LOWER(title) like ?', ["%".strtolower($search)."%"]);
- })
- ->with('schedules')
- ->with('faculty')
- ->orderBy('title')
- ->get());
+ })
+
+ ->with('schedules')
+ ->with('faculty')
+ ->orderBy('title')
+ ->get());
+
+ $schedulesByFaculty = $educationalGroups->groupBy(function ($group) {
+ return $group->faculty->title; // Предполагаем, что у факультета есть поле 'name'
+ });
+
+ $schedulesByFaculty = $schedulesByFaculty->toArray();
+
+ if ($request->has('favorite') && empty($request->input('favorite'))) {
+ $schedulesByFaculty = [];
}
- $searchRequest = request()->input('search');
- return Inertia::render('Client/Schedules/Index', compact('educationalGroups', 'searchRequest'));
+
+
+
+
+
+ $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)
diff --git a/app/Http/Controllers/MainController.php b/app/Http/Controllers/MainController.php
index fbdb383..d4df601 100644
--- a/app/Http/Controllers/MainController.php
+++ b/app/Http/Controllers/MainController.php
@@ -27,6 +27,7 @@ use App\Services\Vicon\EducationalProgram\EducationalProgramService;
use Carbon\Carbon;
use DateTime;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http;
@@ -37,14 +38,32 @@ class MainController extends Controller
{
public function index()
{
- $admissionCampaign = $this->getAdmissionCampaign();
- $educations = $this->getEducationsData();
- $sliders = $this->getActiveSliders();
- $posts = $this->getRecentPosts();
- $events = $this->getUpcomingEvents();
+ // Кешируем данные на 60 минут (можно изменить время по необходимости)
+// $admissionCampaign = Cache::remember('admission_campaign', now()->addHour(), function () {
+// return $this->getAdmissionCampaign();
+// });
+
+ $educations = Cache::remember('educations_data', now()->addHour(), function () {
+ return $this->getEducationsData();
+ });
+
+ $sliders = Cache::remember('active_sliders', now()->addHour(), function () {
+ return $this->getActiveSliders();
+ });
+
+ $posts = Cache::remember('recent_posts', now()->addHour(), function () {
+ return $this->getRecentPosts();
+ });
+
+ $events = Cache::remember('upcoming_events', now()->addHour(), function () {
+ return $this->getUpcomingEvents();
+ });
$path = route('index', null, false);
- $page = Page::where('path', $path)->first();
+ $page = Cache::remember('page_' . $path, now()->addHour(), function () use ($path) {
+ return Page::where('path', $path)->first();
+ });
+
$seo = $page->seo ?? null;
return Inertia::render('Main', compact('posts', 'events', 'sliders', 'educations', 'seo'));
@@ -82,11 +101,10 @@ class MainController extends Controller
private function getActiveSliders()
{
- return ClientMainSliderResource::collection(
+ return (ClientMainSliderResource::collection(
MainSlider::where('is_active', true)
->orderBy('sort', 'asc')
- ->get()
- );
+ ->get()));
}
private function getRecentPosts()
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index d2c0eb7..30caad4 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -4,6 +4,7 @@ namespace App\Http;
use App\Http\Middleware\AccessCheck;
use App\Http\Middleware\InternalRequestOnly;
+use App\Http\Middleware\LimitPost;
use App\Http\Middleware\RateLimitCheckMiddleware;
use App\Http\Middleware\RateLimitCounterMiddleware;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
@@ -84,6 +85,7 @@ class Kernel extends HttpKernel
'rate.limited.check' => RateLimitCheckMiddleware::class,
'ensure.browser' => InternalRequestOnly::class,
'superadmin' => \App\Http\Middleware\EnsureUserIsSuperadmin::class,
+ 'limit.post' => LimitPost::class,
);
}
diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php
index d4ef644..8d003ca 100644
--- a/app/Http/Middleware/Authenticate.php
+++ b/app/Http/Middleware/Authenticate.php
@@ -2,6 +2,7 @@
namespace App\Http\Middleware;
+use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
@@ -12,6 +13,6 @@ class Authenticate extends Middleware
*/
protected function redirectTo(Request $request): ?string
{
- return $request->expectsJson() ? null : route('login');
+ return $request->expectsJson() ? null : throw new AuthorizationException('Forbidden', 403);
}
}
diff --git a/app/Http/Middleware/LimitPost.php b/app/Http/Middleware/LimitPost.php
new file mode 100644
index 0000000..d4030a6
--- /dev/null
+++ b/app/Http/Middleware/LimitPost.php
@@ -0,0 +1,28 @@
+user()->receivedInvitation === null) {
+ return $next($request);
+ }
+ if (auth()->user()->receivedInvitation->post_limit > 0) {
+ return $next($request);
+ } else {
+ throw new HttpException(403, 'Лимит постов исчерпан');
+ }
+ }
+}
diff --git a/app/Models/MainSlider.php b/app/Models/MainSlider.php
index 76fef6c..832fa6e 100644
--- a/app/Models/MainSlider.php
+++ b/app/Models/MainSlider.php
@@ -10,4 +10,10 @@ class MainSlider extends Model
use HasFactory;
protected $guarded = false;
+
+
+ protected $casts = [
+ 'settings' => 'array',
+ 'image' => 'array',
+ ];
}
diff --git a/app/Models/Post.php b/app/Models/Post.php
index 1a8eba8..8a88f8b 100644
--- a/app/Models/Post.php
+++ b/app/Models/Post.php
@@ -34,6 +34,11 @@ class Post extends Model
return $this->belongsTo(Category::class);
}
+ public function author() : BelongsTo
+ {
+ return $this->belongsTo(User::class, 'user_id');
+ }
+
public function seo()
{
diff --git a/app/Models/User.php b/app/Models/User.php
index f7a35d8..615098f 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -94,6 +94,7 @@ class User extends Authenticatable implements FilamentUser
{
if (config('filament-shield.dashboard_user.enabled', false)) {
FilamentShield::createRole(name: config('filament-shield.dashboard_user.name', 'dashboard_user'));
+ FilamentShield::createRole(name: config('', 'editor'));
static::created(function (User $user) {
$user->assignRole(config('filament-shield.dashboard_user.name', 'dashboard_user'));
});
diff --git a/app/Observers/MainSliderObserver.php b/app/Observers/MainSliderObserver.php
new file mode 100644
index 0000000..02dd4e7
--- /dev/null
+++ b/app/Observers/MainSliderObserver.php
@@ -0,0 +1,75 @@
+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();
+ }
+ }
+}
diff --git a/app/Observers/PostObserver.php b/app/Observers/PostObserver.php
index 1d6cfcc..2c9eda3 100644
--- a/app/Observers/PostObserver.php
+++ b/app/Observers/PostObserver.php
@@ -30,7 +30,7 @@ class PostObserver
*/
public function updated(Post $post)
{
- $this->postCacheService->clearCache($post);
+ $this->postCacheService->clearAllCacheByModel();
}
/**
diff --git a/app/Policies/ContactWidgetPolicy.php b/app/Policies/ContactWidgetPolicy.php
new file mode 100644
index 0000000..01afafd
--- /dev/null
+++ b/app/Policies/ContactWidgetPolicy.php
@@ -0,0 +1,108 @@
+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');
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 60b0a8d..986a946 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -3,15 +3,20 @@
namespace App\Providers;
use App\Models\MainSection;
+use App\Models\MainSlider;
use App\Models\Page;
use App\Models\Post;
use App\Models\SubSection;
use App\Observers\MainSectionObserver;
+use App\Observers\MainSliderObserver;
use App\Observers\PageObserver;
use App\Observers\PostObserver;
use App\Observers\SubSectionObserver;
+use App\Services\App\Cache\MainSliderCacheService;
use Carbon\Carbon;
use Filament\Facades\Filament;
+use Filament\Support\Facades\FilamentView;
+use Filament\Tables\View\TablesRenderHook;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Vite;
@@ -34,16 +39,29 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(): void
{
- setlocale(LC_TIME, 'ru_RU.UTF-8');
- Page::observe(PageObserver::class);
- Post::observe(PostObserver::class);
- MainSection::observe(MainSectionObserver::class);
- SubSection::observe(SubSectionObserver::class);
- Carbon::setLocale(config('app.locale'));
+ self::setObserversByModel();
+ self::setLocaleTime();
Model::preventLazyLoading(!app()->isProduction());
// URL::forceScheme('https');
self::registerFilamentNavigationGroups();
$this->loadViewsFrom(__DIR__.'/path/to/views', 'checkpoint');
+
+ FilamentView::registerRenderHook(TablesRenderHook::TOOLBAR_REORDER_TRIGGER_AFTER, function () {
+ (new MainSliderCacheService())->clearAllCacheByModel();
+ });
+ }
+
+ private static function setObserversByModel() : void {
+ Page::observe(PageObserver::class);
+ Post::observe(PostObserver::class);
+ MainSection::observe(MainSectionObserver::class);
+ SubSection::observe(SubSectionObserver::class);
+ MainSlider::observe(MainSliderObserver::class);
+ }
+
+ private static function setLocaleTime() : void {
+ setlocale(LC_TIME, 'ru_RU.UTF-8');
+ Carbon::setLocale(config('app.locale'));
}
diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php
index d601848..84edc65 100644
--- a/app/Providers/Filament/AdminPanelProvider.php
+++ b/app/Providers/Filament/AdminPanelProvider.php
@@ -31,7 +31,7 @@ class AdminPanelProvider extends PanelProvider
->default()
->id('admin')
->path('admin')
- ->registration()
+// ->registration()
->login()
->databaseNotifications()
->databaseNotificationsPolling('5s')
diff --git a/app/Providers/Filament/DashboardPanelProvider.php b/app/Providers/Filament/DashboardPanelProvider.php
index 3d48b55..17a409a 100644
--- a/app/Providers/Filament/DashboardPanelProvider.php
+++ b/app/Providers/Filament/DashboardPanelProvider.php
@@ -29,6 +29,8 @@ class DashboardPanelProvider extends PanelProvider
'primary' => Color::Amber,
])
->login()
+ ->databaseNotifications()
+ ->databaseNotificationsPolling('5s')
->discoverResources(in: app_path('Filament/Resources'), for: 'App\\Filament\\Resources')
->discoverPages(in: app_path('Filament/Dashboard/Pages'), for: 'App\\Filament\\Dashboard\\Pages')
->pages([
diff --git a/app/Services/App/Cache/MainSliderCacheService.php b/app/Services/App/Cache/MainSliderCacheService.php
new file mode 100644
index 0000000..8e9087c
--- /dev/null
+++ b/app/Services/App/Cache/MainSliderCacheService.php
@@ -0,0 +1,48 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/app/Services/App/Cache/PostCacheService.php b/app/Services/App/Cache/PostCacheService.php
index c345950..abb2dbb 100644
--- a/app/Services/App/Cache/PostCacheService.php
+++ b/app/Services/App/Cache/PostCacheService.php
@@ -18,14 +18,18 @@ class PostCacheService extends AbstractCacheService implements CacheInterface
$cacheKeyBySlug = md5($entity->slug);
$cacheKeyById = md5($entity->id);
- Cache::forget($cacheKeyBySlug);
- Cache::forget($cacheKeyById);
+ Cache::forget('post_' .$cacheKeyBySlug);
+ Cache::forget('post_' .$cacheKeyById);
+
+ $this->clearCacheByPrefix('recent_posts*');
+
}
public function clearAllCacheByModel(): void
{
$this->clearCacheByPrefix('post_*');
$this->clearCacheByPrefix('posts_*');
+ $this->clearCacheByPrefix('recent_posts*');
}
/**
diff --git a/app/Services/Filament/Domain/Posts/PostDataProcessor.php b/app/Services/Filament/Domain/Posts/PostDataProcessor.php
new file mode 100644
index 0000000..e6e35aa
--- /dev/null
+++ b/app/Services/Filament/Domain/Posts/PostDataProcessor.php
@@ -0,0 +1,187 @@
+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;
+ }
+}
\ No newline at end of file
diff --git a/app/Services/Filament/Domain/Posts/PostNotificationService.php b/app/Services/Filament/Domain/Posts/PostNotificationService.php
new file mode 100644
index 0000000..4b3dfe8
--- /dev/null
+++ b/app/Services/Filament/Domain/Posts/PostNotificationService.php
@@ -0,0 +1,80 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/app/Services/Filament/Domain/Posts/PostSeoGenerator.php b/app/Services/Filament/Domain/Posts/PostSeoGenerator.php
new file mode 100644
index 0000000..b1584f0
--- /dev/null
+++ b/app/Services/Filament/Domain/Posts/PostSeoGenerator.php
@@ -0,0 +1,111 @@
+ $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;
+ }
+}
\ No newline at end of file
diff --git a/app/Services/Filament/Domain/Posts/PostSliderService.php b/app/Services/Filament/Domain/Posts/PostSliderService.php
new file mode 100644
index 0000000..0d8244b
--- /dev/null
+++ b/app/Services/Filament/Domain/Posts/PostSliderService.php
@@ -0,0 +1,59 @@
+ $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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/Services/Filament/Domain/Posts/VkPostPublisher.php b/app/Services/Filament/Domain/Posts/VkPostPublisher.php
new file mode 100644
index 0000000..88958e6
--- /dev/null
+++ b/app/Services/Filament/Domain/Posts/VkPostPublisher.php
@@ -0,0 +1,66 @@
+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);
+ }
+}
diff --git a/composer.json b/composer.json
index b1b593b..6f1a7f0 100644
--- a/composer.json
+++ b/composer.json
@@ -31,7 +31,8 @@
"symfony/filesystem": "^6.3",
"tightenco/ziggy": "^1.0",
"vkcom/vk-php-sdk": "^5.131",
- "xvladqt/faker-lorem-flickr": "^1.0"
+ "xvladqt/faker-lorem-flickr": "^1.0",
+ "yepsua/filament-range-field": "^0.3.4"
},
"require-dev": {
"fakerphp/faker": "^1.9.1",
diff --git a/composer.lock b/composer.lock
index 915a9d7..3a43256 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "25ebd7fca281d22d4022ea15464d54f2",
+ "content-hash": "0e0616783641aeb05f872887cfa59e66",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -10915,6 +10915,75 @@
"source": "https://github.com/xvladxtremal/Faker-LoremFlickr/tree/v1.0.0"
},
"time": "2021-01-18T02:03:35+00:00"
+ },
+ {
+ "name": "yepsua/filament-range-field",
+ "version": "v0.3.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yepsua/filament-range-field.git",
+ "reference": "fc29d84819960b3ad12354c5473649d7989f1883"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yepsua/filament-range-field/zipball/fc29d84819960b3ad12354c5473649d7989f1883",
+ "reference": "fc29d84819960b3ad12354c5473649d7989f1883",
+ "shasum": ""
+ },
+ "require": {
+ "filament/filament": "^3.0",
+ "illuminate/contracts": "^8.0|^9.0|^10.0",
+ "php": "^8.0",
+ "spatie/laravel-package-tools": "^1.9.2"
+ },
+ "require-dev": {
+ "nunomaduro/collision": "^6.0",
+ "nunomaduro/larastan": "^2.0.1",
+ "orchestra/testbench": "^7.0",
+ "pestphp/pest": "^1.21",
+ "pestphp/pest-plugin-laravel": "^1.1",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan-deprecation-rules": "^1.0",
+ "phpstan/phpstan-phpunit": "^1.0",
+ "phpunit/phpunit": "^9.5",
+ "spatie/laravel-ray": "^1.26"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Yepsua\\Filament\\FilamentRangeFieldServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Yepsua\\Filament\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Omar Yepez",
+ "email": "oyepez003@gmail.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "The missing range/slider field for the Filament forms.",
+ "homepage": "https://github.com/yepsua/filament-range-field",
+ "keywords": [
+ "filament-range-field",
+ "laravel",
+ "yepsua"
+ ],
+ "support": {
+ "issues": "https://github.com/yepsua/filament-range-field/issues",
+ "source": "https://github.com/yepsua/filament-range-field/tree/v0.3.4"
+ },
+ "time": "2023-12-29T06:06:23+00:00"
}
],
"packages-dev": [
diff --git a/config/filament-forms-tinyeditor.php b/config/filament-forms-tinyeditor.php
index 474028b..2fa9d4e 100644
--- a/config/filament-forms-tinyeditor.php
+++ b/config/filament-forms-tinyeditor.php
@@ -28,13 +28,17 @@ return [
'plugins' => 'advlist paste autoresize codesample directionality emoticons fullscreen hr image imagetools link lists media table toc wordcount',
'toolbar' => ' bold italic | numlist bullist | blockquote table hr | link | fullscreen',
'upload_directory' => null,
+ 'browser_spellcheck' => true,
'custom_configs' => [
'table_advtab' => false,
'table_row_advtab' => false,
'table_cell_advtab' => false,
'contextmenu' => '',
'table_resize_bars' => false,
- 'paste_as_text' => true
+ 'paste_as_text' => true,
+ 'browser_spellcheck' => true, // Включение проверки орфографии
+
+
],
]
diff --git a/database/migrations/2025_01_27_154953_add_education_form_id_to_educational_groups_table.php b/database/migrations/2025_01_27_154953_add_education_form_id_to_educational_groups_table.php
new file mode 100644
index 0000000..7f3472b
--- /dev/null
+++ b/database/migrations/2025_01_27_154953_add_education_form_id_to_educational_groups_table.php
@@ -0,0 +1,30 @@
+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');
+ });
+ }
+};
diff --git a/database/migrations/2025_01_27_155121_remove_is_zaoch_from_schedules_table.php b/database/migrations/2025_01_27_155121_remove_is_zaoch_from_schedules_table.php
new file mode 100644
index 0000000..9083022
--- /dev/null
+++ b/database/migrations/2025_01_27_155121_remove_is_zaoch_from_schedules_table.php
@@ -0,0 +1,30 @@
+dropColumn('is_zaoch');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down() : void
+ {
+ Schema::table('schedules', function (Blueprint $table) {
+ $table->boolean('is_zaoch')->default(false);
+ });
+ }
+};
diff --git a/database/migrations/2025_01_27_162420_add_start_end_times_to_main_sliders_table.php b/database/migrations/2025_01_27_162420_add_start_end_times_to_main_sliders_table.php
new file mode 100644
index 0000000..a4f538d
--- /dev/null
+++ b/database/migrations/2025_01_27_162420_add_start_end_times_to_main_sliders_table.php
@@ -0,0 +1,31 @@
+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']); // Удаление колонок при откате миграции
+ });
+ }
+};
diff --git a/database/migrations/2025_01_29_162363_change_columns_to_null_main_sliders_table.php b/database/migrations/2025_01_29_162363_change_columns_to_null_main_sliders_table.php
new file mode 100644
index 0000000..dcdf369
--- /dev/null
+++ b/database/migrations/2025_01_29_162363_change_columns_to_null_main_sliders_table.php
@@ -0,0 +1,38 @@
+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();
+ });
+ }
+};
diff --git a/package-lock.json b/package-lock.json
index 8b23941..1c1c381 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"flowbite": "^2.5.2",
"fslightbox": "^3.4.1",
"fslightbox-vue": "^2.1.3",
+ "js-cookie": "^3.0.5",
"preline": "^1.9.0",
"slugify": "^1.6.6",
"vue3-yandex-smartcaptcha": "^1.0.0"
@@ -2022,6 +2023,14 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/laravel-vite-plugin": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-0.8.1.tgz",
diff --git a/package.json b/package.json
index 2453661..b260e5c 100644
--- a/package.json
+++ b/package.json
@@ -41,6 +41,7 @@
"flowbite": "^2.5.2",
"fslightbox": "^3.4.1",
"fslightbox-vue": "^2.1.3",
+ "js-cookie": "^3.0.5",
"preline": "^1.9.0",
"slugify": "^1.6.6",
"vue3-yandex-smartcaptcha": "^1.0.0"
diff --git a/resources/js/Components/BuilderUi/Pages/PageNavigateLinks.vue b/resources/js/Components/BuilderUi/Pages/PageNavigateLinks.vue
index 944e9db..05fde77 100644
--- a/resources/js/Components/BuilderUi/Pages/PageNavigateLinks.vue
+++ b/resources/js/Components/BuilderUi/Pages/PageNavigateLinks.vue
@@ -1,5 +1,5 @@
-
+
@@ -127,6 +63,7 @@ import BaseIcon from "@/Components/BaseComponents/BaseIcon.vue";
import MobileNavbar from "@/Navbars/MobileNavbar.vue";
import SearchModal from "@/Components/Modals/SearchModal.vue";
import {defineAsyncComponent} from "vue";
+import DesktopNavBar from "@/Navbars/DesktopNavBar.vue";
export default {
@@ -141,6 +78,7 @@ export default {
return {}
},
components: {
+ DesktopNavBar,
SearchModal,
MobileNavbar,
BaseIcon,
diff --git a/resources/js/Navbars/MainPageNavbar.vue b/resources/js/Navbars/MainPageNavbar.vue
index 55b915f..82d9dab 100644
--- a/resources/js/Navbars/MainPageNavbar.vue
+++ b/resources/js/Navbars/MainPageNavbar.vue
@@ -44,6 +44,7 @@
+
@@ -63,6 +64,7 @@ import SearchModal from "@/Components/Modals/SearchModal.vue";
import DesktopNavBar from "@/Navbars/DesktopNavBar.vue";
+
export default {
name: 'MainPageNavBar',
components: {
@@ -87,6 +89,7 @@ export default {
headerFilter: false,
underSliderHeader: this.sliderRef,
bvi: null,
+ isActiveBvi: null,
logos: {
default: '/logos/white_ntspi_logo.svg',
alternate: '/logos/ntspi-logo.svg',
@@ -123,6 +126,19 @@ export default {
},
+ iniBvi() {
+ if (this.getCookie('bvi_panelActive') === null) {
+ this.bvi = new isvek.Bvi({
+ target: '.open-bvi',
+ fontSize: 24,
+ theme: 'black',
+ speech: false,
+ reload: true,
+ panelHide: true
+ });
+ }
+ },
+
handleScroll() {
if (typeof this.sliderRef === 'object') {
const mainSlider = this.sliderRef;
@@ -133,30 +149,19 @@ export default {
this.headerFilter = true
}
},
- getCookie(name) {
- let cookies = document.cookie.split(';');
- for (let i = 0; i < cookies.length; i++) {
- let cookie = cookies[i].trim();
- if (cookie.startsWith(name + '=')) {
- return cookie.substring(name.length + 1);
- }
- }
- return null; // Если cookie не найден
- }
+
},
mounted() {
window.addEventListener('scroll', this.handleScroll)
- // if (this.getCookie('bvi_panelActive') === null) {
- // this.bvi = new isvek.Bvi({
- // target: '.className',
- // fontSize: 24,
- // theme: 'black',
- // speech: false,
- // reload: true,
- // });
- // }
+
+
+ if (this.getCookie('bvi_panelActive') === null) {
+ this.iniBvi()
+ }
+
+
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll)
diff --git a/resources/js/Pages/Client/Programs/Show.vue b/resources/js/Pages/Client/Programs/Show.vue
index 516604a..bfb95bd 100644
--- a/resources/js/Pages/Client/Programs/Show.vue
+++ b/resources/js/Pages/Client/Programs/Show.vue
@@ -106,8 +106,11 @@ export default {
+
Направление подготовки
{{ program.data.directionStudy.name }} {{ program.data.directionStudy.code }}
@@ -118,7 +121,9 @@ export default {
Срок обучения
@@ -148,7 +153,9 @@ export default {
Количество мест на прием
diff --git a/resources/js/Pages/Client/Schedules/Index.vue b/resources/js/Pages/Client/Schedules/Index.vue
index c03bb99..699a89d 100644
--- a/resources/js/Pages/Client/Schedules/Index.vue
+++ b/resources/js/Pages/Client/Schedules/Index.vue
@@ -5,23 +5,34 @@ import { debounce } from "lodash";
import { Link } from "@inertiajs/vue3";
import MainPageNavBar from "@/Navbars/MainPageNavbar.vue";
import BaseIcon from "@/Components/BaseComponents/BaseIcon.vue";
+import ClientScheduleFilter from "@/Components/BuilderUi/Schedules/ClientScheduleFilter.vue";
export default {
name: "Index",
data() {
return {
- searchInput: this.searchRequest,
- favoriteGroups: JSON.parse(localStorage.getItem('favoriteGroups')) || [],
+ searchInput: this.filters.search_filter.value,
+ favoriteGroups: JSON.parse(localStorage.getItem('favoriteGroups')),
showFavorites: false,
+ loading: false,
};
},
- components: {BaseIcon, MainPageNavBar, ClientFooterDown, MainNavbar, Link},
- props: [
- 'educationalGroups',
- 'mainSections',
- 'searchRequest',
- 'navigation',
- ],
+ components: {ClientScheduleFilter, BaseIcon, MainPageNavBar, ClientFooterDown, MainNavbar, Link},
+
+ props: {
+ navigation: {
+ type: Object
+ },
+ filters: {
+ type: Object
+ },
+ forms_education: {
+ type: Object
+ },
+ schedulesByFaculty: {
+ type: Object
+ }
+ },
methods: {
search: debounce(function () {
this.$inertia.reload({
@@ -34,7 +45,7 @@ export default {
});
}, 300),
toggleFavorite(group) {
- const index = this.favoriteGroups.findIndex(g => g.id === group.id);
+ const index = this.favoriteGroups.findIndex(g => g === group);
if (index === -1) {
this.favoriteGroups.push(group);
} else {
@@ -43,18 +54,65 @@ export default {
localStorage.setItem('favoriteGroups', JSON.stringify(this.favoriteGroups));
},
isFavorite(group) {
- return this.favoriteGroups.some(g => g.id === group.id);
+ return this.favoriteGroups.some(g => g === group);
},
toggleShowFavorites() {
- this.showFavorites = !this.showFavorites;
- },
- },
- computed: {
- filteredGroups() {
- if (this.showFavorites) {
- return this.educationalGroups.data.filter(group => this.favoriteGroups.some(g => g.id === group.id));
+ this.loading = true; // Включаем состояние загрузки
+
+ if (this.filters.favorite_filter.value === null) {
+ this.filterFavorite(() => {
+ this.loading = false; // Выключаем состояние загрузки после завершения
+ });
+ } else {
+ this.clearFilterFavorite(() => {
+ this.loading = false; // Выключаем состояние загрузки после завершения
+ });
}
- return this.educationalGroups.data;
+ },
+
+ filterFavorite: debounce(function (callback) {
+ let url = new URL(window.location.href);
+ // Создаем массив для хранения всех ключей, которые нужно удалить
+ const keysToDelete = [];
+
+ // Перебираем все параметры и добавляем ключи, начинающиеся с 'category', в массив
+ for (const [key] of url.searchParams) {
+ if (key.startsWith('favorite')) {
+ keysToDelete.push(key);
+ }
+ }
+ // Удаляем все ключи из массива
+ keysToDelete.forEach(key => url.searchParams.delete(key));
+ let newUrl = url.toString();
+ this.$inertia.visit(newUrl, {
+ method: 'get',
+ preserveState: true,
+ data: {
+ favorite: (this.favoriteGroups.length !== 0) ? this.favoriteGroups : "",
+ },
+ onFinish: callback, // Вызываем колбэк после завершения запроса
+ });
+ }, 500),
+ clearFilterFavorite(callback) {
+ let url = new URL(window.location.href);
+ // Создаем массив для хранения всех ключей, которые нужно удалить
+ const keysToDelete = [];
+
+ // Перебираем все параметры и добавляем ключи, начинающиеся с 'category', в массив
+ for (const [key] of url.searchParams) {
+ if (key.startsWith('favorite')) {
+ keysToDelete.push(key);
+ }
+ }
+ // Удаляем все ключи из массива
+ keysToDelete.forEach(key => url.searchParams.delete(key));
+ let newUrl = url.toString();
+ this.$inertia.visit(newUrl, {
+ method: 'get',
+ preserveState: true,
+ onFinish: callback, // Вызываем колбэк после завершения запроса
+
+ });
},
},
};
@@ -69,14 +127,13 @@ export default {
-
-
+
+
Расписание занятий
-
-
-
+
+
Просто введите название группы
@@ -100,7 +157,7 @@ export default {
id="hs-search-article-1"
class="py-2.5 px-4 block w-full border-transparent rounded-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:bg-gray-100"
placeholder="Поиск"
- :disabled="showFavorites"
+ :disabled="filters.favorite_filter.value !== null"
>
@@ -108,24 +165,20 @@ export default {
-
@@ -133,131 +186,59 @@ export default {
-
-
+
+
-
-
-
-
-
-
-
-
+
+
+
{{ title }}
+
+
+
+
+
+
+
+
+
-
+
+
+ {{ educationalGroup.data.title }}
+
+
+
+
-
-
- {{ educationalGroup.title }} {{ educationalGroup.faculty }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ educationalGroup.title }} {{ educationalGroup.faculty }}
-
-
-
-
-
+
diff --git a/resources/js/Pages/Main.vue b/resources/js/Pages/Main.vue
index 7362f87..868f59c 100644
--- a/resources/js/Pages/Main.vue
+++ b/resources/js/Pages/Main.vue
@@ -4,12 +4,13 @@
- Последние новости
-
-
-
-
-
+ Последние новости
+
+
+
+
+
+
@@ -184,6 +185,7 @@ import BaseMetaHead from "@/Components/BaseComponents/BaseMetaHead.vue";
import PageResourceList from "@/Components/BuilderUi/Pages/Blocks/PageResourceList.vue";
import AppHead from "@/Components/AppHead.vue";
import ContactSectionBlock from "@/Components/BaseComponents/BaseBuilderUi/Blocks/Contacts/ContactSectionBlock.vue";
+import Cookies from "js-cookie";
@@ -238,6 +240,7 @@ export default {
},
methods: {
+ Cookies,
setSliderRef(ref) {
this.sliderRef = ref;
},
diff --git a/resources/js/Pages/Page.vue b/resources/js/Pages/Page.vue
index 2dd6103..614fbdd 100644
--- a/resources/js/Pages/Page.vue
+++ b/resources/js/Pages/Page.vue
@@ -5,7 +5,7 @@
-
+
diff --git a/resources/js/app.js b/resources/js/app.js
index 8c83f16..b6df7b5 100644
--- a/resources/js/app.js
+++ b/resources/js/app.js
@@ -7,6 +7,7 @@ import { createInertiaApp } from '@inertiajs/vue3';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
import {linksReform} from "@/mixins/LinksReform.js";
import YSmartCaptcha from 'vue3-yandex-smartcaptcha'
+import cookieMixin from "@/mixins/cookieMixin.js";
// const appName = import.meta.env.VITE_APP_NAME || 'НТГСПИ';
@@ -20,6 +21,7 @@ createInertiaApp({
return createSSRApp({ render: () => h(App, props) })
.use(plugin)
.mixin(linksReform)
+ .mixin(cookieMixin)
.use(ZiggyVue)
.use(YSmartCaptcha, {
siteKey: "ysc1_jxb2IcslgBuHgHbmmKY1F7Nvb8zYHuAeBgjoaTka6886f893",
diff --git a/resources/js/mixins/cookieMixin.js b/resources/js/mixins/cookieMixin.js
new file mode 100644
index 0000000..1af551b
--- /dev/null
+++ b/resources/js/mixins/cookieMixin.js
@@ -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); // Удаляем куку
+ }
+ }
+ }
+ }
+};
diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php
index bd74b8f..3a62ef3 100644
--- a/resources/views/app.blade.php
+++ b/resources/views/app.blade.php
@@ -30,11 +30,20 @@
z-index: 10000000!important;
}
+ .bvi-active {
+ padding: 0;
+ margin: 0;
+ }
+
.ce-header {
font-weight: bold;
font-size: 20px;
}
+ body .bvi-body {
+ padding: 0;
+ }
+
*, html {
scroll-padding: 6rem;
scroll-behavior: smooth !important;
diff --git a/routes/api.php b/routes/api.php
index cc277ee..5a217e6 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -10,6 +10,7 @@ use App\Http\Controllers\ClientWidgetPageReferenceListController;
use App\Http\Controllers\ClientWidgetPostController;
use App\Http\Controllers\IconController;
use App\Http\Controllers\SearchController;
+use App\Http\Controllers\UpdateEduDataApiController;
use App\Http\Controllers\VkAuthController;
use App\Http\Controllers\VkPostController;
use App\Models\AdmissionCampaign;
@@ -25,6 +26,9 @@ Route::get('/getAcademicYear', function () {
})->name('academic.year');
+
+
+
Route::middleware('ensure.browser')->group(function () {
Route::get('/getNavigation', [NavigateController::class, 'index'])->name('client.main.navigate');
@@ -50,6 +54,8 @@ Route::middleware('ensure.browser')->group(function () {
});
Route::middleware(['auth', 'superadmin'])->group(function () {
+ Route::get('/get-data', [UpdateEduDataApiController::class, 'index']);
+
Route::get('/login/vk', [VkAuthService::class, 'redirectToProvider'])->name('vk.login');
Route::get('/login/vk/callback', [VkAuthService::class, 'handleProviderCallback'])->name('vk.callback');
Route::get('/vk-get-token', [VkAuthService::class, 'getToken'])->name('vk.getToken');
diff --git a/routes/web.php b/routes/web.php
index 52e4ddc..a2321b7 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -86,8 +86,6 @@ Route::middleware('access-check')->group(function () {
Route::get('/divisions', [ClientDivisionController::class, 'index'])->name('client.division.index');
Route::get('/divisions/{slug}', [ClientDivisionController::class, 'show'])->name('client.division.show');
- Route::get('/get-data', [UpdateEduDataApiController::class, 'index']);
-
Route::get('{path}', [PageController::class, 'render'])->where('path', '[0-9,a-z,/,-]+')->name('page.view');
});