From 216ecb402aa6346d8babe3fc141299726b64c1d9 Mon Sep 17 00:00:00 2001 From: abourtnik Date: Thu, 21 Dec 2023 20:25:48 +0100 Subject: [PATCH] feat + fixes --- app/Console/Commands/SyncComments.php | 117 ++++++++++++++--- app/Helpers/Parser.php | 53 ++++++++ app/Http/Controllers/CommentController.php | 23 ++-- app/Http/Controllers/SearchController.php | 9 +- app/Http/Resources/CommentResource.php | 14 +- app/Models/Comment.php | 23 ++-- app/Models/User.php | 14 +- app/Models/Video.php | 11 +- app/Policies/CommentPolicy.php | 2 +- app/Services/YoutubeService.php | 13 +- database/factories/UserFactory.php | 2 +- ..._12_19_173610_add_youtube_id_to_videos.php | 34 +++++ resources/js/components/Comments.jsx | 10 +- resources/js/components/Comments/Comment.jsx | 124 +++++++++++------- resources/js/components/Comments/Expand.jsx | 2 +- resources/js/components/Comments/Form.jsx | 8 +- .../js/components/Comments/ReplyForm.jsx | 4 +- resources/js/components/ImageLoaded.jsx | 4 +- resources/js/components/Replies.jsx | 10 +- resources/js/components/Video.jsx | 15 ++- resources/js/components/index.js | 2 +- resources/js/hooks.js | 8 +- resources/sass/video.scss | 29 ++-- resources/views/auth/login.blade.php | 4 +- resources/views/components/layout.blade.php | 3 + resources/views/pages/history.blade.php | 2 +- resources/views/pages/home.blade.php | 2 +- resources/views/pages/liked.blade.php | 2 +- resources/views/pages/trend.blade.php | 2 +- resources/views/playlists/show.blade.php | 38 +++--- resources/views/subscription/index.blade.php | 2 +- .../users/comments/modals/replies.blade.php | 2 +- resources/views/users/show.blade.php | 8 +- .../views/videos/card-secondary.blade.php | 8 +- resources/views/videos/card.blade.php | 10 +- resources/views/videos/card/card-3.blade.php | 26 ---- resources/views/videos/show.blade.php | 25 +++- routes/api.php | 26 ++-- 38 files changed, 463 insertions(+), 228 deletions(-) create mode 100644 app/Helpers/Parser.php create mode 100644 database/migrations/2023_12_19_173610_add_youtube_id_to_videos.php delete mode 100644 resources/views/videos/card/card-3.blade.php diff --git a/app/Console/Commands/SyncComments.php b/app/Console/Commands/SyncComments.php index c0bda2d..e1d2948 100644 --- a/app/Console/Commands/SyncComments.php +++ b/app/Console/Commands/SyncComments.php @@ -9,48 +9,59 @@ use Carbon\Carbon; use Illuminate\Console\Command; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class SyncComments extends Command { + + private YoutubeService $youtubeService; + + public function __construct(YoutubeService $youtubeService) + { + parent::__construct(); + $this->youtubeService = $youtubeService; + } + /** * The name and signature of the console command. * * @var string */ - protected $signature = 'comments:sync {id : ClipZone video id} {youtubeId : Youtube video id}'; + protected $signature = 'comments:sync'; /** * The console command description. * * @var string */ - protected $description = 'Get comments from Youtube for specific video'; + protected $description = 'Synchronize comments from Youtube'; /** * Execute the console command. * - * @param YoutubeService $youtubeService * @return int */ - public function handle(YoutubeService $youtubeService) : int + public function handle() : int { - list('id' => $id, 'youtubeId' => $youtubeId) = $this->arguments(); + $videos = Video::whereNotNull('youtube_id')->get(); - $video = Video::findOrFail($id); + foreach ($videos as $video) { - $video->comments->each->delete(); + $this->info('Sync comments for video : ' .$video->title. ' ...'); - $data = $youtubeService->getComments($youtubeId); + $video->comments->each->delete(); - $this->saveComments($data['items'], $video, null); + $data = $this->youtubeService->getComments($video->youtube_id); + + $this->saveComments($data['items'], $video, null); + } return Command::SUCCESS; } private function saveComments (array $items, Video $video, ?Comment $parent) { - $usedUsers = []; - $count = count($items); $randomCount = rand($count - 5, $count); @@ -61,11 +72,13 @@ private function saveComments (array $items, Video $video, ?Comment $parent) { $date = Carbon::create($comment['publishedAt']); - $users = $this->getUsers([$video->user_id], $date)->diff($usedUsers); - - $userId = $users->random(); - - $usedUsers[] = $userId; + // Author of video is author of comment + if ($comment['channelId'] === $comment['authorChannelId']['value']) { + $userId = $video->user_id; + } else { + $user = $this->importUser($comment['authorChannelId']['value']); + $userId = $user->id; + } $savedComment = Comment::withoutEvents(function () use ($video, $comment, $userId, $date, $parent) { $data = [ @@ -85,7 +98,7 @@ private function saveComments (array $items, Video $video, ?Comment $parent) { // Add Comment interaction $this->generateInteraction($savedComment, $randomCount, $date); - $randomCount = rand($randomCount - 5, $randomCount - 1); + $randomCount = $randomCount - rand(1, 5); $replies = $item['replies']['comments'] ?? []; @@ -123,9 +136,77 @@ private function generateInteraction(Comment $comment, int $count, Carbon $after $comment->interactions()->create([ 'user_id' => $userId, - 'status' => fake()->boolean(93), + 'status' => fake()->boolean(96), 'perform_at' => fake()->dateTimeBetween($afterDate) ]); } } + + private function importUser (string $youtubeChannelId) : User { + + $this->info($youtubeChannelId); + + $data = $this->youtubeService->getChannelInfo($youtubeChannelId); + + $channel = $data['items'][0]['snippet']; + + $user = User::where('username' , $channel['title'])->first(); + + if ($user) { + $user->update([ + 'description' => $channel['description'] ?: null, + 'created_at' => Carbon::create($channel['publishedAt']), + 'country' => $channel['country'] ?? null, + ]); + + return $user; + + } else { + + // Save avatar + + try { + $contentAvatar = file_get_contents($channel['thumbnails']['medium']['url']); + } + catch (\Exception $e){ + $this->info('error get avatar : '. $e->getMessage()); + } + + if ($contentAvatar ?? null) { + $avatarName = Str::random(40) . '.jpg'; + Storage::put(User::AVATAR_FOLDER . '/' . $avatarName, $contentAvatar); + } + + // Save Banner + if ($data['items'][0]['brandingSettings']['image']['bannerExternalUrl'] ?? null) { + + $url = $data['items'][0]['brandingSettings']['image']['bannerExternalUrl'] . '=w2120-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj'; + + try { + $contentBanner = file_get_contents($url); + } + catch (\Exception $e){ + $this->info('error get banner : '. $e->getMessage()); + } + + if ($contentBanner ?? null) { + $bannerName = Str::random(40) . '.jpg'; + Storage::put(User::BANNER_FOLDER . '/' .$bannerName, $contentBanner); + } + } + + return User::create([ + 'username' => $channel['title'], + 'email' => fake()->unique()->safeEmail(), + 'password' => Str::random(), + 'email_verified_at' => Carbon::create($channel['publishedAt'])->addSeconds(rand(10, 300)), + 'avatar' => $avatarName ?? null, + 'banner' => $bannerName ?? null, + 'description' => $channel['description'] ?: null, + 'country' => $channel['country'] ?? null, + 'show_subscribers' => true, + 'created_at' => Carbon::create($channel['publishedAt']), + ]); + } + } } diff --git a/app/Helpers/Parser.php b/app/Helpers/Parser.php new file mode 100644 index 0000000..87606ba --- /dev/null +++ b/app/Helpers/Parser.php @@ -0,0 +1,53 @@ + 'parseLinks', + 'timecodes' => 'parseTimeCodes', + ]; + + public static function applyParsers(string $string, array $parsers): string|null { + + $result = htmlspecialchars($string); + + foreach ($parsers as $parser) { + + if (!in_array($parser, array_keys(self::PARSERS))) { + throw new \Exception('Invalid parser :'. $parser); + } + + $method = self::PARSERS[$parser]; + + $result = self::$method($result); + } + + return $result; + } + + private static function parseLinks(string $string): string|null { + + $regex = '/(https?:\/\/)?([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/\S*)?)/m'; + + return preg_replace($regex, '$0', $string); + } + + private static function parseTimeCodes(string $string): string|null + { + $regex = '/(\d{1,2}:){1,2}\d{2}/m'; + + return preg_replace_callback($regex, function ($matches) { + + $times = array_reverse(explode(':', $matches[0])); + + $timecode = array_reduce(array_keys($times), function($carry, $index) use ($times) { + return $carry + $times[$index] * pow(60, $index); + } , 0); + + return ""; + + }, $string); + } +} diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 9b51900..f2e0540 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -15,6 +15,8 @@ class CommentController extends Controller { + const REPLIES_PER_PAGE = 10; + public function __construct() { //$this->authorizeResource(Comment::class, 'comment'); @@ -60,11 +62,13 @@ public function list(Request $request) : ResourceCollection { 'dislikes as disliked_by_auth_user' => fn($q) => $q->where('user_id', Auth::id()), ]) ->orderByRaw('likes_count - dislikes_count DESC') - ->latest(); + ->latest() + ->limit(self::REPLIES_PER_PAGE); }, 'reportByAuthUser' ]) ->withCount([ + 'replies as total_replies' => fn($q) => $q->public(), 'likes', 'dislikes', 'likes as liked_by_auth_user' => fn($q) => $q->where('user_id', Auth::id()), @@ -72,9 +76,9 @@ public function list(Request $request) : ResourceCollection { 'replies as author_replies' => fn ($query) => $query->where('user_id', $video->user->id), ]) ->when($video->pinned_comment, fn($query) => $query->orderByRaw('id <> ' .$video->pinned_comment->id)) - ->when($sort === 'top', fn($query) => $query->orderByRaw('likes_count - dislikes_count DESC')->latest()) - ->when($sort === 'newest', fn($query) => $query->latest()) - ->paginate(24) + ->when($sort === 'top', fn($query) => $query->orderByRaw('likes_count - dislikes_count DESC')) + ->latest() + ->simplePaginate(20) ->withQueryString() ))->additional([ 'count' => $video->comments_count, @@ -92,7 +96,8 @@ public function replies (Comment $comment, Request $request) : ResourceCollectio ->with([ 'user', 'video' => fn($q) => $q->with('user'), - 'replies' + 'replies', + 'reportByAuthUser' ]) ->withCount([ 'likes', @@ -101,10 +106,12 @@ public function replies (Comment $comment, Request $request) : ResourceCollectio 'dislikes as disliked_by_auth_user' => fn($q) => $q->where('user_id', Auth::id()) ]) ->when($sort === 'top', fn($query) => $query->orderByRaw('likes_count - dislikes_count DESC')) - ->when($sort === 'recent', fn($query) => $query->latest()) - ->paginate(12) + ->latest() + ->simplePaginate(self::REPLIES_PER_PAGE) ->withQueryString() - )); + ))->additional([ + 'count' => $comment->replies_count, + ]); } diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php index b1d2856..9d8f72c 100644 --- a/app/Http/Controllers/SearchController.php +++ b/app/Http/Controllers/SearchController.php @@ -33,9 +33,12 @@ public function index(Request $request, SearchFilters $filters): View $videos = in_array($type, ['videos', null]) ? Video::active() ->filter($filters) - ->where(function($query) use ($match) { - $query->where('title', 'LIKE', $match)->orWhere('description', 'LIKE', $match); - }) + ->where( + fn($query) => $query + ->where('title', 'LIKE', $match) + ->orWhere('description', 'LIKE', $match) + ->orWhereRelation('user', 'username', 'LIKE', $match) + ) ->with('user') ->latest('publication_date') ->withCount('views') diff --git a/app/Http/Resources/CommentResource.php b/app/Http/Resources/CommentResource.php index f7d7cb1..ca78387 100644 --- a/app/Http/Resources/CommentResource.php +++ b/app/Http/Resources/CommentResource.php @@ -23,6 +23,7 @@ public function toArray($request) : array 'id' => $this->id, 'class' => Comment::class, 'content' => $this->content, + 'parsed_content' => $this->parsed_content, 'short_content' => $this->short_content, 'is_long' => $this->is_long, 'user' => [ @@ -37,7 +38,6 @@ public function toArray($request) : array ], 'created_at' => $this->created_at->diffForHumans(), 'is_updated' => $this->is_updated, - 'model' => Comment::class, 'likes_count' => $this->likes_count, 'dislikes_count' => $this->dislikes_count, 'liked_by_auth_user' => $this->liked_by_auth_user > 0, @@ -46,7 +46,17 @@ public function toArray($request) : array 'can_update' => Auth::user()?->can('update', $this->resource) ?? false, 'can_report' => Auth::user()?->can('report', $this->resource) ?? false, 'can_pin' => Auth::user()?->can('pin', $this->resource) ?? false, - 'replies' => CommentResource::collection($this->replies), + 'replies' => $this->when($this->total_replies > 0, function () { + return [ + 'data' => CommentResource::collection($this->replies), + 'links' => [ + 'next' => $this->replies->count() < $this->total_replies ? route('comments.replies', ['comment' => $this->resource , 'page' => 2]) : null, + ], + 'meta' => [ + 'total' => $this->total_replies + ] + ]; + }), 'is_pinned' => $this->is_pinned, 'is_reply' => $this->is_reply, 'is_author_reply' => $this->when(!$this->is_reply, fn() => $this->author_replies > 0), diff --git a/app/Models/Comment.php b/app/Models/Comment.php index a09b616..942d533 100644 --- a/app/Models/Comment.php +++ b/app/Models/Comment.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Helpers\Parser; use App\Models\Interfaces\Likeable; use App\Models\Interfaces\Reportable; use App\Models\Traits\HasLike; @@ -19,10 +20,11 @@ use Illuminate\Support\Str; use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\Traits\LogsActivity; +use Staudenmeir\EloquentEagerLimit\HasEagerLimit; class Comment extends Model implements Likeable, Reportable { - use HasLike, LogsActivity, HasReport; + use HasLike, LogsActivity, HasReport, HasEagerLimit; protected $guarded = ['id']; @@ -71,13 +73,6 @@ protected function isLong(): Attribute ); } - protected function shortContent(): Attribute - { - return Attribute::make( - get: fn () => Str::limit($this->content, 780,' ...') - ); - } - protected function isReply(): Attribute { return Attribute::make( @@ -106,7 +101,19 @@ protected function isUpdated(): Attribute ); } + protected function shortContent(): Attribute + { + return Attribute::make( + get: fn () => Parser::applyParsers(Str::limit($this->content, 780), ['links', 'timecodes']) + ); + } + protected function parsedContent(): Attribute + { + return Attribute::make( + get: fn () => Parser::applyParsers($this->content, ['links', 'timecodes']) + ); + } /** * -------------------- SCOPES -------------------- diff --git a/app/Models/User.php b/app/Models/User.php index e04a385..0e8e1d3 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -177,13 +177,13 @@ public function premium_subscription () : HasOne { public function avatarUrl() : Attribute { return Attribute::make( - get: fn () => $this->avatar ? Storage::url(self::AVATAR_FOLDER.'/'.$this->avatar) : '/images/default_men.png' + get: fn () => $this->avatar ? Storage::url(self::AVATAR_FOLDER.'/'.$this->avatar) : asset('/images/default_men.png') ); } public function bannerUrl() : Attribute { return Attribute::make( - get: fn () => $this->banner ? Storage::url(self::BANNER_FOLDER.'/'.$this->banner) : '/images/default_banner.jpg' + get: fn () => $this->banner ? Storage::url(self::BANNER_FOLDER.'/'.$this->banner) : asset('/images/default_banner.jpg') ); } @@ -289,6 +289,16 @@ protected function uploadedVideosSize(): Attribute ); } + protected function json(): Attribute + { + return Attribute::make( + get: fn () => collect([ + 'id' => $this->id, + 'avatar' => $this->avatar_url] + )->toJson() + ); + } + /** * -------------------- SCOPES -------------------- */ diff --git a/app/Models/Video.php b/app/Models/Video.php index c217f2a..c945ec0 100644 --- a/app/Models/Video.php +++ b/app/Models/Video.php @@ -4,6 +4,7 @@ use App\Enums\VideoStatus; use App\Enums\Languages; +use App\Helpers\Parser; use App\Models\Interfaces\Reportable; use App\Models\Traits\HasLike; use App\Models\Interfaces\Likeable; @@ -201,21 +202,15 @@ protected function duration(): Attribute protected function shortDescription(): Attribute { - $url = '~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(?$0', Str::limit($this->description, 200))); - return Attribute::make( - get: fn () => $a + get: fn () => Parser::applyParsers(Str::limit($this->description, 200), ['links', 'timecodes']) ); } protected function parsedDescription(): Attribute { - $url = '~(?:(https?)://([^\s<]+)|(www\.[^\s<]+?\.[^\s<]+))(? clean(preg_replace($url, '$0', $this->description)) + get: fn () => Parser::applyParsers($this->description, ['links', 'timecodes']) ); } diff --git a/app/Policies/CommentPolicy.php b/app/Policies/CommentPolicy.php index ed4e92e..f762bda 100644 --- a/app/Policies/CommentPolicy.php +++ b/app/Policies/CommentPolicy.php @@ -107,7 +107,7 @@ public function delete(User $user, Comment $comment): Response|bool */ public function report(User $user, Comment $comment): Response|bool { - return $comment->video->is_public && $comment->user->isNot($user) && !$comment->is_banned && !$comment->isReportedByUser($user) + return $comment->video->is_public && $comment->user->isNot($user) && !$comment->is_banned && !$comment->reportByAuthUser ? Response::allow() : Response::denyWithStatus(403, 'You are not authorized to report this comment'); } diff --git a/app/Services/YoutubeService.php b/app/Services/YoutubeService.php index f960722..da66339 100644 --- a/app/Services/YoutubeService.php +++ b/app/Services/YoutubeService.php @@ -22,7 +22,18 @@ public function getComments(string $videoID) : array|null 'part' => 'snippet,replies', 'videoId' => $videoID, 'order' => 'relevance', - 'maxResults' => 20 + 'maxResults' => 100 + ]); + + return $response->json(); + } + + public function getChannelInfo(string $channelId) : array|null + { + $response = Http::get(self::API_ENDPOINT. '/channels', [ + 'key' => $this->apiKey, + 'part' => 'snippet,brandingSettings', + 'id' => $channelId, ]); return $response->json(); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index b19ce68..38a5e8a 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -18,7 +18,7 @@ class UserFactory extends Factory */ public function definition() : array { - $date = fake()->dateTimeBetween('-14 years'); + $date = fake()->dateTimeBetween('-20 years'); return [ 'username' => fake()->unique()->userName(), diff --git a/database/migrations/2023_12_19_173610_add_youtube_id_to_videos.php b/database/migrations/2023_12_19_173610_add_youtube_id_to_videos.php new file mode 100644 index 0000000..65c435c --- /dev/null +++ b/database/migrations/2023_12_19_173610_add_youtube_id_to_videos.php @@ -0,0 +1,34 @@ +string('youtube_id') + ->nullable() + ->comment('Youtube ID'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::table('videos', function (Blueprint $table) { + $table->dropColumn(['youtube_id']); + }); + } +}; diff --git a/resources/js/components/Comments.jsx b/resources/js/components/Comments.jsx index 51a08b7..dac8e68 100644 --- a/resources/js/components/Comments.jsx +++ b/resources/js/components/Comments.jsx @@ -7,9 +7,7 @@ import {Comment as Skeleton} from "./Skeletons"; import {useInView} from "react-intersection-observer"; import {jsonFetch} from '../hooks' -const Comments = memo(({target, auth, defaultSort}) => { - - const user = auth ? JSON.parse(auth) : null; +const Comments = memo(({target, defaultSort}) => { const {items: comments, setItems: setComments, load, loading, count, setCount, hasMore, setNext} = usePaginateFetch(`/api/comments?video_id=${target}&sort=${defaultSort}`) const [primaryLoading, setPrimaryLoading] = useState(true) @@ -38,7 +36,7 @@ const Comments = memo(({target, auth, defaultSort}) => { }).then(comment => { setComments(comments => [comment, ...comments]); setCount(count => count + 1); - document.getElementById('content').value = ''; + document.getElementById('message-content').value = ''; }).catch(e => e) }, []); @@ -98,7 +96,7 @@ const Comments = memo(({target, auth, defaultSort}) => { - + { (primaryLoading) ?
@@ -107,8 +105,6 @@ const Comments = memo(({target, auth, defaultSort}) => { : comments.map(comment => { +const Comment = memo(({comment, remove, update, pin}) => { const [onEdit, setOnEdit] = useState(false); const [showReply, setShowReply] = useState(false); - const [replies, setReplies] = useState(comment.replies); + const {items: replies, setItems: setReplies, load, loading, count: repliesCount, setCount, hasMore} = + usePaginateFetch(`/api/comments/${comment.id}/replies`, comment?.replies?.data ?? [], comment?.replies?.meta.total, comment?.replies?.links.next) const [showReplies, setShowReplies] = useState(false); useEffect(() => { @@ -20,7 +21,6 @@ const Comment = memo(({comment, user, canReply, remove, update, pin}) => { [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl)) }, []) - const reply = useCallback(async (data) => { return jsonFetch(`/api/comments` , { method: 'POST', @@ -33,7 +33,8 @@ const Comment = memo(({comment, user, canReply, remove, update, pin}) => { setReplies(replies => [comment, ...replies]); setShowReplies(true); setShowReply(false); - document.getElementById('content-' + comment.id).value = ''; + setCount(c => c + 1) + document.getElementById('message-content-' + comment.id).value = ''; }).catch(e => e) }, []); @@ -42,6 +43,7 @@ const Comment = memo(({comment, user, canReply, remove, update, pin}) => { method: 'DELETE', }).then(() => { setReplies(replies => replies.filter(r => r.id !== reply.id)) + setCount(c => c - 1) }).catch(e => e); }, []); @@ -56,19 +58,8 @@ const Comment = memo(({comment, user, canReply, remove, update, pin}) => { }).catch(e => e); }, []); - let attributes = { - ...(!user && { - 'data-bs-toggle': "popover", - 'data-bs-placement': "right", - 'data-bs-title': "Want to reply to this comment ?", - 'data-bs-trigger': "focus", - 'data-bs-html': "true", - 'data-bs-content': "Sign in for reply.
Sign in", - }) - } - const showRepliesText = (show, length) => { - const count = length > 1 ? replies.length + ' replies' : 'reply'; + const count = length > 1 ? length + ' replies' : 'reply'; return (show ? 'Hide ' : 'Show ') + count; } @@ -96,7 +87,6 @@ const Comment = memo(({comment, user, canReply, remove, update, pin}) => { {comment.is_updated && <> Modified}
{ - user &&
+ : + comment.reported_at && +
+ + Reported {comment.reported_at} +
+ : : - comment.reported_at && -
- - Reported {comment.reported_at} -
+ } @@ -151,13 +156,12 @@ const Comment = memo(({comment, user, canReply, remove, update, pin}) => {
{ - user ? + window.USER ? :
@@ -194,19 +198,32 @@ const Comment = memo(({comment, user, canReply, remove, update, pin}) => { }
{ - canReply && + !comment.is_reply ? + window.USER ? + : + : null }
- {showReply && } + {showReply && } { - replies.length > 0 && + repliesCount > 0 && }
@@ -227,13 +243,31 @@ const Comment = memo(({comment, user, canReply, remove, update, pin}) => { ) } + { + hasMore && +
+ +
+ }
} diff --git a/resources/js/components/Comments/Expand.jsx b/resources/js/components/Comments/Expand.jsx index 16f3c7c..58635e1 100644 --- a/resources/js/components/Comments/Expand.jsx +++ b/resources/js/components/Comments/Expand.jsx @@ -7,7 +7,7 @@ const Expand = memo(({comment}) => { return (
-
{expand ? comment.content : comment.short_content}
+
{comment.is_long && }
) diff --git a/resources/js/components/Comments/Form.jsx b/resources/js/components/Comments/Form.jsx index d43dfa9..31b46f0 100644 --- a/resources/js/components/Comments/Form.jsx +++ b/resources/js/components/Comments/Form.jsx @@ -2,7 +2,7 @@ import {useState} from "preact/hooks"; import Button from '../Button' import { memo } from 'preact/compat'; -const CommentForm = memo(({user, add, placeholder = 'Add a comment...', label = 'Comment'}) => { +const CommentForm = memo(({add, placeholder = 'Add a comment...', label = 'Comment'}) => { const [loading, setLoading] = useState(false); @@ -21,13 +21,13 @@ const CommentForm = memo(({user, add, placeholder = 'Add a comment...', label = return ( <> { - user ? + window.USER ?
- {user.username + {'user
- +
diff --git a/resources/js/components/Comments/ReplyForm.jsx b/resources/js/components/Comments/ReplyForm.jsx index 9a887c4..50c1128 100644 --- a/resources/js/components/Comments/ReplyForm.jsx +++ b/resources/js/components/Comments/ReplyForm.jsx @@ -1,7 +1,7 @@ import {useState} from "preact/hooks"; import Button from '../Button' -export default function ReplyForm ({setShowReply, comment, reply, user}) { +export default function ReplyForm ({setShowReply, comment, reply}) { const [loading, setLoading] = useState(false); @@ -21,7 +21,7 @@ export default function ReplyForm ({setShowReply, comment, reply, user}) { return (
- {user.username + {'user
diff --git a/resources/js/components/ImageLoaded.jsx b/resources/js/components/ImageLoaded.jsx index 0692105..a99216bf 100644 --- a/resources/js/components/ImageLoaded.jsx +++ b/resources/js/components/ImageLoaded.jsx @@ -1,6 +1,6 @@ import { useState} from 'preact/hooks'; -export default function ImageLoaded ({source, title, imgclass}) { +export default function ImageLoaded ({source, title, imgclass, hover}) { const [loading, setLoading] = useState(true); @@ -15,7 +15,9 @@ export default function ImageLoaded ({source, title, imgclass}) { {'default } +
{title} +
) } diff --git a/resources/js/components/Replies.jsx b/resources/js/components/Replies.jsx index 150bee3..94c7879 100644 --- a/resources/js/components/Replies.jsx +++ b/resources/js/components/Replies.jsx @@ -6,9 +6,7 @@ import {useInView} from "react-intersection-observer"; import CommentsForm from "./Comments/Form"; import {jsonFetch} from '../hooks' -export default function Replies ({target, video, auth}) { - - const user = JSON.parse(auth); +export default function Replies ({target, video}) { const {items: comments, setItems: setComments, load, loading, count, setCount, hasMore, setNext} = usePaginateFetch(`/api/comments/${target}/replies`) const [primaryLoading, setPrimaryLoading] = useState(true) @@ -39,7 +37,7 @@ export default function Replies ({target, video, auth}) { }).then(comment => { setComments(comments => [comment, ...comments]); setCount(count => count + 1); - document.getElementById('content').value = ''; + document.getElementById('message-content').value = ''; }).catch(e => e) }, []); @@ -90,7 +88,7 @@ export default function Replies ({target, video, auth}) {
- + { (primaryLoading) ?
@@ -99,8 +97,6 @@ export default function Replies ({target, video, auth}) { : comments.map(comment => ) diff --git a/resources/js/components/Video.jsx b/resources/js/components/Video.jsx index 0bcb6ce..c053366 100644 --- a/resources/js/components/Video.jsx +++ b/resources/js/components/Video.jsx @@ -2,7 +2,8 @@ import {useState} from 'preact/hooks'; export default function Video ({video}) { - const [loading, setLoading] = useState(true) + const [loading, setLoading] = useState(true); + const [hover, setHover] = useState(false); const imageLoad = () => { setLoading(false); @@ -10,21 +11,23 @@ export default function Video ({video}) { return (
-
+
{ loading && - {'default + {'default } - {video.title} +
+ {video.title} +
{video.duration}
- + setHover(true)} onMouseLeave={() => setHover(false)} style="position: absolute;inset: 0;z-index:20">
-
+
{video.user.username diff --git a/resources/js/components/index.js b/resources/js/components/index.js index 919bf9a..d5b9066 100644 --- a/resources/js/components/index.js +++ b/resources/js/components/index.js @@ -29,7 +29,7 @@ register(Playlist, 'playlist-videos', [], { shadow: false }); register(Notifications, 'site-notifications', [], { shadow: false }); register(ImageUpload, 'image-upload', ['source'], { shadow: false }); register(VideoUpload, 'video-upload', [], { shadow: false }); -register(ImageLoaded, 'image-loaded', [], { shadow: false }); +register(ImageLoaded, 'image-loaded', ['hover'], { shadow: false }); register(Save, 'save-video', [], { shadow: false }); diff --git a/resources/js/hooks.js b/resources/js/hooks.js index bd295f7..5fb14cb 100644 --- a/resources/js/hooks.js +++ b/resources/js/hooks.js @@ -1,11 +1,11 @@ import {useCallback, useState, useEffect} from 'preact/hooks'; -export function usePaginateFetch (url) { +export function usePaginateFetch (url, initialItems = [], initialCount = null, initialNext = null) { const [loading, setLoading] = useState(false); - const [items, setItems] = useState([]); - const [count, setCount] = useState([]); - const [next, setNext] = useState(null); + const [items, setItems] = useState(initialItems); + const [count, setCount] = useState(initialCount); + const [next, setNext] = useState(initialNext); const load = useCallback(async (sort = null) => { setLoading(true); return await jsonFetch((next || url) + (sort ? '&sort=' + sort : '')).then(data => { diff --git a/resources/sass/video.scss b/resources/sass/video.scss index 97a9a33..a60f4b3 100644 --- a/resources/sass/video.scss +++ b/resources/sass/video.scss @@ -1,18 +1,3 @@ -// Card - -.video-card { - @media (max-width: map-get($grid-breakpoints, "sm")) { - background-color: white; - border-bottom: var(--bs-border-width) var(--bs-border-style) var(--bs-border-color); - } - - .video-thumbnail { - @media (max-width: map-get($grid-breakpoints, "sm")) { - border-radius: 0 !important; - } - } -} - // SUGGESTED VIDEOS .suggested_video { @@ -25,4 +10,18 @@ } } +.image-box img { + transition: all 0.3s; + transform: scale(1); +} + +.image-box.hover { + overflow: hidden; + outline: 3px solid #DC3644; + img { + transform: scale(1.1); + } + +} + diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 84b68a8..ab83536 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -7,10 +7,10 @@
-
+
Login
-
+
@if (session('error'))
{!! session('error') !!} diff --git a/resources/views/components/layout.blade.php b/resources/views/components/layout.blade.php index dd9da26..40fdb34 100644 --- a/resources/views/components/layout.blade.php +++ b/resources/views/components/layout.blade.php @@ -66,5 +66,8 @@ @endif @stack('scripts') + diff --git a/resources/views/pages/history.blade.php b/resources/views/pages/history.blade.php index 86f8277..0ce31ae 100644 --- a/resources/views/pages/history.blade.php +++ b/resources/views/pages/history.blade.php @@ -2,7 +2,7 @@ @section('title', 'History') -@section('class', 'px-0 px-sm-2') +@section('class', 'px-3') @section('content') @forelse($data as $date => $views) diff --git a/resources/views/pages/home.blade.php b/resources/views/pages/home.blade.php index f3e1863..086710f 100644 --- a/resources/views/pages/home.blade.php +++ b/resources/views/pages/home.blade.php @@ -3,7 +3,7 @@ @section('title', 'Share and Watch Amazing Videos') @section('description', 'Enjoy the videos and music you love, upload original content, and share it all with friends, family, and the world on '.config('app.name').'.') -@section('class', 'px-0 px-sm-2 mt-0 mt-sm-3') +@section('class', 'px-3 mt-3') @section('content') diff --git a/resources/views/pages/liked.blade.php b/resources/views/pages/liked.blade.php index 5292eb3..dcae7db 100644 --- a/resources/views/pages/liked.blade.php +++ b/resources/views/pages/liked.blade.php @@ -2,7 +2,7 @@ @section('title', 'Liked videos') -@section('class', 'px-0 px-sm-2') +@section('class', 'px-3') @section('content') @forelse($data as $date => $interactions) diff --git a/resources/views/pages/trend.blade.php b/resources/views/pages/trend.blade.php index 60a3032..68e46ec 100644 --- a/resources/views/pages/trend.blade.php +++ b/resources/views/pages/trend.blade.php @@ -3,7 +3,7 @@ @section('title', 'Trending') @section('description', 'The pulse of what's trending on '.config('app.name').'. Check out the latest music videos, trailers, comedy clips, and everything else that people are watching right now.') -@section('class', 'px-0 px-sm-2 mt-0 mt-sm-3') +@section('class', 'px-3 mt-3') @section('content') diff --git a/resources/views/playlists/show.blade.php b/resources/views/playlists/show.blade.php index 971eb85..ba06330 100644 --- a/resources/views/playlists/show.blade.php +++ b/resources/views/playlists/show.blade.php @@ -57,27 +57,29 @@ class="btn btn-primary btn-sm"
@if($playlist->videos_count) -
    - @foreach($playlist->videos as $key => $video) -
    - @if($video->user->is(Auth::user()) || $video->is_public) - @include('videos.card-secondary', ['video' => $video, 'playlist_video' => true]) - @else -
    -
    -
    - -
    -
    This video is private
    -
    The author update video status to private
    +
    +
      + @foreach($playlist->videos as $key => $video) +
      + @if($video->user->is(Auth::user()) || $video->is_public) + @include('videos.card-secondary', ['video' => $video, 'playlist_video' => true]) + @else +
      +
      +
      + +
      +
      This video is private
      +
      The author update video status to private
      +
      -
      - @endif -
    - @endforeach -
+ @endif +
+ @endforeach + +
@else
This playlist does not contain any videos at the moment. diff --git a/resources/views/subscription/index.blade.php b/resources/views/subscription/index.blade.php index 9a5bc0d..48f32c1 100644 --- a/resources/views/subscription/index.blade.php +++ b/resources/views/subscription/index.blade.php @@ -2,7 +2,7 @@ @section('title', 'Subscriptions') -@section('class', 'px-0 px-sm-2') +@section('class', 'px-3') @section('content') @auth diff --git a/resources/views/users/comments/modals/replies.blade.php b/resources/views/users/comments/modals/replies.blade.php index 4ca2705..370ac60 100644 --- a/resources/views/users/comments/modals/replies.blade.php +++ b/resources/views/users/comments/modals/replies.blade.php @@ -20,6 +20,6 @@ const id = button.dataset.id; const video = button.dataset.video; - event.target.querySelector('.modal-body').innerHTML = ``; + event.target.querySelector('.modal-body').innerHTML = ``; }) diff --git a/resources/views/users/show.blade.php b/resources/views/users/show.blade.php index 597f00d..5ec200b 100644 --- a/resources/views/users/show.blade.php +++ b/resources/views/users/show.blade.php @@ -147,9 +147,9 @@ class="link-primary bg-transparent text-decoration-none d-flex align-items-cente See All
-
+
@foreach($user->videos as $video) - @include('videos.card.card-3', $video) + @include('videos.card', ['video' => $video]) @endforeach
@endif @@ -164,9 +164,9 @@ class="link-primary bg-transparent text-decoration-none d-flex align-items-cente

{{Str::limit($playlist->description, 200)}}

-
+
@foreach($playlist->videos as $video) - @include('videos.card.card-3', $video) + @include('videos.card', ['video' => $video]) @endforeach

diff --git a/resources/views/videos/card-secondary.blade.php b/resources/views/videos/card-secondary.blade.php index b1f8873..41e2a6b 100644 --- a/resources/views/videos/card-secondary.blade.php +++ b/resources/views/videos/card-secondary.blade.php @@ -1,14 +1,14 @@ -
!isset($playlist_video)])> +
!isset($playlist_video)]) x-data="{hover:false}">
- + {{$video->duration}}
- +
-
+
user->username}}> {{$video->user->username}} avatar diff --git a/resources/views/videos/card.blade.php b/resources/views/videos/card.blade.php index 60042a5..c62fc9f 100644 --- a/resources/views/videos/card.blade.php +++ b/resources/views/videos/card.blade.php @@ -1,15 +1,15 @@ -
-
+
+
- + {{$video->duration}}
- +
-
+
user->username}}> {{$video->user->username}} avatar diff --git a/resources/views/videos/card/card-3.blade.php b/resources/views/videos/card/card-3.blade.php deleted file mode 100644 index 3dfaa60..0000000 --- a/resources/views/videos/card/card-3.blade.php +++ /dev/null @@ -1,26 +0,0 @@ - diff --git a/resources/views/videos/show.blade.php b/resources/views/videos/show.blade.php index 4fddb10..48fb265 100644 --- a/resources/views/videos/show.blade.php +++ b/resources/views/videos/show.blade.php @@ -184,7 +184,7 @@ class="btn btn-secondary rounded-4 btn-sm px-3" @endif
-
+
@if(!Auth::check())

-
+
@each('videos.card-secondary', $videos, 'video')
@@ -300,8 +300,8 @@ class="btn btn-danger" const observer = new IntersectionObserver((entries) => { for (const entry of entries) { if(entry.isIntersecting) { - document.getElementById('comments_area').innerHTML ="setVisible(['avatar_url', 'username'])}}' default-sort='{{$video->default_comments_sort}}' />"; - observer.unobserve(entry.target) + document.getElementById('comments_area').innerHTML =""; + observer.unobserve(entry.target); } } }); @@ -312,7 +312,7 @@ class="btn btn-danger" let opened = false; commentsOffcanvas.addEventListener('show.bs.offcanvas', event => { if(!opened) { - document.getElementById('offcanvas-body').innerHTML ="setVisible(['avatar_url', 'username'])}}' default-sort='{{$video->default_comments_sort}}' />"; + document.getElementById('offcanvas-body').innerHTML =""; } opened = true; }) @@ -354,5 +354,20 @@ class="btn btn-danger" window.location.replace('{{$nextVideoUrl}}'); } }, false); + + // TimeCode + + function time (timecode) { + + video.currentTime = timecode; + video.play() + + video.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'center' + }) + } + @endpush diff --git a/routes/api.php b/routes/api.php index a6dadc4..213f9cd 100644 --- a/routes/api.php +++ b/routes/api.php @@ -53,15 +53,13 @@ }); // COMMENTS - Route::prefix('comments')->name('comments.') - ->controller(CommentController::class)->group(function () { - Route::get('/{comment}/replies', 'replies')->name('replies'); - Route::post('/', 'store')->name('store'); - Route::put('/{comment}', 'update')->name('update')->can('update', 'comment'); - Route::delete('/{comment}', 'delete')->name('delete')->can('delete', 'comment'); - Route::post('/{comment}/pin', 'pin')->name('pin')->can('pin', 'comment'); - Route::post('/{comment}/unpin', 'unpin')->name('unpin')->can('pin', 'comment'); - }); + Route::prefix('comments')->name('comments.')->controller(CommentController::class)->group(function () { + Route::post('/', 'store')->name('store'); + Route::put('/{comment}', 'update')->name('update')->can('update', 'comment'); + Route::delete('/{comment}', 'delete')->name('delete')->can('delete', 'comment'); + Route::post('/{comment}/pin', 'pin')->name('pin')->can('pin', 'comment'); + Route::post('/{comment}/unpin', 'unpin')->name('unpin')->can('pin', 'comment'); + }); // REPORT Route::post('/report', [ReportController::class, 'report'])->name('report'); @@ -102,7 +100,7 @@ Route::get("search", [SearchController::class, 'search'])->name('search'); Route::post("search-videos", [SearchController::class, 'searchVideos'])->name('search-videos'); -// VIDEOS + // VIDEOS Route::prefix('videos')->name('videos.')->controller(VideoController::class)->group(function () { Route::get('/home', 'home')->name('home'); Route::get('/trend', 'trend')->name('trend'); @@ -110,7 +108,9 @@ Route::get('/user/{user}', 'user')->name('user'); }); -// COMMENTS - Route::get("comments", [CommentController::class, 'list'])->name('comments.list'); - + // COMMENTS + Route::prefix('comments')->name('comments.')->controller(CommentController::class)->group(function () { + Route::get("/", [CommentController::class, 'list'])->name('comments.list'); + Route::get('/{comment}/replies', 'replies')->name('replies'); + }); });