From 61992f7db20f5bc34db89ebb1de5b98995491dae Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 11 Sep 2026 22:59:27 +0600 Subject: [PATCH 01/10] feat(user): add allow_pokes preference setting to users table and model --- .../Requests/Profile/UpdateProfileRequest.php | 1 + app/Models/User.php | 2 ++ ..._223000_add_allow_pokes_to_users_table.php | 28 +++++++++++++++++++ database/seeders/RolePermissionSeeder.php | 5 ++++ 4 files changed, 36 insertions(+) create mode 100644 database/migrations/2026_09_11_223000_add_allow_pokes_to_users_table.php diff --git a/app/Http/Requests/Profile/UpdateProfileRequest.php b/app/Http/Requests/Profile/UpdateProfileRequest.php index 734b5539..922ea1d1 100644 --- a/app/Http/Requests/Profile/UpdateProfileRequest.php +++ b/app/Http/Requests/Profile/UpdateProfileRequest.php @@ -40,6 +40,7 @@ public function rules(): array 'about' => ['sometimes', 'nullable', 'string', 'max:1000', new CleanText], 'institution' => ['sometimes', 'nullable', 'string', 'max:255', new CleanText], 'activity_privacy' => ['sometimes', 'string', Rule::in(['public', 'appreciators_only', 'private'])], + 'allow_pokes' => ['sometimes', 'boolean'], 'facebook' => ['sometimes', 'nullable', 'string', 'max:255'], 'instagram' => ['sometimes', 'nullable', 'string', 'max:255'], 'github' => ['sometimes', 'nullable', 'string', 'max:255'], diff --git a/app/Models/User.php b/app/Models/User.php index 29e025c0..1b0a7fe1 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -51,6 +51,7 @@ class User extends Authenticatable 'title', 'institution', 'activity_privacy', + 'allow_pokes', 'facebook', 'instagram', 'github', @@ -105,6 +106,7 @@ protected function casts(): array 'password' => 'hashed', 'receive_emails' => 'boolean', 'is_verified' => 'boolean', + 'allow_pokes' => 'boolean', ]; } diff --git a/database/migrations/2026_09_11_223000_add_allow_pokes_to_users_table.php b/database/migrations/2026_09_11_223000_add_allow_pokes_to_users_table.php new file mode 100644 index 00000000..0ff148b3 --- /dev/null +++ b/database/migrations/2026_09_11_223000_add_allow_pokes_to_users_table.php @@ -0,0 +1,28 @@ +boolean('allow_pokes')->default(true)->after('activity_privacy'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('allow_pokes'); + }); + } +}; diff --git a/database/seeders/RolePermissionSeeder.php b/database/seeders/RolePermissionSeeder.php index 4962fd9c..0329cf44 100644 --- a/database/seeders/RolePermissionSeeder.php +++ b/database/seeders/RolePermissionSeeder.php @@ -95,6 +95,11 @@ public function run(): void */ Permission::findOrCreate('manage forums'); + /* + * Peer & Poke management + */ + Permission::findOrCreate('manage peers'); + $admin->syncPermissions(Permission::all()); // Administrators have unrestricted access to all features. From 682e0d1dbff208d30051e447feca9288be15ad3a Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 11 Sep 2026 22:59:31 +0600 Subject: [PATCH 02/10] feat(peer): implement study poke endpoint, rate-limiting cooldown, and notifications --- .../Admin/PeerSettingsController.php | 82 +++++++++++++++++++ .../Controllers/UserProfileController.php | 68 ++++++++++++++- app/Notifications/StudyPokeNotification.php | 52 ++++++++++++ routes/admin.php | 7 ++ routes/web.php | 1 + 5 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/Admin/PeerSettingsController.php create mode 100644 app/Notifications/StudyPokeNotification.php diff --git a/app/Http/Controllers/Admin/PeerSettingsController.php b/app/Http/Controllers/Admin/PeerSettingsController.php new file mode 100644 index 00000000..c484f04b --- /dev/null +++ b/app/Http/Controllers/Admin/PeerSettingsController.php @@ -0,0 +1,82 @@ + 'syllabus', + 'icon' => '🔥', + 'message' => "That syllabus isn't going to finish itself! Open the books!", + ], + [ + 'id' => 'phone_down', + 'icon' => '📱', + 'message' => 'Close the tabs, put the phone on DND, and start studying.', + ], + [ + 'id' => 'lock_in', + 'icon' => '🔒', + 'message' => "Time to lock in. Let's crush today's study goals!", + ], + [ + 'id' => 'coffee', + 'icon' => '☕', + 'message' => 'Grab a hot cup of tea/coffee and head over to your study desk.', + ], + [ + 'id' => 'exam_panic', + 'icon' => '💀', + 'message' => 'Future you in the exam hall will thank you for studying right now.', + ], + [ + 'id' => 'study_buddy', + 'icon' => '🤝', + 'message' => 'I am studying right now, join the grind with me!', + ], + ]; + + public static function getPresets(): array + { + return AppSetting::get('peer_poke_presets', self::DEFAULT_PRESETS) ?? self::DEFAULT_PRESETS; + } + + public function edit() + { + $enabled = (bool) AppSetting::get('peer_poke_enabled', true); + $cooldownHours = (int) AppSetting::get('peer_poke_cooldown_hours', 6); + $presets = self::getPresets(); + + return Inertia::render('admin/PeerSettings', [ + 'settings' => [ + 'enabled' => $enabled, + 'cooldown_hours' => $cooldownHours, + 'presets' => $presets, + ], + ]); + } + + public function update(Request $request) + { + $validated = $request->validate([ + 'enabled' => 'required|boolean', + 'cooldown_hours' => 'required|integer|min:1|max:72', + 'presets' => 'required|array|min:1|max:20', + 'presets.*.id' => 'required|string|max:50', + 'presets.*.icon' => 'required|string|max:10', + 'presets.*.message' => 'required|string|max:200', + ]); + + AppSetting::set('peer_poke_enabled', $validated['enabled'], 'boolean'); + AppSetting::set('peer_poke_cooldown_hours', $validated['cooldown_hours'], 'integer'); + AppSetting::set('peer_poke_presets', $validated['presets'], 'json'); + + return redirect()->back()->with('success', 'Peer & Study Poke settings updated successfully.'); + } +} diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 69c7ce59..31d629ed 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Http\Controllers\Admin\PeerSettingsController; +use App\Models\AppSetting; use App\Models\BlogComment; use App\Models\BlogReaction; use App\Models\ForumAnswer; @@ -10,7 +12,10 @@ use App\Models\Resource; use App\Models\User; use App\Models\UserAppreciation; +use App\Notifications\StudyPokeNotification; use App\Notifications\UserAppreciationNotification; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; use Inertia\Inertia; class UserProfileController extends Controller @@ -90,7 +95,17 @@ public function show(string $username) ->take(30) ->get(); - + // Study Poke evaluation + $pokeEnabled = (bool) AppSetting::get('peer_poke_enabled', true); + $canPoke = $pokeEnabled && auth()->check() && ! $isOwner && ($user->allow_pokes ?? true); + $isPokeOnCooldown = $canPoke && Cache::has('study_poke:'.auth()->id().":{$user->id}"); + $pokePresets = $pokeEnabled ? PeerSettingsController::getPresets() : []; + $pokeData = [ + 'enabled' => $pokeEnabled, + 'canPoke' => $canPoke, + 'isCooldown' => $isPokeOnCooldown, + 'presets' => $pokePresets, + ]; // Early return if activity is locked for this visitor if ($isLocked) { @@ -105,6 +120,7 @@ public function show(string $username) 'suggestedUsers' => $suggestedUsers, 'appreciators' => $appreciators, 'appreciating' => $appreciating, + 'pokeData' => $pokeData, ]); } @@ -133,7 +149,6 @@ public function show(string $username) $totalBlogViews = (int) $user->blogs()->where('is_published', true)->sum('views'); $sharedResourcesCount = Resource::where('user_id', $user->id)->count(); - // Recent Community Activities $recentForumPosts = ForumPost::where('user_id', $user->id) ->latest('id') @@ -277,9 +292,58 @@ public function show(string $username) 'appreciations' => $recentAppreciations->values(), ], 'suggestedUsers' => $suggestedUsers, + 'pokeData' => $pokeData, ]); } + public function poke(Request $request, User $user) + { + $currentAuthUser = auth()->user(); + + // Cannot poke own profile + if ($currentAuthUser->id === $user->id) { + return back()->with('error', 'You cannot poke yourself.'); + } + + $enabled = (bool) AppSetting::get('peer_poke_enabled', true); + if (! $enabled) { + return back()->with('error', 'Study pokes are currently disabled.'); + } + + // Check receiver permission + if (! ($user->allow_pokes ?? true)) { + return back()->with('error', 'This user has disabled study pokes.'); + } + + // Cooldown check + $cooldownKey = "study_poke:{$currentAuthUser->id}:{$user->id}"; + if (Cache::has($cooldownKey)) { + return back()->with('error', 'You are on cooldown for poking this peer.'); + } + + $validated = $request->validate([ + 'preset_id' => 'required|string', + ]); + + $presets = PeerSettingsController::getPresets(); + $selectedPreset = collect($presets)->firstWhere('id', $validated['preset_id']) ?? $presets[0]; + + // Send notification + $user->notify(new StudyPokeNotification( + sender: $currentAuthUser, + message: $selectedPreset['message'], + icon: $selectedPreset['icon'] ?? '⚡', + presetId: $selectedPreset['id'], + )); + + // Set cooldown + $cooldownHours = (int) AppSetting::get('peer_poke_cooldown_hours', 6); + $cooldownSeconds = max(60, $cooldownHours * 3600); + Cache::put($cooldownKey, true, $cooldownSeconds); + + return back()->with('success', "You poked {$user->name} to study! ⚡"); + } + public function toggleAppreciate(User $user) { $currentAuthUser = auth()->user(); diff --git a/app/Notifications/StudyPokeNotification.php b/app/Notifications/StudyPokeNotification.php new file mode 100644 index 00000000..e0600ace --- /dev/null +++ b/app/Notifications/StudyPokeNotification.php @@ -0,0 +1,52 @@ + + */ + public function via(object $notifiable): array + { + return ['database']; + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + 'type' => 'study_poke', + 'title' => "{$this->sender->name} poked you! {$this->icon}", + 'message' => $this->message, + 'url' => route('user.profile', ['username' => $this->sender->username]), + 'sender_id' => $this->sender->id, + 'sender_name' => $this->sender->name, + 'sender_username' => $this->sender->username, + 'sender_image' => $this->sender->image_url, + 'poke_icon' => $this->icon, + 'poke_message' => $this->message, + 'preset_id' => $this->presetId, + ]; + } +} diff --git a/routes/admin.php b/routes/admin.php index a3cd2fdc..f6b6cc8c 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Admin\ForumController as AdminForumController; use App\Http\Controllers\Admin\NodeController as AdminNodeController; use App\Http\Controllers\Admin\NoticeController as AdminNoticeController; +use App\Http\Controllers\Admin\PeerSettingsController; use App\Http\Controllers\Admin\ResourceController as AdminResourceController; use App\Http\Controllers\Admin\SubjectController as AdminSubjectController; use App\Http\Controllers\Admin\SupportTicketController as AdminSupportTicketController; @@ -147,3 +148,9 @@ Route::get('/forums/settings', [AdminForumController::class, 'settings'])->name('forums.settings.edit'); Route::post('/forums/settings', [AdminForumController::class, 'updateSettings'])->name('forums.settings.update'); }); + +// Peer & Study Poke Settings +Route::middleware('permission:manage peers')->group(function () { + Route::get('/peers/settings', [PeerSettingsController::class, 'edit'])->name('peers.settings.edit'); + Route::post('/peers/settings', [PeerSettingsController::class, 'update'])->name('peers.settings.update'); +}); diff --git a/routes/web.php b/routes/web.php index 3e6728b1..007873ba 100644 --- a/routes/web.php +++ b/routes/web.php @@ -36,6 +36,7 @@ Route::post('/resources/{resource}/complete', [ResourceController::class, 'toggleComplete'])->name('resources.complete'); Route::post('/nodes/{node}/vote', [NodeController::class, 'vote'])->name('nodes.vote'); Route::post('/u/{user}/appreciate', [UserProfileController::class, 'toggleAppreciate'])->name('user.appreciate'); + Route::post('/u/{user}/poke', [UserProfileController::class, 'poke'])->name('user.poke'); Route::get('/support/my-tickets', [SupportTicketController::class, 'myTickets'])->name('support.my-tickets'); Route::post('/support/tickets', [SupportTicketController::class, 'store'])->name('support.tickets.store'); From cb5bc02cde6fc836137cc54f6f7b83691774a1dd Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 11 Sep 2026 22:59:36 +0600 Subject: [PATCH 03/10] feat(ui): add study poke modal, profile poke toggle, and admin peer settings --- .../js/components/NotificationDropdown.vue | 7 + resources/js/layouts/AdminLayout.vue | 6 + resources/js/pages/Profile.vue | 46 +++ resources/js/pages/User/Show.vue | 243 +++++++++-- resources/js/pages/admin/PeerSettings.vue | 378 ++++++++++++++++++ 5 files changed, 653 insertions(+), 27 deletions(-) create mode 100644 resources/js/pages/admin/PeerSettings.vue diff --git a/resources/js/components/NotificationDropdown.vue b/resources/js/components/NotificationDropdown.vue index aa434bdc..b62e67fb 100644 --- a/resources/js/components/NotificationDropdown.vue +++ b/resources/js/components/NotificationDropdown.vue @@ -15,6 +15,7 @@ import { ArrowBigUp, Clock, LifeBuoy, + Zap, } from 'lucide-vue-next'; import { onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { getCsrfToken } from '@/lib/useCsrf'; @@ -576,6 +577,12 @@ onBeforeUnmount(() => { " class="h-4 w-4 text-indigo-500" /> + +
+
+
+ + + Receive Pokes + +
+

