diff --git a/app/Http/Controllers/FeatureController.php b/app/Http/Controllers/FeatureController.php new file mode 100644 index 00000000..37e52c10 --- /dev/null +++ b/app/Http/Controllers/FeatureController.php @@ -0,0 +1,219 @@ +with('author'); + + // For authenticated users, show their proposed features + all published + // For guests, show only published features + if ($user = $request->user()) { + $query->where(function ($q) use ($user) { + $q->where('status', FeatureStatusEnum::Published) + ->orWhere('status', FeatureStatusEnum::Implemented) + ->orWhere(function ($subQ) use ($user) { + $subQ->where('status', FeatureStatusEnum::Proposed) + ->where('user_id', $user->id); + }); + }) + // Sort: user's proposed features first, then by votes + ->orderByRaw("CASE WHEN user_id = ? AND status = 'proposed' THEN 0 ELSE 1 END", [$user->id]) + ->orderBy('votes_count', 'desc') + ->orderBy('order', 'asc'); + } else { + $query->whereIn('status', [FeatureStatusEnum::Published, FeatureStatusEnum::Implemented]) + ->orderBy('votes_count', 'desc') + ->orderBy('order', 'asc'); + } + + $features = $query->simplePaginate(5); + + // Attach user vote status to each feature + if ($user = $request->user()) { + $features->getCollection()->transform(function ($feature) use ($user) { + $feature->user_vote = $feature->getUserVote($user); + + return $feature; + }); + } + + // Check if this is a turbo request (frame or stream) + $isTurboRequest = $request->header('Turbo-Frame') || $request->wantsTurboStream(); + + if (! $isTurboRequest) { + return view('features.index', [ + 'features' => $features, + ]); + } + + return turbo_stream([ + turbo_stream()->removeAll('.feature-placeholder'), + turbo_stream()->append('features-frame', view('features._list', [ + 'features' => $features, + ])), + + turbo_stream()->replace('feature-more', view('features._pagination', [ + 'features' => $features, + ])), + ]); + } + + /** + * Store a newly created feature proposal. + */ + public function store(Request $request) + { + $this->authorize('create', Feature::class); + + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'description' => 'required|string|max:5000', + ]); + + $feature = Feature::create([ + 'title' => $validated['title'], + 'description' => $validated['description'], + 'user_id' => $request->user()->id, + ]); + + // Load the author relationship + $feature->load('author'); + + // Add user_vote attribute + $feature->user_vote = $feature->getUserVote($request->user()); + + return turbo_stream([ + turbo_stream() + ->target('features-frame') + ->action('prepend') + ->view('features._list', ['features' => collect([$feature])]), + + turbo_stream() + ->append('.toast-wrapper') + ->view('features._toast', [ + 'message' => 'Спасибо за предложение! Оно будет рассмотрено в ближайшее время.', + ]), + ]); + } + + /** + * Vote for a feature (upvote only, no cancellation). + */ + public function vote(Request $request, Feature $feature) + { + $this->authorize('vote', $feature); + + $validated = $request->validate([ + 'vote' => 'required|integer|in:1', + ]); + + $feature->toggleVote($request->user(), $validated['vote']); + + // Refresh the feature data with vote count + $feature = $feature->fresh(); + $feature->user_vote = $feature->getUserVote($request->user()); + + return turbo_stream([ + turbo_stream() + ->target("feature-vote-{$feature->id}") + ->action('replace') + ->view('features._vote-button', [ + 'feature' => $feature, + ]), + ]); + } + + public function search(Request $request) + { + $query = $request->input('q', ''); + $user = $request->user(); + + // If query is empty or too short, return default list + if (strlen($query) < 3) { + $featureQuery = Feature::query()->with('author'); + + // For authenticated users, show their proposed features + all published + if ($user) { + $featureQuery->where(function ($q) use ($user) { + $q->where('status', FeatureStatusEnum::Published) + ->orWhere('status', FeatureStatusEnum::Implemented) + ->orWhere(function ($subQ) use ($user) { + $subQ->where('status', FeatureStatusEnum::Proposed) + ->where('user_id', $user->id); + }); + }) + // Sort: user's proposed features first, then by votes + ->orderByRaw("CASE WHEN user_id = ? AND status = 'proposed' THEN 0 ELSE 1 END", [$user->id]) + ->orderBy('votes_count', 'desc') + ->orderBy('order', 'asc'); + } else { + $featureQuery->whereIn('status', [FeatureStatusEnum::Published, FeatureStatusEnum::Implemented]) + ->orderBy('votes_count', 'desc') + ->orderBy('order', 'asc'); + } + + $features = $featureQuery->simplePaginate(5); + } else { + // Perform search using Scout + $searchQuery = Feature::search($query); + + // For authenticated users, include their proposed features in search + if ($user) { + $searchQuery->query(fn ($builder) => $builder + ->with('author') + ->where(function ($q) use ($user) { + $q->where('status', FeatureStatusEnum::Published) + ->orWhere('status', FeatureStatusEnum::Implemented) + ->orWhere(function ($subQ) use ($user) { + $subQ->where('status', FeatureStatusEnum::Proposed) + ->where('user_id', $user->id); + }); + }) + ->orderByRaw("CASE WHEN user_id = ? AND status = 'proposed' THEN 0 ELSE 1 END", [$user->id]) + ->orderBy('votes_count', 'desc') + ->orderBy('order', 'asc') + ); + } else { + $searchQuery->whereIn('status', [FeatureStatusEnum::Published, FeatureStatusEnum::Implemented]) + ->query(fn ($builder) => $builder->with('author')) + ->orderBy('votes_count', 'desc') + ->orderBy('order', 'asc'); + } + + $features = $searchQuery->simplePaginate(5); + } + + if ($user) { + $features->getCollection()->transform(function ($feature) use ($user) { + $feature->user_vote = $feature->getUserVote($user); + + return $feature; + }); + } + + return turbo_stream([ + turbo_stream() + ->target('features-frame') + ->action('replace') + ->view('features._search-results', [ + 'features' => $features, + ]), + turbo_stream() + ->target('feature-more') + ->action('replace') + ->view('features._pagination', [ + 'features' => $features, + ]), + ]); + } +} diff --git a/app/Models/Enums/FeatureStatusEnum.php b/app/Models/Enums/FeatureStatusEnum.php new file mode 100644 index 00000000..a492bc6e --- /dev/null +++ b/app/Models/Enums/FeatureStatusEnum.php @@ -0,0 +1,26 @@ + 'На рассмотреннии', + self::Published => 'Опубликовано', + self::Rejected => 'Отменено', + self::Implemented => 'Реализовано', + }; + } +} diff --git a/app/Models/Feature.php b/app/Models/Feature.php new file mode 100644 index 00000000..d9b183f0 --- /dev/null +++ b/app/Models/Feature.php @@ -0,0 +1,140 @@ + FeatureStatusEnum::class, + 'votes_count' => 'integer', + ]; + + protected $attributes = [ + 'status' => 'proposed', + 'votes_count' => 0, + ]; + + protected $allowedFilters = [ + 'title' => Like::class, + 'description' => Like::class, + ]; + + protected $allowedSorts = [ + 'title', + 'votes_count', + 'created_at', + ]; + + /** + * Get only published features. + */ + public function scopePublished(Builder $query): Builder + { + return $query->where('status', FeatureStatusEnum::Published); + } + + public function isProposed(): bool + { + return $this->status === FeatureStatusEnum::Proposed; + } + + public function isImplemented(): bool + { + return $this->status === FeatureStatusEnum::Implemented; + } + + public function isRejected(): bool + { + return $this->status === FeatureStatusEnum::Rejected; + } + + public function isPublished(): bool + { + return $this->status === FeatureStatusEnum::Published; + } + + /** + * Get voters who voted for this feature. + */ + public function voters(): BelongsToMany + { + return $this->belongsToMany(User::class, 'feature_votes') + ->withPivot('vote') + ->withTimestamps(); + } + + /** + * Check if the user has voted for this feature. + */ + public function hasVotedBy(?User $user): bool + { + if (! $user) { + return false; + } + + return $this->voters()->where('user_id', $user->id)->exists(); + } + + /** + * Get the user's vote for this feature (1 or -1). + */ + public function getUserVote(?User $user): ?int + { + if (! $user) { + return null; + } + + $vote = $this->voters()->where('user_id', $user->id)->first(); + + return $vote?->pivot->vote; + } + + /** + * Vote for this feature (one-time only). + */ + public function toggleVote(User $user, int $voteValue = 1): void + { + // Check if user has already voted + $existingVote = $this->voters()->where('user_id', $user->id)->first(); + + // If user already voted, do nothing (no cancellation allowed) + if ($existingVote) { + return; + } + + // Add new vote (only upvotes allowed, voteValue should be 1) + $this->voters()->attach($user->id, ['vote' => $voteValue]); + $this->increment('votes_count', $voteValue); + } + + /** + * Get the indexable data array for the model. + */ + public function toSearchableArray(): array + { + return [ + 'title' => $this->title, + ]; + } +} diff --git a/app/Notifications/FeatureNotification.php b/app/Notifications/FeatureNotification.php new file mode 100644 index 00000000..c3d85786 --- /dev/null +++ b/app/Notifications/FeatureNotification.php @@ -0,0 +1,65 @@ + + */ + public function via(object $notifiable): array + { + return [ + SiteChannel::class, + ]; + } + + /** + * Get the app representation of the notification. + * + * @param mixed $user + * + * @return SiteMessage + */ + public function toSite(User $user) + { + $siteMessageBuilder = (new SiteMessage) + ->title("Предложенная функция \"{$this->feature->title}\" сменила статус на: {$this->feature->status->text()}"); + + if ($this->feature->isImplemented() || $this->feature->isPublished()) { + $siteMessageBuilder->action(route('features.index'), 'Посмотреть'); + } + return $siteMessageBuilder; + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + // + ]; + } +} diff --git a/app/Orchid/PlatformProvider.php b/app/Orchid/PlatformProvider.php index 397a47d4..85599669 100644 --- a/app/Orchid/PlatformProvider.php +++ b/app/Orchid/PlatformProvider.php @@ -83,6 +83,11 @@ public function menu(): array ->icon('bs.collection') ->route('platform.challenges'), + Menu::make('Предложения функций') + ->permission('site.content') + ->icon('bs.lightbulb') + ->route('platform.feature'), + Menu::make('Яндекс Метрика') ->icon('pie-chart') ->href('https://metrika.yandex.ru/dashboard?id=96430041') diff --git a/app/Orchid/Screens/Feature/EditScreen.php b/app/Orchid/Screens/Feature/EditScreen.php new file mode 100644 index 00000000..b30572df --- /dev/null +++ b/app/Orchid/Screens/Feature/EditScreen.php @@ -0,0 +1,178 @@ +feature = $feature; + $this->isExisting = $feature->exists; + + $feature->load(['author']); + + // Convert enum to string value for Orchid select field + if ($feature->exists) { + // Get raw attributes to bypass enum casting + $attributes = $feature->getAttributes(); + $feature->setRawAttributes(array_merge($attributes, [ + 'status' => $feature->status->value, + ]), true); + } + + return [ + 'feature' => $feature, + ]; + } + + /** + * The name of the screen displayed in the header. + */ + public function name(): ?string + { + return $this->isExisting ? 'Редактирование предложения' : 'Создание предложения'; + } + + /** + * Display header description. + */ + public function description(): ?string + { + return ''; + } + + public function permission(): ?iterable + { + return [ + 'site.content', + ]; + } + + /** + * The screen's action buttons. + * + * @return Action[] + */ + public function commandBar(): iterable + { + return [ + Button::make('Удалить') + ->icon('bs.trash3') + ->confirm('Вы уверены, что хотите удалить это предложение?') + ->method('remove') + ->canSee($this->isExisting), + ]; + } + + /** + * @return \Orchid\Screen\Layout[] + */ + public function layout(): iterable + { + return [ + Layout::block( + Layout::rows([ + Input::make('feature.title') + ->title('Название') + ->placeholder('Краткое название функции') + ->required(), + + TextArea::make('feature.description') + ->title('Описание') + ->rows(5) + ->placeholder('Подробное описание функции') + ->required(), + + Select::make('feature.status') + ->title('Статус') + ->fromEnum(FeatureStatusEnum::class, 'text') + ->required(), + ])) + ->title('Предложение функции') + ->description('Управление предложением новой функции') + ->commands( + Button::make($this->feature->exists ? 'Сохранить изменения' : 'Создать') + ->type(Color::SUCCESS) + ->icon('bs.check-circle') + ->method('save') + ), + ]; + } + + /** + * @return RedirectResponse + */ + public function save(Feature $feature, Request $request) + { + $request->validate([ + 'feature.title' => ['required', 'string', 'max:255'], + 'feature.description' => ['required', 'string'], + 'feature.status' => ['required', 'string'], + ]); + + $data = $request->input('feature'); + + // If creating new feature, set user_id + if (! $feature->exists) { + $data['user_id'] = $request->user()->id; + } + + $feature->forceFill($data)->save(); + + if ($feature->isPublished() || $feature->isRejected() || $feature->isImplemented()) { + $feature->author->notify(new FeatureNotification($feature)); + } + + Toast::info($feature->wasRecentlyCreated ? 'Предложение создано' : 'Информация обновлена'); + + return redirect()->route('platform.feature'); + } + + /** + * @throws \Exception + * + * @return RedirectResponse + */ + public function remove(Feature $feature) + { + $feature->delete(); + + Toast::info('Предложение удалено'); + + $feature->author->notify(new FeatureNotification($feature)); + + return redirect()->route('platform.feature'); + } +} diff --git a/app/Orchid/Screens/Feature/ListScreen.php b/app/Orchid/Screens/Feature/ListScreen.php new file mode 100644 index 00000000..21af1323 --- /dev/null +++ b/app/Orchid/Screens/Feature/ListScreen.php @@ -0,0 +1,172 @@ + Feature::with('author') + ->filters() + ->defaultSort('votes_count', 'desc') + ->paginate(), + ]; + } + + /** + * The name of the screen displayed in the header. + * + * @return string|null + */ + public function name(): ?string + { + return 'Предложения функций'; + } + + /** + * A description of the screen to be displayed in the header. + * + * @return string|null + */ + public function description(): ?string + { + return 'Управление предложениями новых функций от пользователей'; + } + + /** + * The screen's action buttons. + * + * @return \Orchid\Screen\Action[] + */ + public function commandBar(): iterable + { + return [ + Link::make('Создать') + ->icon('bs.plus-circle') + ->route('platform.feature.create'), + ]; + } + + public function permission(): ?iterable + { + return [ + 'site.content', + ]; + } + + /** + * The screen's layout elements. + * + * @throws \ReflectionException + * + * @return \Orchid\Screen\Layout[]|string[] + */ + public function layout(): iterable + { + return [ + Layout::table('features', [ + + TD::make('title', 'Название') + ->width(200) + ->sort() + ->cantHide() + ->render(function (Feature $feature) { + return "".e($feature->title).''; + })->filter(Input::make()), + + TD::make('description', 'Описание') + ->width(300) + ->cantHide() + ->render(function (Feature $feature) { + return Str::of($feature->description)->words(15); + })->filter(Input::make()), + + TD::make('votes_count', 'Голоса') + ->sort() + ->render(function (Feature $feature) { + $class = $feature->votes_count > 0 ? 'text-success' : ($feature->votes_count < 0 ? 'text-danger' : ''); + + return "{$feature->votes_count}"; + }), + + TD::make('status', 'Статус') + ->sort() + ->render(function (Feature $feature) { + $badges = [ + 'proposed' => 'secondary', + 'published' => 'success', + 'rejected' => 'danger', + 'implemented' => 'info', + ]; + $statusValue = is_object($feature->status) ? $feature->status->value : $feature->status; + $badge = $badges[$statusValue] ?? 'secondary'; + + return "{$statusValue}"; + }), + + TD::make('Автор') + ->align(TD::ALIGN_CENTER) + ->cantHide() + ->render(fn (Feature $feature) => new Persona($feature->author->presenter())), + + TD::make('created_at', 'Создано') + ->usingComponent(DateTimeSplit::class) + ->align(TD::ALIGN_RIGHT) + ->sort(), + + TD::make('Действия') + ->align(TD::ALIGN_CENTER) + ->width('100px') + ->render(fn (Feature $feature) => DropDown::make() + ->icon('bs.three-dots-vertical') + ->list([ + Link::make('Редактировать') + ->route('platform.feature.edit', $feature->id) + ->icon('bs.pencil'), + + Button::make('Удалить') + ->icon('bs.trash3') + ->confirm('Вы уверены, что хотите удалить это предложение?') + ->method('remove', [ + 'feature' => $feature->id, + ]), + ])), + ]), + + ]; + } + + /** + * @param Feature $feature + * + * @return void + */ + public function remove(Feature $feature): void + { + $feature->delete(); + + $feature->author->notify(new FeatureNotification($feature)); + + Toast::info('Предложение удалено'); + } +} diff --git a/app/Policies/FeaturePolicy.php b/app/Policies/FeaturePolicy.php new file mode 100644 index 00000000..fc713c1f --- /dev/null +++ b/app/Policies/FeaturePolicy.php @@ -0,0 +1,74 @@ +id === $feature->user_id; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Feature $feature): bool + { + return $user->id === $feature->user_id; + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Feature $feature): bool + { + return $user->id === $feature->user_id; + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Feature $feature): bool + { + return $user->id === $feature->user_id; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 96ec338d..9ed5ef43 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,6 +4,7 @@ use App\Models\ChallengeApplication; use App\Models\Comment; +use App\Models\Feature; use App\Models\IdeaKey; use App\Models\Meet; use App\Models\Package; @@ -11,6 +12,7 @@ use App\Models\Post; use App\Policies\ChallengeApplicationPolicy; use App\Policies\CommentPolicy; +use App\Policies\FeaturePolicy; use App\Policies\IdeaKeyPolicy; use App\Policies\MeetPolicy; use App\Policies\PackagePolicy; @@ -87,6 +89,7 @@ protected function policies() Comment::class => CommentPolicy::class, Meet::class => MeetPolicy::class, Post::class => PostPolicy::class, + Feature::class => FeaturePolicy::class, Package::class => PackagePolicy::class, Position::class => PositionPolicy::class, IdeaKey::class => IdeaKeyPolicy::class, diff --git a/database/migrations/2025_11_07_180141_create_features_table.php b/database/migrations/2025_11_07_180141_create_features_table.php new file mode 100644 index 00000000..f4c47c08 --- /dev/null +++ b/database/migrations/2025_11_07_180141_create_features_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('title'); + $table->text('description'); + $table->integer('votes_count')->default(0); + $table->string('status')->default('proposed'); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->timestamps(); + + $table->index(['status', 'votes_count']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('features'); + } +}; diff --git a/database/migrations/2025_11_07_180231_create_feature_votes_table.php b/database/migrations/2025_11_07_180231_create_feature_votes_table.php new file mode 100644 index 00000000..545c1b66 --- /dev/null +++ b/database/migrations/2025_11_07_180231_create_feature_votes_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('feature_id')->constrained()->cascadeOnDelete(); + $table->foreignId('user_id')->constrained()->cascadeOnDelete(); + $table->tinyInteger('vote')->default(1); // 1 for upvote, -1 for downvote + $table->timestamps(); + + $table->unique(['feature_id', 'user_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('feature_votes'); + } +}; diff --git a/resources/js/controllers/feature-modal_controller.js b/resources/js/controllers/feature-modal_controller.js new file mode 100644 index 00000000..b6dd4ef8 --- /dev/null +++ b/resources/js/controllers/feature-modal_controller.js @@ -0,0 +1,53 @@ +import { Controller } from '@hotwired/stimulus'; +import { Modal } from 'bootstrap'; + +export default class extends Controller { + connect() { + this.boundHandleSubmitEnd = this.handleSubmitEnd.bind(this); + this.boundBeforeCache = this.beforeCache.bind(this); + + this.element.addEventListener('turbo:submit-end', this.boundHandleSubmitEnd); + document.addEventListener('turbo:before-cache', this.boundBeforeCache); + } + + disconnect() { + this.element.removeEventListener('turbo:submit-end', this.boundHandleSubmitEnd); + document.removeEventListener('turbo:before-cache', this.boundBeforeCache); + } + + handleSubmitEnd(event) { + // Close modal if form submission was successful (no validation errors) + if (event.detail.success) { + this.closeModalAndCleanup(); + } + } + + beforeCache() { + // Ensure modal is closed before page is cached + this.closeModalAndCleanup(); + } + + closeModalAndCleanup() { + const modal = Modal.getInstance(this.element); + if (modal) { + modal.hide(); + } + + // Force remove modal backdrop + const backdrop = document.querySelector('.modal-backdrop'); + if (backdrop) { + backdrop.remove(); + } + + // Remove modal-open class from body + document.body.classList.remove('modal-open'); + document.body.style.removeProperty('overflow'); + document.body.style.removeProperty('padding-right'); + + // Reset form + const form = this.element.querySelector('form'); + if (form) { + form.reset(); + } + } +} \ No newline at end of file diff --git a/resources/js/controllers/feature-search_controller.js b/resources/js/controllers/feature-search_controller.js new file mode 100644 index 00000000..14cf0a10 --- /dev/null +++ b/resources/js/controllers/feature-search_controller.js @@ -0,0 +1,17 @@ +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['form', 'input']; + + connect() { + this.timeout = null; + } + + search() { + clearTimeout(this.timeout); + + this.timeout = setTimeout(() => { + this.formTarget.requestSubmit(); + }, 300); + } +} diff --git a/resources/views/features/_list.blade.php b/resources/views/features/_list.blade.php new file mode 100644 index 00000000..2ea48017 --- /dev/null +++ b/resources/views/features/_list.blade.php @@ -0,0 +1,36 @@ +@foreach($features as $feature) +
+
+
+
+
+ @include('features._vote-button', ['feature' => $feature]) +
+ +
+
+ @if($feature->icon) + + @endif +
+
+ {{ $feature->title }} + @if($feature->isProposed()) + {{ $feature->status->text() }} + @endif +
+

