Add sports-match type, device tracking, profile visits, and share refactor

- New SportsMatch model/controller and sports UI components/modal
- Move share-modal to a reusable x-share-modal/x-share-button component
- Add VideoSharedWithUser notification and share-to-members flow
- Device/user-agent tracking on views, downloads, share accesses
- ProfileVisit model + migration; subscription source tracking
- Email thumbnail support; remove stale TODO files
This commit is contained in:
ghassan committed 2026-05-29 01:50:28 +03:00
1 parent 6aae6f86b6
commit 73527f3781
61 files changed
+4014 -636

No files matched your search

+75 -2
View File
@@ -21,6 +21,33 @@ class UserController extends Controller
$this->middleware('auth')->except(['channel']);
}
// Typeahead search for members (used by the "Share to member" picker)
public function searchUsers(Request $request)
{
$q = trim((string) $request->query('q', ''));
if (mb_strlen($q) < 1) {
return response()->json(['users' => []]);
}
$users = User::where('id', '!=', Auth::id())
->where(function ($w) use ($q) {
$w->where('name', 'like', "%{$q}%")
->orWhere('username', 'like', "%{$q}%");
})
->orderBy('name')
->limit(8)
->get(['id', 'name', 'username', 'avatar']);
return response()->json([
'users' => $users->map(fn ($u) => [
'id' => $u->id,
'name' => $u->name,
'channel' => $u->channel,
'avatar' => $u->avatar_url,
]),
]);
}
// Profile page - personal overview for the authenticated user
public function profile()
{
@@ -378,7 +405,49 @@ class UserController extends Controller
]);
}
public function toggleSubscribe(User $user)
public function recordProfileVisit(Request $request, User $user)
{
// Don't record self-visits or repeated visits from the same person in the last 30 minutes
$visitorId = Auth::id();
$deviceId = $request->cookie('_did');
if ($visitorId && $visitorId === $user->id) {
return response()->json(['ok' => true, 'skipped' => 'self']);
}
$sourceVideoId = $request->integer('source_video_id') ?: null;
if ($sourceVideoId && ! Video::whereKey($sourceVideoId)->exists()) {
$sourceVideoId = null;
}
$dedup = \App\Models\ProfileVisit::where('profile_user_id', $user->id)
->where('created_at', '>=', now()->subMinutes(30))
->when($visitorId, fn ($q) => $q->where('visitor_user_id', $visitorId))
->when(! $visitorId && $deviceId, fn ($q) => $q->whereNull('visitor_user_id')->where('device_id', $deviceId))
->when($sourceVideoId, fn ($q) => $q->where('source_video_id', $sourceVideoId))
->exists();
if ($dedup) {
return response()->json(['ok' => true, 'skipped' => 'dedup']);
}
$ip = $request->header('CF-Connecting-IP') ?? $request->header('X-Real-IP') ?? $request->ip();
$geo = \App\Services\GeoIpService::lookup($ip);
\App\Models\ProfileVisit::create([
'profile_user_id' => $user->id,
'visitor_user_id' => $visitorId,
'device_id' => $deviceId,
'source_video_id' => $sourceVideoId,
'ip_address' => $ip,
'country' => $geo['country'] ?? null,
'created_at' => now(),
]);
return response()->json(['ok' => true]);
}
public function toggleSubscribe(Request $request, User $user)
{
$me = Auth::user();
@@ -390,7 +459,11 @@ class UserController extends Controller
$me->subscriptions()->detach($user->id);
$subscribed = false;
} else {
$me->subscriptions()->attach($user->id);
$sourceVideoId = $request->integer('source_video_id') ?: null;
if ($sourceVideoId && ! \App\Models\Video::whereKey($sourceVideoId)->exists()) {
$sourceVideoId = null;
}
$me->subscriptions()->attach($user->id, ['source_video_id' => $sourceVideoId]);
$subscribed = true;
try { $user->notify(new NewSubscriberNotification($me)); } catch (\Throwable) {}
}