+ সহপাঠীদের থেকে Study Poke বা তাড়া পেতে এই অপশনটি + অন রাখুন। +

+
+ + +
+ diff --git a/resources/js/pages/User/Show.vue b/resources/js/pages/User/Show.vue index c8375e76..fe2cb531 100644 --- a/resources/js/pages/User/Show.vue +++ b/resources/js/pages/User/Show.vue @@ -25,6 +25,8 @@ import { UploadCloud, Users, X, + Zap, + Send, } from 'lucide-vue-next'; import { computed, ref, watch } from 'vue'; import EmptyState from '@/components/EmptyState.vue'; @@ -59,6 +61,17 @@ const props = defineProps<{ isLocked?: boolean; lockReason?: 'private' | 'appreciators_only' | null; activityPrivacy?: string; + pokeData?: { + enabled: boolean; + canPoke: boolean; + isCooldown: boolean; + cooldownUntil: number | null; + presets: Array<{ + id: string; + icon: string; + message: string; + }>; + }; appreciators?: Array<{ id: number; name: string; @@ -247,6 +260,58 @@ watch( }, ); +const showPokeModal = ref(false); +const selectedPresetId = ref(''); +const isSubmittingPoke = ref(false); +const localPokeCooldown = ref(props.pokeData?.isCooldown ?? false); + +watch( + () => props.pokeData?.isCooldown, + (val) => { + localPokeCooldown.value = val ?? false; + }, +); + +const handleOpenPokeModal = () => { + if (!currentUser.value) { + showGuestModal.value = true; + + return; + } + + if (props.pokeData?.presets && props.pokeData.presets.length > 0) { + if (!selectedPresetId.value) { + selectedPresetId.value = props.pokeData.presets[0].id; + } + } + + showPokeModal.value = true; +}; + +const handleSendPoke = () => { + if (!selectedPresetId.value) { + return; + } + + isSubmittingPoke.value = true; + router.post( + `/u/${props.profileUser.id}/poke`, + { + preset_id: selectedPresetId.value, + }, + { + preserveScroll: true, + onSuccess: () => { + showPokeModal.value = false; + localPokeCooldown.value = true; + }, + onFinish: () => { + isSubmittingPoke.value = false; + }, + }, + ); +}; + const handleAppreciate = () => { if (!currentUser.value) { showGuestModal.value = true; @@ -429,36 +494,63 @@ const timeAgo = formatTimeAgo; - + + + :title=" + localIsAppreciated + ? 'Appreciating (click to remove)' + : 'Appreciate this member' + " + > + + {{ + localIsAppreciated + ? 'Appreciating' + : 'Appreciate' + }} + + @@ -1628,4 +1720,101 @@ const timeAgo = formatTimeAgo; + + + +
+
+ +
+ + +
+
+ +
+
+

+ Send a Study Poke 👉 +

+

+ Give @{{ profileUser.username }} a friendly study + reminder +

+
+
+ + +
+ +
+ + +
+ + + +
+
+
+
diff --git a/resources/js/pages/admin/PeerSettings.vue b/resources/js/pages/admin/PeerSettings.vue new file mode 100644 index 00000000..48a12317 --- /dev/null +++ b/resources/js/pages/admin/PeerSettings.vue @@ -0,0 +1,378 @@ + + + From 80266fb0c9bd1161c5d5d4a842fbcb17c70aa620 Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 11 Sep 2026 22:59:52 +0600 Subject: [PATCH 04/10] test(peer): add feature tests for study poke and peer settings --- tests/Feature/StudyPokeTest.php | 155 ++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 tests/Feature/StudyPokeTest.php diff --git a/tests/Feature/StudyPokeTest.php b/tests/Feature/StudyPokeTest.php new file mode 100644 index 00000000..cc141134 --- /dev/null +++ b/tests/Feature/StudyPokeTest.php @@ -0,0 +1,155 @@ +seed(RolePermissionSeeder::class); +}); + +test('guests are redirected when attempting to poke a user', function () { + $targetUser = User::factory()->create(['username' => 'bob']); + + $this->post("/u/{$targetUser->id}/poke", ['preset_id' => 'syllabus']) + ->assertRedirect('/login'); +}); + +test('users cannot poke their own profile', function () { + $user = User::factory()->create(['username' => 'alice']); + + $this->actingAs($user) + ->post("/u/{$user->id}/poke", ['preset_id' => 'syllabus']) + ->assertSessionHas('error'); +}); + +test('authenticated user can poke another user and dispatch notification', function () { + Notification::fake(); + Cache::flush(); + + $userA = User::factory()->create(['username' => 'alice']); + $userB = User::factory()->create(['username' => 'bob']); + + $response = $this->actingAs($userA) + ->post("/u/{$userB->id}/poke", ['preset_id' => 'syllabus']); + + $response->assertSessionHas('success'); + + Notification::assertSentTo( + $userB, + StudyPokeNotification::class, + function (StudyPokeNotification $notification) use ($userA) { + return $notification->sender->id === $userA->id + && $notification->presetId === 'syllabus'; + } + ); + + // Verify cooldown is in cache + $cacheKey = "study_poke:{$userA->id}:{$userB->id}"; + expect(Cache::has($cacheKey))->toBeTrue(); +}); + +test('subsequent pokes during cooldown are blocked', function () { + Notification::fake(); + Cache::flush(); + + $userA = User::factory()->create(['username' => 'alice']); + $userB = User::factory()->create(['username' => 'bob']); + + // First poke succeeds + $this->actingAs($userA) + ->post("/u/{$userB->id}/poke", ['preset_id' => 'syllabus']) + ->assertSessionHas('success'); + + // Second poke fails due to cooldown + $this->actingAs($userA) + ->post("/u/{$userB->id}/poke", ['preset_id' => 'coffee']) + ->assertSessionHas('error'); + + Notification::assertSentTimes(StudyPokeNotification::class, 1); +}); + +test('cannot poke user who has turned off allow_pokes', function () { + Notification::fake(); + Cache::flush(); + + $userA = User::factory()->create(['username' => 'alice']); + $userB = User::factory()->create([ + 'username' => 'bob', + 'allow_pokes' => false, + ]); + + $this->actingAs($userA) + ->post("/u/{$userB->id}/poke", ['preset_id' => 'syllabus']) + ->assertSessionHas('error'); + + Notification::assertNothingSent(); +}); + +test('user profile passes pokeData with active presets and cooldown status', function () { + Cache::flush(); + + $userA = User::factory()->create(['username' => 'alice']); + $userB = User::factory()->create(['username' => 'bob']); + + $response = $this->actingAs($userA)->get("/u/{$userB->username}"); + + $response->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('User/Show') + ->has('pokeData') + ->where('pokeData.enabled', true) + ->where('pokeData.canPoke', true) + ->where('pokeData.isCooldown', false) + ->has('pokeData.presets') + ); +}); + +test('admin can update peer and poke settings with manage peers permission', function () { + $permission = Permission::findOrCreate('manage peers'); + $adminRole = Role::findOrCreate('admin'); + $adminRole->givePermissionTo($permission); + + $adminUser = User::factory()->create([ + 'email_verified_at' => now(), + 'is_verified' => true, + ]); + $adminUser->assignRole('admin'); + $adminUser->givePermissionTo('view admin'); + + $response = $this->actingAs($adminUser)->get('/admin/peers/settings'); + $response->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('admin/PeerSettings') + ->has('settings.presets') + ); + + $newPresets = [ + [ + 'id' => 'custom_1', + 'icon' => '🚀', + 'message' => 'Blast off into your study session now!', + ], + ]; + + $updateResponse = $this->actingAs($adminUser) + ->post('/admin/peers/settings', [ + 'enabled' => true, + 'cooldown_hours' => 12, + 'presets' => $newPresets, + ]); + + $updateResponse->assertRedirect(); + + expect(AppSetting::get('peer_poke_cooldown_hours'))->toBe(12); + expect(AppSetting::get('peer_poke_presets'))->toBe($newPresets); +}); From f5c9935e2bfcfe17725702fc101a9dfef0ce5c5b Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 11 Sep 2026 23:03:30 +0600 Subject: [PATCH 05/10] refactor(peer): switch cooldown to minutes and clean up default presets --- .../Admin/PeerSettingsController.php | 28 +++------------ .../Controllers/UserProfileController.php | 4 +-- resources/js/pages/admin/PeerSettings.vue | 36 ++++--------------- tests/Feature/StudyPokeTest.php | 6 ++-- 4 files changed, 16 insertions(+), 58 deletions(-) diff --git a/app/Http/Controllers/Admin/PeerSettingsController.php b/app/Http/Controllers/Admin/PeerSettingsController.php index c484f04b..612e9823 100644 --- a/app/Http/Controllers/Admin/PeerSettingsController.php +++ b/app/Http/Controllers/Admin/PeerSettingsController.php @@ -20,26 +20,6 @@ class PeerSettingsController extends Controller 'icon' => '📱', 'message' => 'Close the tabs, put the phone on DND, and start studying.', ], - [ - 'id' => 'lock_in', - 'icon' => '🔒', - 'message' => "Time to lock in. Let's crush today's study goals!", - ], - [ - 'id' => 'coffee', - 'icon' => '☕', - 'message' => 'Grab a hot cup of tea/coffee and head over to your study desk.', - ], - [ - 'id' => 'exam_panic', - 'icon' => '💀', - 'message' => 'Future you in the exam hall will thank you for studying right now.', - ], - [ - 'id' => 'study_buddy', - 'icon' => '🤝', - 'message' => 'I am studying right now, join the grind with me!', - ], ]; public static function getPresets(): array @@ -50,13 +30,13 @@ public static function getPresets(): array public function edit() { $enabled = (bool) AppSetting::get('peer_poke_enabled', true); - $cooldownHours = (int) AppSetting::get('peer_poke_cooldown_hours', 6); + $cooldownMinutes = (int) AppSetting::get('peer_poke_cooldown_minutes', 360); $presets = self::getPresets(); return Inertia::render('admin/PeerSettings', [ 'settings' => [ 'enabled' => $enabled, - 'cooldown_hours' => $cooldownHours, + 'cooldown_minutes' => $cooldownMinutes, 'presets' => $presets, ], ]); @@ -66,7 +46,7 @@ public function update(Request $request) { $validated = $request->validate([ 'enabled' => 'required|boolean', - 'cooldown_hours' => 'required|integer|min:1|max:72', + 'cooldown_minutes' => 'required|integer|min:1|max:10080', 'presets' => 'required|array|min:1|max:20', 'presets.*.id' => 'required|string|max:50', 'presets.*.icon' => 'required|string|max:10', @@ -74,7 +54,7 @@ public function update(Request $request) ]); AppSetting::set('peer_poke_enabled', $validated['enabled'], 'boolean'); - AppSetting::set('peer_poke_cooldown_hours', $validated['cooldown_hours'], 'integer'); + AppSetting::set('peer_poke_cooldown_minutes', $validated['cooldown_minutes'], 'integer'); AppSetting::set('peer_poke_presets', $validated['presets'], 'json'); return redirect()->back()->with('success', 'Peer & Study Poke settings updated successfully.'); diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 31d629ed..6cfc3b77 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -337,8 +337,8 @@ public function poke(Request $request, User $user) )); // Set cooldown - $cooldownHours = (int) AppSetting::get('peer_poke_cooldown_hours', 6); - $cooldownSeconds = max(60, $cooldownHours * 3600); + $cooldownMinutes = (int) AppSetting::get('peer_poke_cooldown_minutes', 360); + $cooldownSeconds = max(30, $cooldownMinutes * 60); Cache::put($cooldownKey, true, $cooldownSeconds); return back()->with('success', "You poked {$user->name} to study! ⚡"); diff --git a/resources/js/pages/admin/PeerSettings.vue b/resources/js/pages/admin/PeerSettings.vue index 48a12317..0e95697c 100644 --- a/resources/js/pages/admin/PeerSettings.vue +++ b/resources/js/pages/admin/PeerSettings.vue @@ -22,7 +22,7 @@ interface PokePreset { interface PeerSettingsProps { settings: { enabled: boolean; - cooldown_hours: number; + cooldown_minutes: number; presets: PokePreset[]; }; } @@ -40,33 +40,11 @@ const defaultPresets: PokePreset[] = [ icon: '📱', message: 'Close the tabs, put the phone on DND, and start studying.', }, - { - id: 'lock_in', - icon: '🔒', - message: "Time to lock in. Let's crush today's study goals!", - }, - { - id: 'coffee', - icon: '☕', - message: - 'Grab a hot cup of tea/coffee and head over to your study desk.', - }, - { - id: 'exam_panic', - icon: '💀', - message: - 'Future you in the exam hall will thank you for studying right now.', - }, - { - id: 'study_buddy', - icon: '🤝', - message: 'I am studying right now, join the grind with me!', - }, ]; const form = useForm({ enabled: props.settings.enabled, - cooldown_hours: props.settings.cooldown_hours, + cooldown_minutes: props.settings.cooldown_minutes, presets: JSON.parse( JSON.stringify(props.settings.presets || defaultPresets), ) as PokePreset[], @@ -231,7 +209,7 @@ const submit = () => { - +
@@ -243,7 +221,7 @@ const submit = () => { - Cooldown Duration (Hours) + Cooldown Duration (Minutes)

{

hoursminutes
diff --git a/tests/Feature/StudyPokeTest.php b/tests/Feature/StudyPokeTest.php index cc141134..62d74884 100644 --- a/tests/Feature/StudyPokeTest.php +++ b/tests/Feature/StudyPokeTest.php @@ -72,7 +72,7 @@ function (StudyPokeNotification $notification) use ($userA) { // Second poke fails due to cooldown $this->actingAs($userA) - ->post("/u/{$userB->id}/poke", ['preset_id' => 'coffee']) + ->post("/u/{$userB->id}/poke", ['preset_id' => 'phone_down']) ->assertSessionHas('error'); Notification::assertSentTimes(StudyPokeNotification::class, 1); @@ -144,12 +144,12 @@ function (StudyPokeNotification $notification) use ($userA) { $updateResponse = $this->actingAs($adminUser) ->post('/admin/peers/settings', [ 'enabled' => true, - 'cooldown_hours' => 12, + 'cooldown_minutes' => 720, 'presets' => $newPresets, ]); $updateResponse->assertRedirect(); - expect(AppSetting::get('peer_poke_cooldown_hours'))->toBe(12); + expect(AppSetting::get('peer_poke_cooldown_minutes'))->toBe(720); expect(AppSetting::get('peer_poke_presets'))->toBe($newPresets); }); From afe12258599fd366e6b0b9812166bd2929f42a3c Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 11 Sep 2026 23:17:37 +0600 Subject: [PATCH 06/10] refactor(peer): streamline poke branding, update modal to BaseModal, and enable guest visibility --- .../Controllers/UserProfileController.php | 17 +- resources/js/pages/Profile.vue | 3 +- resources/js/pages/User/Show.vue | 178 ++++++++---------- resources/js/pages/admin/PeerSettings.vue | 25 ++- tests/Feature/StudyPokeTest.php | 17 ++ 5 files changed, 117 insertions(+), 123 deletions(-) diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 6cfc3b77..f913cb26 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -95,10 +95,10 @@ public function show(string $username) ->take(30) ->get(); - // Study Poke evaluation + // Poke evaluation $pokeEnabled = (bool) AppSetting::get('peer_poke_enabled', true); - $canPoke = $pokeEnabled && auth()->check() && ! $isOwner && ($user->allow_pokes ?? true); - $isPokeOnCooldown = $canPoke && Cache::has('study_poke:'.auth()->id().":{$user->id}"); + $canPoke = $pokeEnabled && ! $isOwner && ($user->allow_pokes ?? true); + $isPokeOnCooldown = $canPoke && auth()->check() && Cache::has('study_poke:'.auth()->id().":{$user->id}"); $pokePresets = $pokeEnabled ? PeerSettingsController::getPresets() : []; $pokeData = [ 'enabled' => $pokeEnabled, @@ -300,22 +300,19 @@ public function poke(Request $request, User $user) { $currentAuthUser = auth()->user(); - // Cannot poke own profile if ($currentAuthUser->id === $user->id) { return back()->with('error', 'You cannot poke yourself.'); } $enabled = (bool) AppSetting::get('peer_poke_enabled', true); if (! $enabled) { - return back()->with('error', 'Study pokes are currently disabled.'); + return back()->with('error', 'Pokes are currently disabled.'); } - // Check receiver permission if (! ($user->allow_pokes ?? true)) { - return back()->with('error', 'This user has disabled study pokes.'); + return back()->with('error', 'This user has disabled pokes.'); } - // Cooldown check $cooldownKey = "study_poke:{$currentAuthUser->id}:{$user->id}"; if (Cache::has($cooldownKey)) { return back()->with('error', 'You are on cooldown for poking this peer.'); @@ -328,7 +325,6 @@ public function poke(Request $request, User $user) $presets = PeerSettingsController::getPresets(); $selectedPreset = collect($presets)->firstWhere('id', $validated['preset_id']) ?? $presets[0]; - // Send notification $user->notify(new StudyPokeNotification( sender: $currentAuthUser, message: $selectedPreset['message'], @@ -336,12 +332,11 @@ public function poke(Request $request, User $user) presetId: $selectedPreset['id'], )); - // Set cooldown $cooldownMinutes = (int) AppSetting::get('peer_poke_cooldown_minutes', 360); $cooldownSeconds = max(30, $cooldownMinutes * 60); Cache::put($cooldownKey, true, $cooldownSeconds); - return back()->with('success', "You poked {$user->name} to study! ⚡"); + return back()->with('success', "You poked {$user->name}! ⚡"); } public function toggleAppreciate(User $user) diff --git a/resources/js/pages/Profile.vue b/resources/js/pages/Profile.vue index fc18a1ee..32fc4e94 100644 --- a/resources/js/pages/Profile.vue +++ b/resources/js/pages/Profile.vue @@ -781,8 +781,7 @@ const submitForm = () => {

- সহপাঠীদের থেকে Study Poke বা তাড়া পেতে এই অপশনটি - অন রাখুন। + সহপাঠীদের থেকে Poke পেতে এই অপশনটি অন রাখুন।

diff --git a/resources/js/pages/User/Show.vue b/resources/js/pages/User/Show.vue index fe2cb531..5d7b483c 100644 --- a/resources/js/pages/User/Show.vue +++ b/resources/js/pages/User/Show.vue @@ -26,9 +26,9 @@ import { Users, X, Zap, - Send, } from 'lucide-vue-next'; import { computed, ref, watch } from 'vue'; +import BaseModal from '@/components/BaseModal.vue'; import EmptyState from '@/components/EmptyState.vue'; import UserListItem from '@/components/UserListItem.vue'; import VerifiedBadge from '@/components/VerifiedBadge.vue'; @@ -245,6 +245,7 @@ const localAppreciationsCount = ref(props.appreciationsCount); const showAppreciatorsModal = ref(false); const showAppreciatingModal = ref(false); const showGuestModal = ref(false); +const guestModalAction = ref<'appreciate' | 'poke'>('appreciate'); watch( () => props.isAppreciated, @@ -274,6 +275,7 @@ watch( const handleOpenPokeModal = () => { if (!currentUser.value) { + guestModalAction.value = 'poke'; showGuestModal.value = true; return; @@ -314,6 +316,7 @@ const handleSendPoke = () => { const handleAppreciate = () => { if (!currentUser.value) { + guestModalAction.value = 'appreciate'; showGuestModal.value = true; return; @@ -509,15 +512,14 @@ const timeAgo = formatTimeAgo; :title=" localPokeCooldown ? 'Poked recently (Cooldown active)' - : 'Send a Study Poke to ' + - profileUser.name + : 'Send a Poke to ' + profileUser.name " > {{ - localPokeCooldown ? 'Poked' : 'Study Poke' + localPokeCooldown ? 'Poked' : 'Poke' }} @@ -1686,19 +1688,35 @@ const timeAgo = formatTimeAgo;
- + +

- Sign in to Appreciate + {{ + guestModalAction === 'poke' + ? 'Sign in to Poke' + : 'Sign in to Appreciate' + }}

- You need to be logged in to send appreciation and support - fellow students and contributors. + {{ + guestModalAction === 'poke' + ? 'You need to be logged in to send pokes to fellow peers.' + : 'You need to be logged in to send appreciation and support fellow students and contributors.' + }}

@@ -1721,100 +1739,66 @@ const timeAgo = formatTimeAgo;
- - -
-
- + + + + +
+ +
+ + +
diff --git a/resources/js/pages/admin/PeerSettings.vue b/resources/js/pages/admin/PeerSettings.vue index 0e95697c..455b30f4 100644 --- a/resources/js/pages/admin/PeerSettings.vue +++ b/resources/js/pages/admin/PeerSettings.vue @@ -62,7 +62,7 @@ const addPreset = () => { const removePreset = (index: number) => { if (form.presets.length <= 1) { - alert('You must have at least one study poke preset.'); + alert('You must have at least one poke preset.'); return; } @@ -90,7 +90,7 @@ const submit = () => {