{{ $feature->description }}

+ + Предложил + • {{ $feature->created_at->diffForHumans() }} + +
+
+
+
+
+
+
+@endforeach diff --git a/resources/views/features/_pagination.blade.php b/resources/views/features/_pagination.blade.php new file mode 100644 index 00000000..39feb603 --- /dev/null +++ b/resources/views/features/_pagination.blade.php @@ -0,0 +1,30 @@ +@if($features->hasMorePages()) + + + @foreach(range(0,2) as $placeholder) +
+
+
+
+
+ + +
+ +
+ + + + + +
+
+
+
+
+ @endforeach +
+@endif \ No newline at end of file diff --git a/resources/views/features/_search-results.blade.php b/resources/views/features/_search-results.blade.php new file mode 100644 index 00000000..e40f39f3 --- /dev/null +++ b/resources/views/features/_search-results.blade.php @@ -0,0 +1,12 @@ + +@if($features->isEmpty()) +
+
+ +

Ничего не найдено

+
+
+@else + @include('features._list') +@endif +
diff --git a/resources/views/features/_vote-button.blade.php b/resources/views/features/_vote-button.blade.php new file mode 100644 index 00000000..066d8f08 --- /dev/null +++ b/resources/views/features/_vote-button.blade.php @@ -0,0 +1,38 @@ +
+ @if($feature->status->value === 'implemented') +
+ +
{{ $feature->votes_count }}
+ Реализовано +
+ @else + @auth + @if(($feature->user_vote ?? 0) === 1) +
+ +
{{ $feature->votes_count }}
+ Вы проголосовали +
+ @else +
+ @csrf + + +
+
{{ $feature->votes_count }}
+ @endif + @else + +
{{ $feature->votes_count }}
+ @endauth + @endif +
\ No newline at end of file diff --git a/resources/views/features/index.blade.php b/resources/views/features/index.blade.php new file mode 100644 index 00000000..cf3726ca --- /dev/null +++ b/resources/views/features/index.blade.php @@ -0,0 +1,134 @@ +@extends('layout') +@section('title', 'Голосование за функции') +@section('description', 'Предлагайте новые функции для сайта и голосуйте за идеи других пользователей') + +@section('content') + +
+
+
+

