channels page
This commit is contained in:
parent
ed8ab47bdd
commit
92793d7a76
@ -14,6 +14,18 @@ class ChannelController extends Controller
|
|||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @return \Illuminate\Http\JsonResponse
|
* @return \Illuminate\Http\JsonResponse
|
||||||
*/
|
*/
|
||||||
|
public function toggleFetch(Request $request, $channel_id)
|
||||||
|
{
|
||||||
|
// Start a query on the Video model
|
||||||
|
$channel = Channel::where('id', $channel_id)->first();
|
||||||
|
|
||||||
|
$channel->update([
|
||||||
|
'fetching_enabled' => !$channel->fetching_enabled
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Return the videos as a JSON response
|
||||||
|
return response()->json(true);
|
||||||
|
}
|
||||||
public function getChannels(Request $request)
|
public function getChannels(Request $request)
|
||||||
{
|
{
|
||||||
// Start a query on the Video model
|
// Start a query on the Video model
|
||||||
@ -29,4 +41,24 @@ class ChannelController extends Controller
|
|||||||
// Return the videos as a JSON response
|
// Return the videos as a JSON response
|
||||||
return response()->json($channels);
|
return response()->json($channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getChannelsFull(Request $request)
|
||||||
|
{
|
||||||
|
// Start a query on the Video model
|
||||||
|
$query = Channel::query()->with(['videos', 'videos.clips']);
|
||||||
|
|
||||||
|
if ($request->has('languages')) {
|
||||||
|
$query->whereIn('language', $request->input('languages'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('channelFetchEnabled') && $request->input('channelFetchEnabled') == "true") {
|
||||||
|
$query->where('fetching_enabled', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the videos (you can add pagination if desired)
|
||||||
|
$channels = $query->get();
|
||||||
|
|
||||||
|
// Return the videos as a JSON response
|
||||||
|
return response()->json($channels);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,6 +84,12 @@ export default function AppLayout({
|
|||||||
>
|
>
|
||||||
Clips
|
Clips
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
href={route('channels')}
|
||||||
|
active={route().current('channels')}
|
||||||
|
>
|
||||||
|
Channels
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
231
src/resources/js/Pages/Channels.tsx
Normal file
231
src/resources/js/Pages/Channels.tsx
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import axios from "axios";
|
||||||
|
import AppLayout from '@/Layouts/AppLayout';
|
||||||
|
import { Video, Channel } from "@/types";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
import {faDownload, faFileVideo, faFont, faSquareCheck} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { router } from '@inertiajs/core';
|
||||||
|
import { Link, Head } from '@inertiajs/react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import useRoute from '@/Hooks/useRoute';
|
||||||
|
import {MultiSelect} from "@/Components/MultiSelect";
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
export default function Channels() {
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const [channels, setChannels] = useState<Channel[]>([]);
|
||||||
|
const [distinctLanguages, setDistinctLanguages] = useState<string[]>([]);
|
||||||
|
const [loadingVideos, setLoadingVideos] = useState<boolean>(true);
|
||||||
|
const [loadingFetch, setLoadingFetch] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Filter states
|
||||||
|
const [selectedLanguages, setSelectedLanguages] = useState<string[]>([]);
|
||||||
|
const [channelFetchEnabled, setChannelFetchEnabled] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
|
const toggleFetchingEnabled = async (channelId: number) => {
|
||||||
|
setLoadingFetch(true);
|
||||||
|
try {
|
||||||
|
// fire your API; adjust the URL/method to match your backend
|
||||||
|
const { data: updatedChannel } = await axios.post(
|
||||||
|
`/api/channels/${channelId}/toggleFetch`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2) update local state
|
||||||
|
setChannels(prev =>
|
||||||
|
prev.map(ch =>
|
||||||
|
ch.id === channelId
|
||||||
|
? { ...ch, fetching_enabled: !ch.fetching_enabled }
|
||||||
|
: ch
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setLoadingFetch(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to toggle fetching:", error);
|
||||||
|
setLoadingFetch(false);
|
||||||
|
// you might want to show a toast here
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoadingVideos(true);
|
||||||
|
const params: Record<string, string | string[] | boolean> = {};
|
||||||
|
|
||||||
|
if (selectedLanguages.length > 0) {
|
||||||
|
params.languages = selectedLanguages.filter(lang => lang !== '');
|
||||||
|
}
|
||||||
|
params.channelFetchEnabled = channelFetchEnabled;
|
||||||
|
|
||||||
|
axios.get('/api/channels/getFull', { params })
|
||||||
|
.then(response => {
|
||||||
|
console.log(response.data);
|
||||||
|
setChannels(response.data);
|
||||||
|
setLoadingVideos(false);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching clips:', error);
|
||||||
|
setLoadingVideos(false);
|
||||||
|
});
|
||||||
|
}, [selectedLanguages, channelFetchEnabled]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout
|
||||||
|
title="Videos"
|
||||||
|
renderHeader={() => (
|
||||||
|
<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
|
||||||
|
Videos
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="py-12">
|
||||||
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
||||||
|
<div className="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg p-6">
|
||||||
|
{/* Filter Options */}
|
||||||
|
<div className="mb-6 grid grid-cols-1 sm:grid-cols-3 md:grid-cols-5 gap-4">
|
||||||
|
{/* Language Filter */}
|
||||||
|
<div className="form-control flex flex-col">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text pl-2">Language</span>
|
||||||
|
</label>
|
||||||
|
{loadingVideos ? (
|
||||||
|
<span className="loading loading-dots loading-md"></span>
|
||||||
|
) : (
|
||||||
|
<MultiSelect
|
||||||
|
label="Languages"
|
||||||
|
options={distinctLanguages.map(ln => ({
|
||||||
|
value: ln,
|
||||||
|
label: ln,
|
||||||
|
}))}
|
||||||
|
selected={selectedLanguages}
|
||||||
|
onChange={setSelectedLanguages}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* Clip exists filter */}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text pl-2">Fetching enabled?</span>
|
||||||
|
</label>
|
||||||
|
{loadingVideos ? (
|
||||||
|
<span className="loading loading-dots loading-md"></span>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="checkbox checkbox-primary mt-3 ml-3"
|
||||||
|
checked={channelFetchEnabled}
|
||||||
|
onChange={e => setChannelFetchEnabled(e.target.checked)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table">
|
||||||
|
{/* head */}
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" className="checkbox"/>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<th>Channel</th>
|
||||||
|
<th>Title</th>
|
||||||
|
<th>Fetching enabled</th>
|
||||||
|
<th>...</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{loadingVideos ? (
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
) :
|
||||||
|
channels.map((channel) => (
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" className="checkbox"/>
|
||||||
|
</label>
|
||||||
|
</th>
|
||||||
|
<td className="pr-0">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="avatar">
|
||||||
|
<div className="mask mask-squircle h-12 w-12">
|
||||||
|
<img
|
||||||
|
src={channel.img_url}
|
||||||
|
alt={channel.channel_name}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-bold">{channel.channel_name}</div>
|
||||||
|
<div className="text-sm opacity-50">{channel.twitch_id ? "Twitch" : channel.youtube_id ? "YouTube" : ""}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="flex flex-col w-max max-w-[500px]">
|
||||||
|
<div className="mb-2">
|
||||||
|
|
||||||
|
Average clips per VOD:{' '}
|
||||||
|
{channel.videos.length > 0
|
||||||
|
? (
|
||||||
|
channel.videos.reduce(
|
||||||
|
(total, video) => total + (video.clips?.length || 0),
|
||||||
|
0
|
||||||
|
) / channel.videos.length
|
||||||
|
).toFixed(2)
|
||||||
|
: 'NaN'}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="badge badge-ghost badge-sm">
|
||||||
|
Clip count: {
|
||||||
|
channel.videos.reduce(
|
||||||
|
(total, video) => total + (video.clips?.length || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span className="badge badge-ghost badge-sm">
|
||||||
|
VOD count: {channel.videos.length || 0}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
{loadingFetch ? (
|
||||||
|
<span className="loading loading-dots loading-md"></span>
|
||||||
|
) : (
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faSquareCheck}
|
||||||
|
className={classNames(
|
||||||
|
channel.fetching_enabled ? "text-green-500" : "text-gray-500",
|
||||||
|
"cursor-pointer" // show it's clickable
|
||||||
|
)}
|
||||||
|
onClick={() => toggleFetchingEnabled(channel.id)}
|
||||||
|
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<th>
|
||||||
|
<button className="btn btn-ghost btn-xs opacity-20 pointer-events-none cursor-not-allowed">Actions</button>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
@ -16,4 +16,6 @@ Route::get('/user', function (Request $request) {
|
|||||||
Route::get('videos/get', [VideoController::class, 'getVideos']);
|
Route::get('videos/get', [VideoController::class, 'getVideos']);
|
||||||
Route::get('clips/get', [ClipController::class, 'getClips']);
|
Route::get('clips/get', [ClipController::class, 'getClips']);
|
||||||
Route::get('channels/get', [ChannelController::class, 'getChannels']);
|
Route::get('channels/get', [ChannelController::class, 'getChannels']);
|
||||||
|
Route::get('channels/getFull', [ChannelController::class, 'getChannelsFull']);
|
||||||
|
Route::post('channels/{id}/toggleFetch', [ChannelController::class, 'toggleFetch']);
|
||||||
Route::get('dashboard/stats', [StatsController::class, 'getStats']);
|
Route::get('dashboard/stats', [StatsController::class, 'getStats']);
|
||||||
|
@ -28,6 +28,10 @@ Route::middleware([
|
|||||||
return Inertia::render('Videos');
|
return Inertia::render('Videos');
|
||||||
})->name('videos');
|
})->name('videos');
|
||||||
|
|
||||||
|
Route::get('/channels', function () {
|
||||||
|
return Inertia::render('Channels');
|
||||||
|
})->name('channels');
|
||||||
|
|
||||||
Route::get('/clips', function (Request $request) {
|
Route::get('/clips', function (Request $request) {
|
||||||
return Inertia::render('Clips', [
|
return Inertia::render('Clips', [
|
||||||
'video_id' => $request->get('video_id', null),
|
'video_id' => $request->get('video_id', null),
|
||||||
|
Loading…
Reference in New Issue
Block a user