diff --git a/app/Http/Controllers/Forum/CategoriesController.php b/app/Http/Controllers/Forum/CategoriesController.php index d102f3c..0d4d443 100644 --- a/app/Http/Controllers/Forum/CategoriesController.php +++ b/app/Http/Controllers/Forum/CategoriesController.php @@ -3,9 +3,15 @@ namespace App\Http\Controllers\Forum; use App\Http\Controllers\Controller; +use App\Models\Forum\Category; use Illuminate\Http\Request; class CategoriesController extends Controller { - // + public function index() + { + $categories = Category::with('threads')->get(); + + return view('forum.index', compact('categories')); + } } diff --git a/app/Http/Controllers/MediaListController.php b/app/Http/Controllers/MediaListController.php index ccdec60..1d61d63 100644 --- a/app/Http/Controllers/MediaListController.php +++ b/app/Http/Controllers/MediaListController.php @@ -5,6 +5,8 @@ namespace App\Http\Controllers; use App\Http\Requests\MediaListRequest; use App\Models\MediaList; use App\Models\MediaListItem; +use App\Models\Movie; +use App\Models\Show; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -12,27 +14,31 @@ use Illuminate\Support\Str; class MediaListController extends Controller { - public function index() + public function index(string $slug) { $lists = MediaList::whereNotIn('name', MediaList::SPECIAL_LISTS) ->where('user_id', Auth::user()->id) ->get(); - return view('profile.lists.index', compact('lists')); + $user = User::where('slug', $slug)->first(); + + return view('profile.lists.index', compact('lists', 'user')); } public function show(string $userSlug, string $listSlug) { $user = User::where('slug', $userSlug)->first(); $list = MediaList::where('user_id', $user->id)->where('slug', $listSlug)->first(); - $items = MediaListItem::where('media_list_id', $list->id)->paginate(15); + $items = MediaListItem::with('item')->where('media_list_id', $list->id)->paginate(15); - return view('profile.lists.show', compact('list', 'items')); + return view('profile.lists.show', compact('list', 'items', 'user')); } public function create() { - return view('profile.lists.create'); + $user = Auth::user(); + + return view('profile.lists.create', compact('user')); } public function store(MediaListRequest $request) @@ -48,4 +54,49 @@ class MediaListController extends Controller return redirect()->route('profile.list.show', [Auth::user()->slug, $slug]); } + + public function toggleListItem(string $listSlug, int $itemId, string $itemType) + { + $mediaList = MediaList::where('slug', $listSlug) + ->where('user_id', Auth::user()->id) + ->first(); + + if ($itemType == 'movie') { + $item = Movie::find($itemId); + } else { + $item = Show::find($itemId); + } + + if (!$item) { + return response()->json([ + 'success' => false + ]); + } + + $mediaListItem = MediaListItem::where('media_list_id', $mediaList->id) + ->where('item_id', $itemId) + ->where('item_type', $itemType) + ->first(); + + if ($mediaListItem) { + $mediaListItem->delete(); + + return response()->json([ + 'success' => true, + 'added' => false, + ]); + } + + MediaListItem::create([ + 'media_list_id' => $mediaList->id, + 'item_id' => $itemId, + 'item_type' => $itemType, + 'tmdb_id' => $item->tmdb_id, + ]); + + return response()->json([ + 'success' => true, + 'added' => true, + ]); + } } diff --git a/app/Http/Controllers/RatingController.php b/app/Http/Controllers/RatingController.php new file mode 100644 index 0000000..0b10768 --- /dev/null +++ b/app/Http/Controllers/RatingController.php @@ -0,0 +1,28 @@ + auth()->user()->id, + 'item_id' => $itemId, + 'item_type' => $itemType, + ], + [ + 'rate' => $rate + ] + ); + + return response()->json([ + 'success' => true, + 'rate' => $rate, + ]); + } +} diff --git a/app/Models/Forum/Category.php b/app/Models/Forum/Category.php index 9a973d1..ebe74f8 100644 --- a/app/Models/Forum/Category.php +++ b/app/Models/Forum/Category.php @@ -6,5 +6,10 @@ use Illuminate\Database\Eloquent\Model; class Category extends Model { - // + protected $table = 'forum_categories'; + + public function threads() + { + return $this->hasMany(Thread::class, 'category_id', 'id'); + } } diff --git a/app/Models/Forum/Thread.php b/app/Models/Forum/Thread.php index d8b21cc..c175386 100644 --- a/app/Models/Forum/Thread.php +++ b/app/Models/Forum/Thread.php @@ -6,5 +6,5 @@ use Illuminate\Database\Eloquent\Model; class Thread extends Model { - // + protected $table = 'forum_threads'; } diff --git a/app/Models/MediaListItem.php b/app/Models/MediaListItem.php index 5386f1f..454ab3d 100644 --- a/app/Models/MediaListItem.php +++ b/app/Models/MediaListItem.php @@ -12,4 +12,13 @@ class MediaListItem extends Model 'item_type', 'tmdb_id', ]; + + public function item() + { + if ($this->item_type == 'movie') { + return $this->hasOne(Movie::class, 'id', 'item_id'); + } + + return $this->hasOne(Show::class, 'id', 'item_id'); + } } diff --git a/app/Models/Movie.php b/app/Models/Movie.php index b753b06..4c65afd 100644 --- a/app/Models/Movie.php +++ b/app/Models/Movie.php @@ -32,6 +32,32 @@ class Movie extends Model 'release_date' => 'date' ]; + public function wasListedBy(string $listSlug, User $user) + { + $mediaList = MediaList::where('slug', $listSlug)->where('user_id', $user->id)->first(); + + if ($mediaList) { + $mediaListItem = MediaListItem::where('media_list_id', $mediaList->id) + ->where('item_id', $this->id) + ->where('item_type', 'movie') + ->first(); + + return !empty($mediaListItem); + } + + return false; + } + + public function ratingBy(User $user) + { + $rating = Rate::where('user_id', $user->id) + ->where('item_id', $this->id) + ->where('item_type', 'movie') + ->first(); + + return $rating ? $rating->rate : null; + } + public function director() { return $this->belongsToMany(Person::class, 'movie_cast', 'movie_id', 'person_id') diff --git a/app/Models/Rate.php b/app/Models/Rate.php new file mode 100644 index 0000000..aaa0284 --- /dev/null +++ b/app/Models/Rate.php @@ -0,0 +1,17 @@ +where('tmdb_id', $tmdbReview['id']) ->first(); + // @todo create user + $user = User::where('name', $tmdbReview['author'])->first(); + + if (!$user) { + $user = User::create([ + 'name' => $tmdbReview['author'], + 'slug' => Str::slug($tmdbReview['author']), + 'email' => $tmdbReview['author'].'@nowhere.wtf', + 'password' => 'no_password_neeeded_here', + 'is_dummy_account' => true, + ]); + } + + if (!$existingReview) { Comment::create([ 'entity_id' => $movie->id, 'entity_type' => Comment::TYPE_MOVIE, + 'user_id' => $user->id, 'author_name' => $tmdbReview['author'], 'author_avatar' => $tmdbReview['author_details']['avatar_path'], 'author_rating' => $tmdbReview['author_details']['rating'], diff --git a/app/Service/ToolService.php b/app/Service/ToolService.php new file mode 100644 index 0000000..550adfa --- /dev/null +++ b/app/Service/ToolService.php @@ -0,0 +1,18 @@ +path(); + $uri = ltrim(str_replace(env('APP_URL'), '', $routePath), '/'); + + return $currentPath == $uri; + } +} diff --git a/database/migrations/2025_01_27_191007_create_threads_table.php b/database/migrations/2025_01_27_191007_create_threads_table.php index 691ce56..37f2ab7 100644 --- a/database/migrations/2025_01_27_191007_create_threads_table.php +++ b/database/migrations/2025_01_27_191007_create_threads_table.php @@ -16,8 +16,8 @@ return new class extends Migration $table->unsignedBigInteger('category_id'); $table->string('name'); $table->string('slug'); - $table->string('icon'); - $table->text('description'); + $table->string('icon')->nullable(); + $table->text('description')->nullable(); $table->integer('order')->default(0); $table->timestamps(); }); diff --git a/database/migrations/2025_01_31_221518_create_rates_table.php b/database/migrations/2025_01_31_221518_create_rates_table.php new file mode 100644 index 0000000..7b56bed --- /dev/null +++ b/database/migrations/2025_01_31_221518_create_rates_table.php @@ -0,0 +1,31 @@ +id(); + $table->unsignedBigInteger('item_id'); + $table->enum('item_type', ['movie', 'show']); + $table->decimal('rate', 10, 2); + $table->unsignedBigInteger('user_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ratings'); + } +}; diff --git a/package-lock.json b/package-lock.json index 455d8f8..defe277 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,9 @@ "requires": true, "packages": { "": { + "dependencies": { + "jquery": "^3.7.1" + }, "devDependencies": { "@popperjs/core": "^2.11.6", "autoprefixer": "^10.4.20", @@ -2065,6 +2068,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, "node_modules/laravel-vite-plugin": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.1.1.tgz", diff --git a/package.json b/package.json index 5a70f21..8725fd7 100644 --- a/package.json +++ b/package.json @@ -16,5 +16,8 @@ "sass": "^1.56.1", "tailwindcss": "^3.4.13", "vite": "^6.0" + }, + "dependencies": { + "jquery": "^3.7.1" } } diff --git a/public/img/email-pattern.webp b/public/img/email-pattern.webp new file mode 100644 index 0000000..019ef43 Binary files /dev/null and b/public/img/email-pattern.webp differ diff --git a/public/img/folk-pattern.webp b/public/img/folk-pattern.webp new file mode 100644 index 0000000..badec3c Binary files /dev/null and b/public/img/folk-pattern.webp differ diff --git a/public/img/funky-lines.webp b/public/img/funky-lines.webp new file mode 100644 index 0000000..9385220 Binary files /dev/null and b/public/img/funky-lines.webp differ diff --git a/public/img/greek-vase.webp b/public/img/greek-vase.webp new file mode 100644 index 0000000..dc7e7f2 Binary files /dev/null and b/public/img/greek-vase.webp differ diff --git a/public/img/hypnotize.webp b/public/img/hypnotize.webp new file mode 100644 index 0000000..0afc58f Binary files /dev/null and b/public/img/hypnotize.webp differ diff --git a/public/img/leaves-pattern.webp b/public/img/leaves-pattern.webp new file mode 100644 index 0000000..efe7172 Binary files /dev/null and b/public/img/leaves-pattern.webp differ diff --git a/public/img/star_rating.png b/public/img/star_rating.png new file mode 100644 index 0000000..5a47a34 Binary files /dev/null and b/public/img/star_rating.png differ diff --git a/public/img/user_no_avatar.png b/public/img/user_no_avatar.png new file mode 100644 index 0000000..9d8e08a Binary files /dev/null and b/public/img/user_no_avatar.png differ diff --git a/public/uploads/forums/categories/cinema.png b/public/uploads/forums/categories/cinema.png new file mode 100644 index 0000000..10c53f7 Binary files /dev/null and b/public/uploads/forums/categories/cinema.png differ diff --git a/public/uploads/forums/categories/film-camera.png b/public/uploads/forums/categories/film-camera.png new file mode 100644 index 0000000..f06dbe8 Binary files /dev/null and b/public/uploads/forums/categories/film-camera.png differ diff --git a/public/uploads/forums/categories/popcorn.png b/public/uploads/forums/categories/popcorn.png new file mode 100644 index 0000000..98fbe26 Binary files /dev/null and b/public/uploads/forums/categories/popcorn.png differ diff --git a/resources/js/app.js b/resources/js/app.js index e59d6a0..2439f8b 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1 +1,62 @@ import './bootstrap'; +import './star_rating'; + +import $ from 'jquery'; + +$(".my-rating").starRating({ + starSize: 20, + totalStars: 10, + callback: function(currentRating, $el){ + let type = $($el[0]).data('type'); + let id = $($el[0]).data('id'); + + let url = '/rate/'+type+'/'+id+'/'+currentRating; + + var self = this + + $.ajax({ + url: url, + method: 'GET', + success: function(data){ + $(self).attr('data-rating', currentRating) + } + }); + } +}); + +$('.mark-as-seen-btn').on('click', function (e) { + e.preventDefault(); + let url = '/lists/' + $(this).data('list') + '/' + $(this).data('id') + '/' + $(this).data('type'); + var self = this + $.ajax({ + url: url, + method: 'GET', + success: function(data){ + if(data.added) { + $(self).addClass('seen'); + } else { + $(self).removeClass('seen'); + } + + } + }); + return false; +}); + +$('.like-btn').on('click', function (e) { + e.preventDefault(); + let url = '/lists/' + $(this).data('list') + '/' + $(this).data('id') + '/' + $(this).data('type'); + var self = this + $.ajax({ + url: url, + method: 'GET', + success: function(data){ + if(data.added) { + $(self).addClass('liked'); + } else { + $(self).removeClass('liked'); + } + } + }); + return false; +}); diff --git a/resources/js/star_rating.js b/resources/js/star_rating.js new file mode 100644 index 0000000..5195de7 --- /dev/null +++ b/resources/js/star_rating.js @@ -0,0 +1,343 @@ +import $ from 'jquery'; + +/* + * jQuery StarRatingSvg v1.2.0 + * + * http://github.com/nashio/star-rating-svg + * Author: Ignacio Chavez + * hello@ignaciochavez.com + * Licensed under MIT + */ + +;(function ( $, window, document, undefined ) { + + 'use strict'; + + // Create the defaults once + var pluginName = 'starRating'; + var noop = function(){}; + var defaults = { + totalStars: 5, + useFullStars: false, + starShape: 'straight', + emptyColor: 'lightgray', + hoverColor: 'orange', + activeColor: 'gold', + ratedColor: 'crimson', + useGradient: true, + readOnly: false, + disableAfterRate: true, + baseUrl: false, + starGradient: { + start: '#FEF7CD', + end: '#FF9511' + }, + strokeWidth: 4, + strokeColor: 'black', + initialRating: 0, + starSize: 40, + callback: noop, + onHover: noop, + onLeave: noop + }; + + // The actual plugin constructor + var Plugin = function( element, options ) { + var _rating; + var newRating; + var roundFn; + + this.element = element; + this.$el = $(element); + this.settings = $.extend( {}, defaults, options ); + + // grab rating if defined on the element + _rating = this.$el.data('rating') || this.settings.initialRating; + + // round to the nearest half + roundFn = this.settings.forceRoundUp ? Math.ceil : Math.round; + newRating = (roundFn( _rating * 2 ) / 2).toFixed(1); + this._state = { + rating: newRating + }; + + // create unique id for stars + this._uid = Math.floor( Math.random() * 999 ); + + // override gradient if not used + if( !options.starGradient && !this.settings.useGradient ){ + this.settings.starGradient.start = this.settings.starGradient.end = this.settings.activeColor; + } + + this._defaults = defaults; + this._name = pluginName; + this.init(); + }; + + var methods = { + init: function () { + this.renderMarkup(); + this.addListeners(); + this.initRating(); + }, + + addListeners: function(){ + if( this.settings.readOnly ){ return; } + this.$stars.on('mouseover', this.hoverRating.bind(this)); + this.$stars.on('mouseout', this.restoreState.bind(this)); + this.$stars.on('click', this.handleRating.bind(this)); + }, + + // apply styles to hovered stars + hoverRating: function(e){ + var index = this.getIndex(e); + this.paintStars(index, 'hovered'); + this.settings.onHover(index + 1, this._state.rating, this.$el); + }, + + // clicked on a rate, apply style and state + handleRating: function(e){ + var index = this.getIndex(e); + var rating = index + 1; + + this.applyRating(rating, this.$el); + this.executeCallback( rating, this.$el ); + + if(this.settings.disableAfterRate){ + this.$stars.off(); + } + }, + + applyRating: function(rating){ + var index = rating - 1; + // paint selected and remove hovered color + this.paintStars(index, 'rated'); + this._state.rating = index + 1; + this._state.rated = true; + }, + + restoreState: function(e){ + var index = this.getIndex(e); + var rating = this._state.rating || -1; + // determine star color depending on manually rated + var colorType = this._state.rated ? 'rated' : 'active'; + this.paintStars(rating - 1, colorType); + this.settings.onLeave(index + 1, this._state.rating, this.$el); + }, + + getIndex: function(e){ + var $target = $(e.currentTarget); + var width = $target.width(); + var side = $(e.target).attr('data-side'); + var minRating = this.settings.minRating; + + // hovered outside the star, calculate by pixel instead + side = (!side) ? this.getOffsetByPixel(e, $target, width) : side; + side = (this.settings.useFullStars) ? 'right' : side ; + + // get index for half or whole star + var index = $target.index() - ((side === 'left') ? 0.5 : 0); + + // pointer is way to the left, rating should be none + index = ( index < 0.5 && (e.offsetX < width / 4) ) ? -1 : index; + + // force minimum rating + index = ( minRating && minRating <= this.settings.totalStars && index < minRating ) ? minRating - 1 : index; + return index; + }, + + getOffsetByPixel: function(e, $target, width){ + var leftX = e.pageX - $target.offset().left; + return ( leftX <= (width / 2) && !this.settings.useFullStars) ? 'left' : 'right'; + }, + + initRating: function(){ + this.paintStars(this._state.rating - 1, 'active'); + }, + + paintStars: function(endIndex, stateClass){ + var $polygonLeft; + var $polygonRight; + var leftClass; + var rightClass; + var s = this.settings; + + $.each(this.$stars, function(index, star){ + $polygonLeft = $(star).find('[data-side="left"]'); + $polygonRight = $(star).find('[data-side="right"]'); + leftClass = rightClass = (index <= endIndex) ? stateClass : 'empty'; + + // has another half rating, add half star + leftClass = ( index - endIndex === 0.5 ) ? stateClass : leftClass; + + $polygonLeft.attr('class', 'svg-' + leftClass + '-' + this._uid); + $polygonRight.attr('class', 'svg-' + rightClass + '-' + this._uid); + + // get color for level + var ratedColorsIndex = endIndex >= 0 ? Math.ceil(endIndex) : 0; + + var ratedColor; + if (s.ratedColors && s.ratedColors.length && s.ratedColors[ratedColorsIndex]) { + ratedColor = s.ratedColors[ratedColorsIndex]; + } else { + ratedColor = this._defaults.ratedColor; + } + + // only override colors in rated stars and when rated number is valid + if (stateClass === 'rated' && endIndex > -1) { + // limit to painting only to rated stars, and specific case for half star + if (index <= Math.ceil(endIndex) || (index < 1 && endIndex < 0)) { + $polygonLeft.attr('style', 'fill:'+ratedColor); + } + if (index <= endIndex) { + $polygonRight.attr('style', 'fill:'+ratedColor); + } + } + }.bind(this)); + }, + + renderMarkup: function () { + var s = this.settings; + var baseUrl = s.baseUrl ? location.href.split('#')[0] : ''; + + // inject an svg manually to have control over attributes + var star = '
'; + + // inject svg markup + var starsMarkup = ''; + for( var i = 0; i < s.totalStars; i++){ + starsMarkup += star; + } + this.$el.append(starsMarkup); + this.$stars = this.$el.find('.jq-star'); + }, + + getVectorPath: function(id, attrs){ + return (attrs.starShape === 'rounded') ? + this.getRoundedVectorPath(id, attrs) : this.getSpikeVectorPath(id, attrs); + }, + + getSpikeVectorPath: function(id, attrs){ + return '{{ $celebrity->biography }}
- info movies + @if($celebrity->also_known_as) ++ Also known as: + @foreach(json_decode($celebrity->also_known_as ?? "{}", true) as $alsoKnown) + {{ $alsoKnown }} + @if(!$loop->last), @endif + @endforeach +
+ @endif + popularity /