+ My + Submissions +
++ আপনার আপলোডকৃত কন্টেন্ট, মডারেশন স্ট্যাটাস ও কোটা ট্র্যাকার +
++ বর্তমানে পর্যালোচনাধীন কন্টেন্ট সংখ্যা কোটায় যুক্ত হয়। + অনুমোদিত বা বাতিল হলে কোটা খালি হয়ে যায়। ভেরিফাইড + মেম্বারদের কোটা বেশি। +
+diff --git a/app/Http/Controllers/Admin/ResourceController.php b/app/Http/Controllers/Admin/ResourceController.php
index 0355185d..72a2798d 100644
--- a/app/Http/Controllers/Admin/ResourceController.php
+++ b/app/Http/Controllers/Admin/ResourceController.php
@@ -2,10 +2,7 @@
namespace App\Http\Controllers\Admin;
-use App\Http\Controllers\Controller;
-use App\Http\Requests\Resource\BulkImageStoreRequest;
-use App\Http\Requests\Resource\BulkVideoStoreRequest;
-use App\Http\Requests\Resource\StoreResourceRequest;
+use App\Http\Controllers\ResourceController as BaseResourceController;
use App\Http\Requests\Resource\UpdateResourceRequest;
use App\Models\Node;
use App\Models\Resource;
@@ -13,90 +10,33 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
-use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
-use Illuminate\Validation\ValidationException;
-class ResourceController extends Controller
+class ResourceController extends BaseResourceController
{
- /**
- * Determine maximum allowed pending submissions for the user.
- * Verified users: 100, Unverified users: 30.
- */
- protected function getMaxPendingSubmissions(): int
- {
- return Auth::user()?->is_verified ? 100 : 30;
- }
-
- /**
- * Check if user would exceed their pending change requests quota.
- * Throws standard ValidationException so it returns as a typed error in Inertia errors.
- */
- protected function ensureUnderPendingLimit(int $incomingCount = 1): void
- {
- if (Auth::user()?->can('moderate resources')) {
- return;
- }
-
- $userId = Auth::id();
- $maxLimit = $this->getMaxPendingSubmissions();
-
- $currentPending = ResourceChangeRequest::where('user_id', $userId)
- ->where('status', 'pending')
- ->count();
-
- if (($currentPending + $incomingCount) > $maxLimit) {
- throw ValidationException::withMessages([
- 'pending_limit' => "আপনি সর্বোচ্চ {$maxLimit}টি কন্টেন্ট আপলোড করার অনুরোধ করতে পারেন। আপনার আপলোডকৃত {$currentPending}টি কন্টেন্ট বর্তমানে পর্যালোচনাধীন রয়েছে, তাই অনুগ্রহ করে অপেক্ষা করুন। ",
- ]);
- }
- }
-
- public function store(StoreResourceRequest $request)
+ public function update(UpdateResourceRequest $request, Resource $resource)
{
$validated = $request->validated();
- if ($request->hasFile('file')) {
- $validated['file_path'] = $request->file('file')->store("resources/{$validated['resource_type']}s");
- }
-
- if ($request->user()->can('moderate resources')) {
- $validated['user_id'] = Auth::id();
- Resource::create($validated);
+ $isModerator = $request->user()->can('moderate resources');
- return back()->with('success', 'Resource created successfully.');
+ if (! $isModerator) {
+ $this->ensureCanSubmit(1);
}
- $this->ensureUnderPendingLimit(1);
-
- ResourceChangeRequest::recordCreate(
- Auth::id(),
- (int) $validated['node_id'],
- $validated
- );
-
- return back()->with('success', 'Resource submitted for moderation.');
- }
-
- public function update(UpdateResourceRequest $request, Resource $resource)
- {
- $validated = $request->validated();
-
if ($request->hasFile('file')) {
- if ($request->user()->can('moderate resources') && $resource->file_path) {
+ if ($isModerator && $resource->file_path) {
Storage::delete($resource->file_path);
}
$validated['file_path'] = $request->file('file')->store("resources/{$validated['resource_type']}s");
}
- if ($request->user()->can('moderate resources')) {
+ if ($isModerator) {
$resource->update($validated);
return back()->with('success', 'Resource updated successfully.');
}
- $this->ensureUnderPendingLimit(1);
-
ResourceChangeRequest::recordUpdate(
Auth::id(),
$resource,
@@ -117,157 +57,13 @@ public function destroy(Resource $resource)
return redirect()->back()->with('success', 'Resource deleted successfully.');
}
- $this->ensureUnderPendingLimit(1);
+ $this->ensureCanSubmit(1);
ResourceChangeRequest::recordDelete(Auth::id(), $resource);
return redirect()->back()->with('success', 'Resource deletion request submitted for moderation.');
}
- public function storeBulkImages(BulkImageStoreRequest $request)
- {
- $validated = $request->validated();
- $filesCount = count($request->file('files') ?? []);
- $isModerator = $request->user()->can('moderate resources');
-
- if (! $isModerator) {
- $this->ensureUnderPendingLimit($filesCount);
- }
-
- $userId = Auth::id();
- $nodeId = (int) $validated['node_id'];
-
- DB::transaction(function () use ($request, $validated, $userId, $nodeId, $isModerator) {
- foreach ($request->file('files') as $index => $file) {
- $filePath = $file->store('resources/images');
- $title = $validated['custom_titles'][$index];
-
- if ($isModerator) {
- Resource::create([
- 'user_id' => $userId,
- 'node_id' => $nodeId,
- 'title' => $title,
- 'resource_type' => 'image',
- 'file_path' => $filePath,
- ]);
- } else {
- ResourceChangeRequest::recordCreate($userId, $nodeId, [
- 'title' => $title,
- 'resource_type' => 'image',
- 'file_path' => $filePath,
- ]);
- }
- }
- });
-
- $message = $isModerator ? 'Images uploaded successfully.' : 'Images submitted for moderation.';
-
- return back()->with('success', $message);
- }
-
- public function storeBulkVideos(BulkVideoStoreRequest $request)
- {
- $validated = $request->validated();
-
- $playlistUrl = $validated['playlist_url'];
-
- // Extract playlist ID
- parse_str(parse_url($playlistUrl, PHP_URL_QUERY), $query);
-
- $playlistId = $query['list'] ?? null;
-
- if (! $playlistId) {
- return back()->with('error', 'Invalid Youtube URL');
- }
-
- $videos = [];
- $pageToken = null;
-
- do {
- $response = Http::get('https://www.googleapis.com/youtube/v3/playlistItems', [
- 'part' => 'snippet',
- 'playlistId' => $playlistId,
- 'maxResults' => 50,
- 'pageToken' => $pageToken,
- 'key' => config('services.youtube.key'),
- ]);
-
- if (! $response->successful()) {
- return back()->with('error', 'Unable to fetch youtube url');
- }
-
- $data = $response->json();
-
- foreach ($data['items'] as $item) {
- $videos[] = [
- 'title' => $item['snippet']['title'],
- 'video_id' => $item['snippet']['resourceId']['videoId'],
- 'position' => $item['snippet']['position'],
- ];
- }
-
- $pageToken = $data['nextPageToken'] ?? null;
- } while ($pageToken);
-
- if (! empty($validated['is_reversed'])) {
- $videos = array_reverse($videos);
- }
-
- // Apply naming strategy
- foreach ($videos as $index => &$video) {
- if ($validated['naming_strategy'] === 'youtube') {
- $video['title'] = $video['title'];
- } else {
- $number = str_pad(
- ($validated['start_number'] ?? 1) + $index,
- 2,
- '0',
- STR_PAD_LEFT
- );
-
- $prefix = trim($validated['naming_prefix'] ?? '');
- $video['title'] = $prefix !== '' ? "{$prefix} - {$number}" : $number;
- }
- }
-
- $userId = Auth::id();
- $nodeId = (int) $validated['node_id'];
- $videosCount = count($videos);
- $isModerator = $request->user()->can('moderate resources');
-
- if (! $isModerator) {
- $this->ensureUnderPendingLimit($videosCount);
- }
-
- DB::transaction(function () use ($videos, $userId, $nodeId, $isModerator) {
- foreach ($videos as $video) {
- $finalUrl = "https://www.youtube.com/watch?v={$video['video_id']}";
-
- if ($isModerator) {
- Resource::create([
- 'user_id' => $userId,
- 'node_id' => $nodeId,
- 'title' => $video['title'],
- 'resource_type' => 'video',
- 'external_url' => $finalUrl,
- ]);
- } else {
- ResourceChangeRequest::recordCreate($userId, $nodeId, [
- 'title' => $video['title'],
- 'resource_type' => 'video',
- 'external_url' => $finalUrl,
- ]);
- }
- }
- });
-
- $message = $isModerator
- ? 'YouTube playlist imported successfully.'
- : 'YouTube playlist imported and submitted for moderation.';
-
- return back()->with('success', $message);
- }
-
public function bulkRename(Request $request, Node $node)
{
if ($node->isEffectivelyFrozen()) {
diff --git a/app/Http/Controllers/NodeController.php b/app/Http/Controllers/NodeController.php
index 9ac54df7..e7199ea9 100644
--- a/app/Http/Controllers/NodeController.php
+++ b/app/Http/Controllers/NodeController.php
@@ -65,6 +65,7 @@ public function show(Subject $subject, $path)
'id' => $node->id,
'name' => $node->name,
'slug' => $node->slug,
+ 'is_frozen' => $node->isEffectivelyFrozen(),
],
'nodes' => $nodes,
'breadcrumb' => $node->breadcrumb(),
diff --git a/app/Http/Controllers/ResourceController.php b/app/Http/Controllers/ResourceController.php
index e4649c31..1d1e4c65 100644
--- a/app/Http/Controllers/ResourceController.php
+++ b/app/Http/Controllers/ResourceController.php
@@ -2,10 +2,18 @@
namespace App\Http\Controllers;
+use App\Http\Requests\Resource\BulkImageStoreRequest;
+use App\Http\Requests\Resource\BulkVideoStoreRequest;
+use App\Http\Requests\Resource\StoreResourceRequest;
use App\Models\Node;
use App\Models\Resource;
+use App\Models\ResourceChangeRequest;
use App\Models\ResourceCompletion;
+use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
class ResourceController extends Controller
@@ -71,4 +79,227 @@ public function toggleComplete(Resource $resource)
return back();
}
+
+ /**
+ * Determine maximum allowed pending submissions for the user.
+ * Verified users: 100, Unverified users: 30.
+ */
+ public function getMaxPendingSubmissions(): int
+ {
+ return Auth::user()?->is_verified ? 100 : 30;
+ }
+
+ /**
+ * Check if user is eligible to submit (account age) and within pending change requests quota.
+ */
+ public function ensureCanSubmit(int $incomingCount = 1): void
+ {
+ $user = Auth::user();
+ if ($user?->can('moderate resources')) {
+ return;
+ }
+
+ if ($user?->created_at && $user->created_at->diffInHours(now()) < 24) {
+ throw ValidationException::withMessages([
+ 'account_age' => 'আপনার অ্যাকাউন্টটি নতুন। স্প্যাম রোধে অ্যাকাউন্ট তৈরির ২৪ ঘণ্টা পর রিসোর্স আপলোড করা যাবে।',
+ ]);
+ }
+
+ $userId = Auth::id();
+ $maxLimit = $this->getMaxPendingSubmissions();
+
+ $currentPending = ResourceChangeRequest::where('user_id', $userId)
+ ->where('status', 'pending')
+ ->count();
+
+ if (($currentPending + $incomingCount) > $maxLimit) {
+ throw ValidationException::withMessages([
+ 'pending_limit' => "আপনি সর্বোচ্চ {$maxLimit}টি কন্টেন্ট আপলোড করার অনুরোধ করতে পারেন। আপনার আপলোডকৃত {$currentPending}টি কন্টেন্ট বর্তমানে পর্যালোচনাধীন রয়েছে, তাই অনুগ্রহ করে অপেক্ষা করুন। ",
+ ]);
+ }
+ }
+
+ public function store(StoreResourceRequest $request)
+ {
+ $validated = $request->validated();
+ $isModerator = $request->user()->can('moderate resources');
+
+ if (! $isModerator) {
+ $this->ensureCanSubmit(1);
+ }
+
+ if ($request->hasFile('file')) {
+ $validated['file_path'] = $request->file('file')->store("resources/{$validated['resource_type']}s");
+ }
+
+ if ($isModerator) {
+ $validated['user_id'] = Auth::id();
+ Resource::create($validated);
+
+ return back()->with('success', 'Resource created successfully.');
+ }
+
+ ResourceChangeRequest::recordCreate(
+ Auth::id(),
+ (int) $validated['node_id'],
+ $validated
+ );
+
+ return redirect()->route('submissions.index')->with('success', 'Resource submitted for moderation.');
+ }
+
+ public function storeBulkImages(BulkImageStoreRequest $request)
+ {
+ $validated = $request->validated();
+ $filesCount = count($request->file('files') ?? []);
+ $isModerator = $request->user()->can('moderate resources');
+
+ if (! $isModerator) {
+ $this->ensureCanSubmit($filesCount);
+ }
+
+ $userId = Auth::id();
+ $nodeId = (int) $validated['node_id'];
+
+ DB::transaction(function () use ($request, $validated, $userId, $nodeId, $isModerator) {
+ foreach ($request->file('files') as $index => $file) {
+ $filePath = $file->store('resources/images');
+ $title = $validated['custom_titles'][$index];
+
+ if ($isModerator) {
+ Resource::create([
+ 'user_id' => $userId,
+ 'node_id' => $nodeId,
+ 'title' => $title,
+ 'resource_type' => 'image',
+ 'file_path' => $filePath,
+ ]);
+ } else {
+ ResourceChangeRequest::recordCreate($userId, $nodeId, [
+ 'title' => $title,
+ 'resource_type' => 'image',
+ 'file_path' => $filePath,
+ ]);
+ }
+ }
+ });
+
+ if ($isModerator) {
+ return back()->with('success', 'Images uploaded successfully.');
+ }
+
+ return redirect()->route('submissions.index')->with('success', 'Images submitted for moderation.');
+ }
+
+ public function storeBulkVideos(BulkVideoStoreRequest $request)
+ {
+ $validated = $request->validated();
+ $isModerator = $request->user()->can('moderate resources');
+
+ if (! $isModerator) {
+ $this->ensureCanSubmit(1);
+ }
+
+ $playlistUrl = $validated['playlist_url'];
+
+ // Extract playlist ID
+ parse_str(parse_url($playlistUrl, PHP_URL_QUERY), $query);
+
+ $playlistId = $query['list'] ?? null;
+
+ if (! $playlistId) {
+ return back()->with('error', 'Invalid Youtube URL');
+ }
+
+ $videos = [];
+ $pageToken = null;
+
+ do {
+ $response = Http::get('https://www.googleapis.com/youtube/v3/playlistItems', [
+ 'part' => 'snippet',
+ 'playlistId' => $playlistId,
+ 'maxResults' => 50,
+ 'pageToken' => $pageToken,
+ 'key' => config('services.youtube.key'),
+ ]);
+
+ if (! $response->successful()) {
+ return back()->with('error', 'Unable to fetch youtube url');
+ }
+
+ $data = $response->json();
+
+ foreach ($data['items'] as $item) {
+ $videos[] = [
+ 'title' => $item['snippet']['title'],
+ 'video_id' => $item['snippet']['resourceId']['videoId'],
+ 'position' => $item['snippet']['position'],
+ ];
+ }
+
+ if (! $isModerator && count($videos) >= $this->getMaxPendingSubmissions()) {
+ break;
+ }
+
+ $pageToken = $data['nextPageToken'] ?? null;
+ } while ($pageToken);
+
+ if (! empty($validated['is_reversed'])) {
+ $videos = array_reverse($videos);
+ }
+
+ // Apply naming strategy
+ foreach ($videos as $index => &$video) {
+ if ($validated['naming_strategy'] === 'youtube') {
+ $video['title'] = $video['title'];
+ } else {
+ $number = str_pad(
+ ($validated['start_number'] ?? 1) + $index,
+ 2,
+ '0',
+ STR_PAD_LEFT
+ );
+
+ $prefix = trim($validated['naming_prefix'] ?? '');
+ $video['title'] = $prefix !== '' ? "{$prefix} - {$number}" : $number;
+ }
+ }
+
+ $userId = Auth::id();
+ $nodeId = (int) $validated['node_id'];
+ $videosCount = count($videos);
+ $isModerator = $request->user()->can('moderate resources');
+
+ if (! $isModerator) {
+ $this->ensureCanSubmit($videosCount);
+ }
+
+ DB::transaction(function () use ($videos, $userId, $nodeId, $isModerator) {
+ foreach ($videos as $video) {
+ $finalUrl = "https://www.youtube.com/watch?v={$video['video_id']}";
+
+ if ($isModerator) {
+ Resource::create([
+ 'user_id' => $userId,
+ 'node_id' => $nodeId,
+ 'title' => $video['title'],
+ 'resource_type' => 'video',
+ 'external_url' => $finalUrl,
+ ]);
+ } else {
+ ResourceChangeRequest::recordCreate($userId, $nodeId, [
+ 'title' => $video['title'],
+ 'resource_type' => 'video',
+ 'external_url' => $finalUrl,
+ ]);
+ }
+ }
+ });
+
+ if ($isModerator) {
+ return back()->with('success', 'YouTube playlist imported successfully.');
+ }
+
+ return redirect()->route('submissions.index')->with('success', 'YouTube playlist imported and submitted for moderation.');
+ }
}
diff --git a/app/Http/Controllers/SubmissionController.php b/app/Http/Controllers/SubmissionController.php
new file mode 100644
index 00000000..59517998
--- /dev/null
+++ b/app/Http/Controllers/SubmissionController.php
@@ -0,0 +1,58 @@
+is_verified ? 100 : 30;
+ $counts = ResourceChangeRequest::where('user_id', $user->id)
+ ->selectRaw('status, count(*) as total')
+ ->groupBy('status')
+ ->pluck('total', 'status');
+
+ $pendingCount = (int) $counts->get('pending', 0);
+ $approvedCount = (int) $counts->get('approved', 0);
+ $rejectedCount = (int) $counts->get('rejected', 0);
+ $totalCount = (int) $counts->sum();
+
+ $status = $request->query('status');
+ $query = ResourceChangeRequest::where('user_id', $user->id)
+ ->with(['resource', 'node.subject'])
+ ->latest();
+
+ if ($status && in_array($status, ['pending', 'approved', 'rejected'])) {
+ $query->where('status', $status);
+ }
+
+ $submissions = $query->paginate(15)->withQueryString();
+
+ $submissions->getCollection()->each(function ($submission) {
+ $submission->node?->append('breadcrumb');
+ });
+
+ return Inertia::render('Submissions/Index', [
+ 'submissions' => $submissions,
+ 'quota' => [
+ 'used' => $pendingCount,
+ 'max' => $maxLimit,
+ 'is_verified' => (bool) $user->is_verified,
+ ],
+ 'counts' => [
+ 'all' => $totalCount,
+ 'pending' => $pendingCount,
+ 'approved' => $approvedCount,
+ 'rejected' => $rejectedCount,
+ ],
+ 'currentStatus' => $status ?? 'all',
+ ]);
+ }
+}
diff --git a/app/Notifications/ResourceModerationNotification.php b/app/Notifications/ResourceModerationNotification.php
index 4266c5d6..a3e72bfa 100644
--- a/app/Notifications/ResourceModerationNotification.php
+++ b/app/Notifications/ResourceModerationNotification.php
@@ -35,14 +35,9 @@ public function via(object $notifiable): array
*/
public function toArray(object $notifiable): array
{
- $node = $this->changeRequest->node;
- if ($node && $node->subject) {
- $path = implode('/', array_column($node->breadcrumb(), 'slug'));
- $hash = $this->status === 'rejected' ? '#rejected' : '#live';
- $url = "/admin/subjects/{$node->subject->slug}/nodes/{$path}{$hash}";
- } else {
- $url = '/admin';
- }
+ $url = $this->status === 'rejected'
+ ? '/my-submissions?status=rejected'
+ : '/my-submissions?status=approved';
if ($this->totalCount > 1) {
$title = $this->status === 'rejected'
diff --git a/database/migrations/2026_10_13_160000_remove_create_resources_permission.php b/database/migrations/2026_10_13_160000_remove_create_resources_permission.php
new file mode 100644
index 00000000..e69193f1
--- /dev/null
+++ b/database/migrations/2026_10_13_160000_remove_create_resources_permission.php
@@ -0,0 +1,23 @@
+delete();
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Permission::findOrCreate('create resources', 'web');
+ }
+};
diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php
index 9017d5a4..85e9ad9e 100644
--- a/database/seeders/RolePermissionSeeder.php
+++ b/database/seeders/RolePermissionSeeder.php
@@ -46,7 +46,6 @@ public function run(): void
/*
* Resource management
*/
- Permission::findOrCreate('create resources');
Permission::findOrCreate('edit resources');
Permission::findOrCreate('delete resources');
Permission::findOrCreate('moderate resources');
diff --git a/resources/js/components/EmptyState.vue b/resources/js/components/EmptyState.vue
index fc37ccc8..c8027f96 100644
--- a/resources/js/components/EmptyState.vue
+++ b/resources/js/components/EmptyState.vue
@@ -1,19 +1,13 @@
-
-
-
- {{
- changeRequest?.status === 'rejected'
- ? 'This submission was reviewed and rejected'
- : 'Awaiting moderation review before public release'
- }}
-
- {{
- changeRequest.rejection_reason ||
- 'No specific reason provided.'
- }}
-
- {{ changeRequest.payload.content }}
-
+ {{ request.payload.content }}
+
+ The author has requested complete and permanent
+ removal of this resource and all attached files.
+
- {{
- changeRequest?.status === 'rejected'
- ? 'Rejected Resource Submission'
- : 'Resource Submission'
- }}
-
-
- {{
- changeRequest.payload?.title ||
- changeRequest.targetResource?.title ||
- '(Untitled)'
- }}
-
-
-
+ {{ request.payload?.title || '(untitled)' }}
+
+
+
+
+
+
+
+ Target: "{{
+ request.resource?.title ||
+ request.targetResource?.title ||
+ 'Resource'
+ }}"
+
+
+ এই ফোল্ডারটি বর্তমানে লক করা রয়েছে। তাই এখানে কোনো কিছু আপলোড + করা যাবে না। +
+ ++ আপনার আপলোডকৃত কন্টেন্ট, মডারেশন স্ট্যাটাস ও কোটা ট্র্যাকার +
++ বর্তমানে পর্যালোচনাধীন কন্টেন্ট সংখ্যা কোটায় যুক্ত হয়। + অনুমোদিত বা বাতিল হলে কোটা খালি হয়ে যায়। ভেরিফাইড + মেম্বারদের কোটা বেশি। +
+