Add post deletion w/ confirmation, search (broken), profile page

This commit is contained in:
yuriko 🦊 2025-05-24 19:30:44 -04:00
parent bfb497c367
commit 827d125639
21 changed files with 374 additions and 27 deletions

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Comment;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -19,4 +20,12 @@ class DeletionController extends Controller
$comment->delete();
return redirect("/posts/$postid");
}
public function deletePost(Post $post)
{
$post->featured = null;
$post->save();
$post->delete();
return redirect()->route('posts.home');
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace App\Livewire\Pages;
use App\Models\Comment;
use App\Models\Post;
use App\Models\User;
use Livewire\Component;
class Profile extends Component
{
public User $user;
public function render()
{
$favorite_posts = $this->user->favorites()->withType(Post::class)->count();
$favorite_comments = $this->user->favorites()->withType(Comment::class)->count();
return view('livewire.pages.profile', [
'favorite_posts' => $favorite_posts, 'favorite_comments' => $favorite_comments
])->title($this->user->name);
}
}

View file

@ -15,7 +15,7 @@ class Upload extends Component
{
use WithFileUploads;
#[Validate('extensions:jpg,jpeg,bmp,gif,png,webp,apng,mp4,wmv,mkv|mimes:jpg,jpeg,bmp,gif,png,webp,apng,mp4,wmv,mkv|max:81920')]
#[Validate('file')]
public $file;
#[Validate('required|in:safe,suggestive,explicit')]

29
app/Livewire/Search.php Normal file
View file

@ -0,0 +1,29 @@
<?php
namespace App\Livewire;
use App\Models\User;
use Livewire\Attributes\Validate;
use Livewire\Component;
use Spatie\Searchable\ModelSearchAspect;
use Spatie\Searchable\Search as SpatieSearch;
class Search extends Component
{
#[Validate('string|min:3')]
public string $searchText = '';
public $searchResults = [];
public function render()
{
return view('livewire.search');
}
public function updated()
{
$this->searchResults = (new SpatieSearch())
->registerModel(User::class, 'name')
->perform($this->searchText);
}
}

View file

@ -5,10 +5,11 @@ namespace App\Models;
use MongoDB\Laravel\Eloquent\Model;
use MongoDB\Laravel\Eloquent\SoftDeletes;
use MongoDB\Laravel\Relations\BelongsTo;
use Overtrue\LaravelFavorite\Traits\Favoriteable;
class Comment extends Model
{
use SoftDeletes;
use SoftDeletes, Favoriteable;
protected $fillable = [ 'message' ];

View file

@ -9,11 +9,12 @@ use MongoDB\Laravel\Eloquent\SoftDeletes;
use MongoDB\Laravel\Relations\BelongsTo;
use MongoDB\Laravel\Relations\BelongsToMany;
use MongoDB\Laravel\Relations\HasMany;
use Overtrue\LaravelFavorite\Traits\Favoriteable;
use Symfony\Component\HttpFoundation\StreamedResponse;
class Post extends Model
{
use SoftDeletes;
use SoftDeletes, Favoriteable;
protected $fillable = [ 'rating', 'extension', 'featured' ];

View file

@ -2,19 +2,21 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use MongoDB\Laravel\Auth\User as Authenticatable;
use MongoDB\Laravel\Relations\HasMany;
use Overtrue\LaravelFavorite\Traits\Favoriter;
use Spatie\Searchable\Searchable;
use Spatie\Searchable\SearchResult;
class User extends Authenticatable
class User extends Authenticatable implements Searchable
{
protected $connection = 'mongodb';
protected $table = 'users';
use HasApiTokens, HasFactory, Notifiable;
use HasApiTokens, HasFactory, Notifiable, Favoriter;
protected $fillable = [
'name',
@ -43,4 +45,14 @@ class User extends Authenticatable
{
return $this->hasMany(Comment::class);
}
public function getSearchResult(): SearchResult
{
$url = url("/profiles/$this->id");
return new SearchResult(
$this,
$this->name,
$url
);
}
}

View file

@ -19,8 +19,10 @@
"league/flysystem-gridfs": "3.x-dev",
"livewire/livewire": "^3.6",
"mongodb/laravel-mongodb": "^5.4",
"overtrue/laravel-favorite": "^5.3",
"predis/predis": "^3.0",
"socialiteproviders/authentik": "^5.2"
"socialiteproviders/authentik": "^5.2",
"spatie/laravel-searchable": "^1.13"
},
"require-dev": {
"fakerphp/faker": "^1.23",

138
composer.lock generated
View file

@ -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": "91ba0ac099738f27d18cf80cec4b7d41",
"content-hash": "00ff44f83137e0e396a391da23ecfde8",
"packages": [
{
"name": "brick/math",
@ -3313,6 +3313,76 @@
],
"time": "2025-05-08T08:14:37+00:00"
},
{
"name": "overtrue/laravel-favorite",
"version": "5.3.2",
"source": {
"type": "git",
"url": "https://github.com/overtrue/laravel-favorite.git",
"reference": "de63bc6f45cbdbda8f717e38e859b25e0a20995c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/overtrue/laravel-favorite/zipball/de63bc6f45cbdbda8f717e38e859b25e0a20995c",
"reference": "de63bc6f45cbdbda8f717e38e859b25e0a20995c",
"shasum": ""
},
"require": {
"laravel/framework": "^9.0|^10.0|^11.0|^12.0",
"php": "^8.0.2"
},
"require-dev": {
"brainmaestro/composer-git-hooks": "dev-master",
"friendsofphp/php-cs-fixer": "^3.5",
"laravel/pint": "^1.2",
"mockery/mockery": "^1.4.4",
"orchestra/testbench": "^8.0|^9.0|^10.0",
"phpunit/phpunit": "^10.0.0|^11.5.3"
},
"type": "library",
"extra": {
"hooks": {
"pre-push": [
"composer test"
],
"pre-commit": [
"composer fix-style"
]
},
"laravel": {
"providers": [
"Overtrue\\LaravelFavorite\\FavoriteServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Overtrue\\LaravelFavorite\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "overtrue",
"email": "anzhengchao@gmail.com"
}
],
"description": "User favorite features for Laravel Application.",
"support": {
"issues": "https://github.com/overtrue/laravel-favorite/issues",
"source": "https://github.com/overtrue/laravel-favorite/tree/5.3.2"
},
"funding": [
{
"url": "https://github.com/overtrue",
"type": "github"
}
],
"time": "2025-03-23T04:56:07+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.0.0",
@ -4505,6 +4575,72 @@
},
"time": "2025-02-24T19:33:30+00:00"
},
{
"name": "spatie/laravel-searchable",
"version": "1.13.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-searchable.git",
"reference": "7821e4c72277133cf541ea181724af64b2b972c5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-searchable/zipball/7821e4c72277133cf541ea181724af64b2b972c5",
"reference": "7821e4c72277133cf541ea181724af64b2b972c5",
"shasum": ""
},
"require": {
"ext-pdo": "*",
"laravel/framework": "^8.78|^9.0|^10.0|^11.0|^12.0",
"php": "^7.3|^8.0"
},
"require-dev": {
"larapack/dd": "^1.0",
"orchestra/testbench": "^6.27|^7.0|^8.0|^9.0|^10.0",
"phpunit/phpunit": "^9.3|^10.0|^11.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Spatie\\Searchable\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Alex Vanderbist",
"email": "alex@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
},
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Pragmatically search through models and other sources",
"homepage": "https://github.com/spatie/laravel-searchable",
"keywords": [
"laravel-searchable",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-searchable/issues",
"source": "https://github.com/spatie/laravel-searchable/tree/1.13.0"
},
"funding": [
{
"url": "https://spatie.be/open-source/support-us",
"type": "custom"
}
],
"time": "2025-02-25T15:59:46+00:00"
},
{
"name": "symfony/clock",
"version": "v7.2.0",

28
config/favorite.php Normal file
View file

@ -0,0 +1,28 @@
<?php
return [
/**
* Use uuid as primary key.
*/
'uuids' => false,
/*
* User tables foreign key name.
*/
'user_foreign_key' => 'user_id',
/*
* Table name for favorites records.
*/
'favorites_table' => 'favorites',
/*
* Model name for favorite record.
*/
'favorite_model' => Overtrue\LaravelFavorite\Favorite::class,
/*
* Model name for favoriter model.
*/
'favoriter_model' => App\Models\User::class,
];

View file

@ -64,13 +64,12 @@ return [
*/
'temporary_file_upload' => [
'disk' => 'gridfs', // Example: 'local', 's3' | Default: 'default'
'rules' => ['required', 'file', 'max:20480'], // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'disk' => null, // Example: 'local', 's3' | Default: 'default'
'rules' => ['required', 'file', 'max:12288'], // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
'png', 'gif', 'bmp', 'svg', 'mp4',
'mov', 'avi', 'wmv', 'mp3',
'png', 'gif', 'bmp', 'svg',
'jpg', 'jpeg', 'webp',
],
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\Schema;
use MongoDB\Laravel\Schema\Blueprint;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create(config('favorite.favorites_table'), function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger(config('favorite.user_foreign_key'))->index()->comment('user_id');
$table->morphs('favoriteable');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists(config('favorite.favorites_table'));
}
};

View file

@ -7,9 +7,7 @@
<h1>{{ config('app.name') }}</h1>
@auth
<wa-input placeholder="Search for posts, tags, users, etc.">
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
</wa-input>
<livewire:search />
@endauth
</div>

View file

@ -35,7 +35,7 @@
@endguest
@auth
<wa-button appearance="plain">{{ Auth::user()->name }}</wa-button>
<wa-button href="{{ url('/profiles/' . Auth::id()) }}" appearance="plain" wire:navigate.hover>{{ Auth::user()->name }}</wa-button>
<wa-icon-button href="{{ route('logout') }}" appearance="plain" name="arrow-left-from-bracket"></wa-icon-button>
@endauth
</div>

View file

@ -0,0 +1,28 @@
<div class="wa-stack">
<h1>{{ $user->name }}</h1>
<div class="wa-grid">
<wa-card>
<span class="wa-heading-m">
<wa-format-number value="{{ $user->posts->count() }}"></wa-format-number> {{ Str::plural('post', $user->posts->count()) }}
</span>
<span>
<wa-format-number value="{{ $favorite_posts }}"></wa-format-number> {{ Str::plural('favorite', $favorite_posts) }}
</span>
</wa-card>
<wa-card>
<span class="wa-heading-m">
<wa-format-number value="{{ $user->comments->count() }}"></wa-format-number> {{ Str::plural('comment', $user->comments->count()) }}
</span>
</wa-card>
<wa-card>
<span class="wa-heading-m">
Last seen {{ $user->updated_at->diffForHumans() }}
</span>
</wa-card>
</div>
</div>

View file

@ -1,17 +1,12 @@
<div class="wa-flank wa-align-items-start wa-gap-3xl" style="--flank-size: 20rem;">
{{-- Sidebar --}}
<div class="wa-stack" wire:poll>
<div class="wa-stack">
<div class="wa-cluster">
<span class="wa-caption-m">Changes are automatically saved.</span>
<wa-button href="{{ url("posts/$post->id") }}" appearance="outlined" variant="neutral" size="small" wire:navigate.hover>
<wa-icon slot="prefix" name="check"></wa-icon>
<span>Finish</span>
</wa-button>
</div>
<wa-divider></wa-divider>
<wa-callout variant="brand" appearance="outlined">
<wa-icon slot="icon" name="circle-info"></wa-icon>
<span>Changes are automatically saved.</span>
</wa-callout>
<form wire:submit class="wa-stack">
<wa-select wire:model.live="rating" label="Rating" value="{{ $post->rating }}" wire:loading.attr="disabled" @error('rating') hint="{{ $message }}" @enderror>
@ -39,11 +34,46 @@
<span>Tags</span>
</div>
<wa-divider></wa-divider>
<div class="wa-split">
<wa-button href="{{ url("posts/$post->id") }}" appearance="outlined" variant="neutral" size="small" wire:navigate.hover>
<wa-icon slot="prefix" name="arrow-left"></wa-icon>
<span>Exit</span>
</wa-button>
<wa-button appearance="outlined" variant="danger" size="small" wire:click="$js.openDialog">
<wa-icon slot="prefix" name="trash"></wa-icon>
<span>Delete post</span>
</wa-button>
</div>
</div>
{{-- Main content --}}
<div class="wa-stack wa-gap-2xl" wire:poll>
<div class="wa-stack wa-gap-2xl">
<livewire:posts.image :$post lazy />
</div>
{{-- Confirm deletion dialog --}}
<wa-dialog label="Confirm post deletion" without-header light-dismiss id="modal-confirm-post-delete" style="--width: 360px;">
<div class="wa-stack wa-align-items-center">
<span>Are you sure you want to delete this post?</span>
<div class="wa-split">
<wa-button variant="neutral" appearance="outlined" data-dialog="close">
No, go back
</wa-button>
<wa-button href="{{ url("delete/post/$post->id") }}" appearance="outlined" variant="danger" wire:navigate>
Yes, delete it
</wa-button>
</div>
</div>
</wa-dialog>
@script
<script>
const dialog = document.querySelector('#modal-confirm-post-delete');
$js('openDialog', () => { dialog.open = true})
</script>
@endscript
</div>

View file

@ -0,0 +1,19 @@
<form wire:submit>
<wa-input wire:model.live="searchText" type="text" placeholder="Search for posts, tags, users, etc.">
<wa-icon slot="prefix" name="magnifying-glass"></wa-icon>
</wa-input>
<wa-popup
placement="bottom-end"
distance="10"
sync="width"
auto-size="vertical"
auto-size-padding="10"
{{ $searchResults ? 'active' : '' }}>
<wa-card>
@isset($searchResults)
<pre><code>{{ var_dump($searchResults) }}</code></pre>
@endisset
</wa-card>
</wa-popup>
</form>

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\DeletionController;
use App\Livewire\App\Home as AppHome;
use App\Livewire\Pages\Profile as ProfilePage;
use App\Livewire\Pages\Upload as UploadPage;
use App\Livewire\Posts\Index as PostsPage;
use App\Livewire\Posts\Edit as EditPost;
@ -15,6 +16,7 @@ Route::get('/', AppHome::class)->name('home');
// Authenticated routes
Route::middleware('auth')->group(function () {
Route::get('/upload', UploadPage::class)->name('upload');
Route::get('/profiles/{user}', ProfilePage::class);
});
// Post routes
@ -30,4 +32,5 @@ Route::middleware('auth')->prefix('posts')->group(function () {
// Object deletion routes
Route::middleware('auth')->prefix('delete')->controller(DeletionController::class)->group(function () {
Route::get('comment/{comment}', 'deleteComment');
Route::get('post/{post}', 'deletePost');
});

0
storage/app/.gitignore vendored Normal file → Executable file
View file

0
storage/app/private/.gitignore vendored Normal file → Executable file
View file

0
storage/app/public/.gitignore vendored Normal file → Executable file
View file