Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions GEMINI.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Project Guidelines & Automated Checks

## Formatting and Linting
When asked to fix or check formatting/linting issues, or right before the final commit prior to pushing, run the automated fix commands directly instead of manually inspecting and fixing errors one by one:
When asked to fix or check formatting/linting issues, or before committing changes, run the automated fix commands directly instead of manually inspecting and fixing errors one by one:

```bash
npm run format && composer lint && npm run lint
Expand All @@ -16,7 +16,7 @@ npm run format && composer lint && npm run lint
```
2. Switch to that branch.
3. Use atomic commits where applicable.
4. Run formatting and linting checks only before the last commit:
4. Always run formatting and linting checks before committing:
```bash
npm run format && composer lint && npm run lint
```
Expand All @@ -26,6 +26,3 @@ npm run format && composer lint && npm run lint
git push -u origin <new-branch>
```
7. Create a Pull Request (PR) with a clear, respective title and description linking relevant issues.



62 changes: 62 additions & 0 deletions app/Http/Controllers/Admin/PeerSettingsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use App\Models\AppSetting;
use Illuminate\Http\Request;
use Inertia\Inertia;

class PeerSettingsController extends Controller
{
public const DEFAULT_PRESETS = [
[
'id' => '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.',
],
];

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);
$cooldownMinutes = (int) AppSetting::get('peer_poke_cooldown_minutes', 360);
$presets = self::getPresets();

return Inertia::render('admin/PeerSettings', [
'settings' => [
'enabled' => $enabled,
'cooldown_minutes' => $cooldownMinutes,
'presets' => $presets,
],
]);
}

public function update(Request $request)
{
$validated = $request->validate([
'enabled' => 'required|boolean',
'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',
'presets.*.message' => 'required|string|max:200',
]);

AppSetting::set('peer_poke_enabled', $validated['enabled'], 'boolean');
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.');
}
}
70 changes: 68 additions & 2 deletions app/Http/Controllers/UserProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -90,7 +95,16 @@ public function show(string $username)
->take(30)
->get();


$pokeEnabled = (bool) AppSetting::get('peer_poke_enabled', true);
$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,
'canPoke' => $canPoke,
'isCooldown' => $isPokeOnCooldown,
'presets' => $pokePresets,
];

// Early return if activity is locked for this visitor
if ($isLocked) {
Expand All @@ -105,6 +119,7 @@ public function show(string $username)
'suggestedUsers' => $suggestedUsers,
'appreciators' => $appreciators,
'appreciating' => $appreciating,
'pokeData' => $pokeData,
]);
}

Expand Down Expand Up @@ -133,7 +148,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')
Expand Down Expand Up @@ -277,7 +291,59 @@ public function show(string $username)
'appreciations' => $recentAppreciations->values(),
],
'suggestedUsers' => $suggestedUsers,
'pokeData' => $pokeData,
]);
}

public function poke(Request $request, User $user)
{
$currentAuthUser = auth()->user();

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', 'Pokes are currently disabled.');
}

if (! ($user->allow_pokes ?? true)) {
return back()->with('error', 'This user has disabled pokes.');
}

$validated = $request->validate([
'preset_id' => 'required|string',
]);

$presets = PeerSettingsController::getPresets();
$selectedPreset = collect($presets)->firstWhere('id', $validated['preset_id']);

if (! $selectedPreset) {
return back()->with('error', 'The selected poke message is invalid.');
}

$cooldownKey = "study_poke:{$currentAuthUser->id}:{$user->id}";
$cooldownMinutes = (int) AppSetting::get('peer_poke_cooldown_minutes', 360);
$cooldownSeconds = max(30, $cooldownMinutes * 60);

if (! Cache::add($cooldownKey, true, $cooldownSeconds)) {
return back()->with('error', 'You are on cooldown for poking this peer.');
}

try {
$user->notify(new StudyPokeNotification(
sender: $currentAuthUser,
message: $selectedPreset['message'],
icon: $selectedPreset['icon'] ?? '⚡',
presetId: $selectedPreset['id'],
));
} catch (\Throwable $e) {
Cache::forget($cooldownKey);
throw $e;
}

return back()->with('success', "You poked {$user->name}! ⚡");
}

public function toggleAppreciate(User $user)
Expand Down
1 change: 1 addition & 0 deletions app/Http/Requests/Profile/UpdateProfileRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
2 changes: 2 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class User extends Authenticatable
'title',
'institution',
'activity_privacy',
'allow_pokes',
'facebook',
'instagram',
'github',
Expand Down Expand Up @@ -105,6 +106,7 @@ protected function casts(): array
'password' => 'hashed',
'receive_emails' => 'boolean',
'is_verified' => 'boolean',
'allow_pokes' => 'boolean',
];
}

Expand Down
52 changes: 52 additions & 0 deletions app/Notifications/StudyPokeNotification.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

namespace App\Notifications;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Notification;

class StudyPokeNotification extends Notification implements ShouldQueue
{
use Queueable;

public function __construct(
public User $sender,
public string $message,
public string $icon = '⚡',
public ?string $presetId = null,
) {}

/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['database'];
}

/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
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,
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Spatie\Permission\PermissionRegistrar;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
app()[PermissionRegistrar::class]->forgetCachedPermissions();

$permission = Permission::findOrCreate('manage peers', 'web');

$admin = Role::where('name', 'admin')->where('guard_name', 'web')->first();
if ($admin) {
$admin->givePermissionTo($permission);
}

app()[PermissionRegistrar::class]->forgetCachedPermissions();
}

/**
* Reverse the migrations.
*/
public function down(): void
{
app()[PermissionRegistrar::class]->forgetCachedPermissions();

$permission = Permission::where('name', 'manage peers')->where('guard_name', 'web')->first();
$permission?->delete();

app()[PermissionRegistrar::class]->forgetCachedPermissions();
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->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');
});
}
};
5 changes: 5 additions & 0 deletions database/seeders/RolePermissionSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions resources/js/components/NotificationDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -576,6 +577,12 @@ onBeforeUnmount(() => {
"
class="h-4 w-4 text-indigo-500"
/>
<Zap
v-else-if="
item.data?.type === 'study_poke'
"
class="h-4 w-4 fill-amber-500/20 text-amber-500"
/>
<Clock
v-else-if="
item.data?.type === 'forum_pending'
Expand Down
6 changes: 6 additions & 0 deletions resources/js/layouts/AdminLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,12 @@ const allNavigation: AdminNavItem[] = [
icon: 'group',
permission: 'view users',
},
{
name: 'Peer & Pokes',
to: '/admin/peers/settings',
icon: 'bolt',
permission: 'manage peers',
},
{
name: 'Send Emails',
to: '/admin/emails/send',
Expand Down
Loading