Голосование за фичу

+

Предлагайте новые фичи и голосуйте за идеи, которые хотите видеть на сайте

+
+ + @auth +
+ +
+ @else +
+ + Войдите, чтобы предлагать новые функции и голосовать +
+ @endauth + +
+ +
+
+
+ + + + +
+
+
+ +
+ + @if($features->isEmpty()) +
+
+ +

Пока нет идей

+ @auth + + @endauth +
+
+ @else + @include('features._list') + @endif +
+ + @include('features._pagination') +
+
+
+
+
+ + @auth + + + @endauth +@endsection diff --git a/routes/platform.php b/routes/platform.php index ff4b920b..4a8c694d 100644 --- a/routes/platform.php +++ b/routes/platform.php @@ -122,3 +122,21 @@ ->breadcrumbs(fn (Trail $trail) => $trail ->parent('platform.index') ->push('Серетный Санта')); + +Route::screen('features', App\Orchid\Screens\Feature\ListScreen::class) + ->name('platform.feature') + ->breadcrumbs(fn (Trail $trail) => $trail + ->parent('platform.index') + ->push('Предложения функций')); + +Route::screen('features/{feature}/edit', App\Orchid\Screens\Feature\EditScreen::class) + ->name('platform.feature.edit') + ->breadcrumbs(fn (Trail $trail, $feature) => $trail + ->parent('platform.feature') + ->push($feature->title ?? 'Создание', route('platform.feature.edit', $feature))); + +Route::screen('features/create', App\Orchid\Screens\Feature\EditScreen::class) + ->name('platform.feature.create') + ->breadcrumbs(fn (Trail $trail) => $trail + ->parent('platform.feature') + ->push('Создать', route('platform.feature.create')));