diff --git a/app/Http/Controllers/Admin/AuthController.php b/app/Http/Controllers/Admin/AuthController.php index 4bfdee2..ce3230a 100644 --- a/app/Http/Controllers/Admin/AuthController.php +++ b/app/Http/Controllers/Admin/AuthController.php @@ -81,7 +81,11 @@ public function handleGoogleCallback(Request $request) Auth::login($user, remember: true); $request->session()->regenerate(); - $redirect = redirect()->intended(route('profile.edit')); + $defaultUrl = $user->username + ? route('user.profile', $user->username) + : route('profile.edit'); + + $redirect = redirect()->intended($defaultUrl); if ($isNewUser) { $redirect->with('success', 'Account not found. New account created.'); diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php index 88e3e51..9ad2f44 100644 --- a/app/Http/Controllers/BlogController.php +++ b/app/Http/Controllers/BlogController.php @@ -62,7 +62,7 @@ public function show(Blog $blog) $reactionsCount = $blog->reactions()->count(); $reactors = $blog->reactions() - ->with(['user:id,name,username,image_path,institution', 'user.roles:id,name']) + ->with(['user:id,name,username,image_path,institution']) ->latest('id') ->limit(50) ->get() @@ -71,7 +71,7 @@ public function show(Blog $blog) ->values(); $comments = $blog->comments() - ->with(['user:id,name,username,image_path,institution', 'user.roles:id,name']) + ->with(['user:id,name,username,image_path,institution']) ->latest() ->get(); diff --git a/app/Http/Controllers/ResourceController.php b/app/Http/Controllers/ResourceController.php index 656bc35..bf4cf79 100644 --- a/app/Http/Controllers/ResourceController.php +++ b/app/Http/Controllers/ResourceController.php @@ -38,7 +38,7 @@ public function show($id) $completionsCount = ResourceCompletion::where('resource_id', $id)->count(); $completers = ResourceCompletion::where('resource_id', $id) - ->with(['user:id,name,username,image_path,institution', 'user.roles:id,name']) + ->with(['user:id,name,username,image_path,institution']) ->latest() ->take(10) ->get() diff --git a/app/Http/Controllers/UserProfileController.php b/app/Http/Controllers/UserProfileController.php index 4a9d859..a4e94cf 100644 --- a/app/Http/Controllers/UserProfileController.php +++ b/app/Http/Controllers/UserProfileController.php @@ -21,8 +21,6 @@ public function show(string $username) $user = User::where('username', $username) ->firstOrFail(); - $user->load('roles:id,name'); - // Study Completions $completedResourcesCount = $user->completedResources()->count(); $recentCompletions = $user->completedResources() @@ -40,7 +38,7 @@ public function show(string $username) ->get(); $blogsCount = $user->blogs()->where('is_published', true)->count(); $totalBlogViews = (int) $user->blogs()->where('is_published', true)->sum('views'); - $totalBlogLikes = BlogReaction::whereIn('blog_id', $user->blogs()->where('is_published', true)->select('id'))->count(); + $upvotesCount = NodeVote::where('user_id', $user->id)->where('type', 'up')->count(); $sharedResourcesCount = Resource::where('user_id', $user->id)->count(); // Appreciations (Received & Given) @@ -52,23 +50,45 @@ public function show(string $username) $appreciators = $user->appreciators() ->select(['users.id', 'users.name', 'users.username', 'users.image_path', 'users.institution', 'users.title']) - ->with('roles:id,name') - ->latest('user_appreciations.id') - ->take(50) + ->inRandomOrder() + ->take(30) ->get(); $appreciating = $user->appreciatingUsers() ->select(['users.id', 'users.name', 'users.username', 'users.image_path', 'users.institution', 'users.title']) - ->with('roles:id,name') - ->latest('user_appreciations.id') - ->take(50) + ->inRandomOrder() + ->take(30) ->get(); - // Recent Community Activities (Uploads, completed topics, comments made, reactions given, upvoted folders, appreciations given) + // Recent Community Activities (Folders created, uploads, completed topics, comments made, reactions given, upvoted folders, appreciations given) + $recentFolders = Node::where('user_id', $user->id) + ->with([ + 'subject:id,name,slug', + 'parent:id,name,slug', + 'parent.parent:id,name,slug', + ]) + ->latest('id') + ->take(3) + ->get() + ->map(function ($node) { + $url = $this->buildNodeUrl($node); + + return [ + 'type' => 'folder', + 'title' => $node->name, + 'subtitle' => $node->subject?->name.($node->parent ? ' · '.$node->parent->name : ''), + 'url' => $url, + 'created_at' => $node->created_at?->diffForHumans(), + 'timestamp' => $node->created_at?->timestamp ?? 0, + ]; + }) + ->filter() + ->values(); + $recentUploads = Resource::where('user_id', $user->id) ->with(['node:id,name,subject_id', 'node.subject:id,name']) ->latest() - ->take(4) + ->take(3) ->get() ->map(fn ($item) => [ 'type' => 'upload', @@ -77,12 +97,13 @@ public function show(string $username) 'resource_type' => $item->resource_type, 'url' => "/resources/{$item->id}", 'created_at' => $item->created_at?->diffForHumans(), + 'timestamp' => $item->created_at?->timestamp ?? 0, ]); $recentCompleted = ResourceCompletion::where('user_id', $user->id) ->with(['resource:id,title,node_id', 'resource.node:id,name,subject_id', 'resource.node.subject:id,name']) ->latest() - ->take(4) + ->take(3) ->get() ->map(fn ($item) => [ 'type' => 'completion', @@ -90,26 +111,28 @@ public function show(string $username) 'subtitle' => $item->resource?->node?->subject?->name.' · '.$item->resource?->node?->name, 'url' => $item->resource ? "/resources/{$item->resource->id}" : null, 'created_at' => $item->created_at?->diffForHumans(), + 'timestamp' => $item->created_at?->timestamp ?? 0, ]) ->filter(fn ($item) => $item['title'] !== null); $recentReactions = BlogReaction::where('user_id', $user->id) ->with('blog:id,title,slug') ->latest() - ->take(4) + ->take(3) ->get() ->map(fn ($item) => [ 'type' => 'reaction', 'title' => $item->blog?->title, 'url' => $item->blog ? "/blogs/{$item->blog->slug}" : null, 'created_at' => $item->created_at?->diffForHumans(), + 'timestamp' => $item->created_at?->timestamp ?? 0, ]) ->filter(fn ($item) => $item['title'] !== null); $recentComments = BlogComment::where('user_id', $user->id) ->with('blog:id,title,slug') ->latest() - ->take(4) + ->take(3) ->get() ->map(fn ($item) => [ 'type' => 'comment', @@ -117,6 +140,7 @@ public function show(string $username) 'content' => $item->content, 'url' => $item->blog ? "/blogs/{$item->blog->slug}" : null, 'created_at' => $item->created_at?->diffForHumans(), + 'timestamp' => $item->created_at?->timestamp ?? 0, ]) ->filter(fn ($item) => $item['title'] !== null); @@ -128,7 +152,7 @@ public function show(string $username) 'node.parent.parent:id,name,slug', ]) ->latest('id') - ->take(4) + ->take(3) ->get() ->map(function ($item) { $node = $item->node; @@ -144,6 +168,7 @@ public function show(string $username) 'subtitle' => $node->subject?->name.($node->parent ? ' · '.$node->parent->name : ''), 'url' => $url, 'created_at' => $item->created_at?->diffForHumans(), + 'timestamp' => $item->created_at?->timestamp ?? 0, ]; }) ->filter() @@ -152,7 +177,7 @@ public function show(string $username) $recentAppreciations = UserAppreciation::where('appreciator_id', $user->id) ->with('user:id,name,username') ->latest('id') - ->take(4) + ->take(3) ->get() ->map(fn ($item) => [ 'type' => 'appreciation', @@ -160,6 +185,7 @@ public function show(string $username) 'username' => $item->user?->username, 'url' => $item->user ? "/u/{$item->user->username}" : null, 'created_at' => $item->created_at?->diffForHumans(), + 'timestamp' => $item->created_at?->timestamp ?? 0, ]) ->filter(fn ($item) => $item['title'] !== null) ->values(); @@ -169,7 +195,6 @@ public function show(string $username) ->whereNotNull('username') ->whereHas('roles') ->select(['id', 'name', 'username', 'title', 'institution', 'image_path', 'about']) - ->with('roles:id,name') ->inRandomOrder() ->take(2) ->get(); @@ -180,7 +205,6 @@ public function show(string $username) $randomUsers = User::whereNotIn('id', $excludedIds) ->whereNotNull('username') ->select(['id', 'name', 'username', 'title', 'institution', 'image_path', 'about']) - ->with('roles:id,name') ->inRandomOrder() ->take($remainingNeeded) ->get(); @@ -200,13 +224,13 @@ public function show(string $username) 'instagram' => $user->instagram, 'github' => $user->github, 'created_at' => $user->created_at?->format('M Y') ?? '2026', - 'roles' => $user->roles->pluck('name'), - 'is_staff' => $user->roles->isNotEmpty(), + 'is_verified' => $user->is_verified, + 'is_staff' => $user->is_verified, ], 'stats' => [ 'completedResourcesCount' => $completedResourcesCount, 'blogsCount' => $blogsCount, - 'totalBlogLikes' => (int) $totalBlogLikes, + 'upvotesCount' => $upvotesCount, 'totalBlogViews' => (int) $totalBlogViews, 'sharedResourcesCount' => $sharedResourcesCount, ], @@ -218,6 +242,7 @@ public function show(string $username) 'recentCompletions' => $recentCompletions, 'blogs' => $publishedBlogs, 'recentActivities' => [ + 'folders' => $recentFolders->values(), 'uploads' => $recentUploads->values(), 'completions' => $recentCompleted->values(), 'reactions' => $recentReactions->values(), diff --git a/app/Models/User.php b/app/Models/User.php index bda86bd..c86e494 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -64,8 +64,16 @@ protected static function booted(): void protected $appends = [ 'image_url', + 'is_verified', ]; + public function getIsVerifiedAttribute(): bool + { + return $this->relationLoaded('roles') + ? $this->roles->isNotEmpty() + : $this->roles()->exists(); + } + public function getImageUrlAttribute() { if (! $this->image_path) { diff --git a/resources/js/components/UserListItem.vue b/resources/js/components/UserListItem.vue index 0954ac8..937d50b 100644 --- a/resources/js/components/UserListItem.vue +++ b/resources/js/components/UserListItem.vue @@ -1,6 +1,6 @@ + + diff --git a/resources/js/pages/Blog/Show.vue b/resources/js/pages/Blog/Show.vue index 3c91ae1..955e65b 100644 --- a/resources/js/pages/Blog/Show.vue +++ b/resources/js/pages/Blog/Show.vue @@ -14,10 +14,10 @@ import { LogIn, X, Loader2, - BadgeCheck, } from 'lucide-vue-next'; import { computed, ref, watch } from 'vue'; import UserListItem from '@/components/UserListItem.vue'; +import VerifiedBadge from '@/components/VerifiedBadge.vue'; const props = defineProps({ blog: { @@ -638,13 +638,8 @@ const goBack = () => { {{ comment.user?.name || 'Anonymous' }} - { class="mt-1 inline-flex items-center gap-1.5 text-lg font-semibold text-slate-900 underline transition dark:text-gray-100" > {{ blog.user?.name }} - diff --git a/resources/js/pages/Profile.vue b/resources/js/pages/Profile.vue index fe1696d..21ae4af 100644 --- a/resources/js/pages/Profile.vue +++ b/resources/js/pages/Profile.vue @@ -21,6 +21,10 @@ const page = usePage(); const user = computed(() => props.user || page.props.auth?.user); const hasNoRole = computed(() => { + if (typeof user.value?.is_verified === 'boolean') { + return !user.value.is_verified; + } + const roles = user.value?.roles; return !roles || roles.length === 0; diff --git a/resources/js/pages/User/Show.vue b/resources/js/pages/User/Show.vue index 7560753..a66ca85 100644 --- a/resources/js/pages/User/Show.vue +++ b/resources/js/pages/User/Show.vue @@ -13,6 +13,7 @@ import { Eye, Facebook, FileText, + Folder, Github, GraduationCap, Heart, @@ -25,6 +26,7 @@ import { } from 'lucide-vue-next'; import { computed, ref, watch } from 'vue'; import UserListItem from '@/components/UserListItem.vue'; +import VerifiedBadge from '@/components/VerifiedBadge.vue'; const props = defineProps<{ profileUser: { @@ -39,13 +41,14 @@ const props = defineProps<{ instagram: string | null; github: string | null; created_at: string; - roles: string[]; - is_staff: boolean; + is_verified?: boolean; + is_staff?: boolean; + roles?: string[]; }; stats: { completedResourcesCount: number; blogsCount: number; - totalBlogLikes: number; + upvotesCount: number; totalBlogViews: number; sharedResourcesCount: number; }; @@ -58,7 +61,7 @@ const props = defineProps<{ username: string; image_path?: string | null; institution?: string | null; - roles?: Array<{ id: number; name: string }>; + is_verified?: boolean; }>; appreciating?: Array<{ id: number; @@ -66,7 +69,7 @@ const props = defineProps<{ username: string; image_path?: string | null; institution?: string | null; - roles?: Array<{ id: number; name: string }>; + is_verified?: boolean; }>; recentCompletions: Array<{ id: number; @@ -93,6 +96,14 @@ const props = defineProps<{ created_at: string; }>; recentActivities: { + folders?: Array<{ + type: string; + title: string; + subtitle?: string; + url: string | null; + created_at: string; + timestamp?: number; + }>; uploads: Array<{ type: string; title: string; @@ -100,6 +111,7 @@ const props = defineProps<{ resource_type?: string; url: string | null; created_at: string; + timestamp?: number; }>; completions: Array<{ type: string; @@ -107,12 +119,14 @@ const props = defineProps<{ subtitle?: string; url: string | null; created_at: string; + timestamp?: number; }>; reactions: Array<{ type: string; title: string; url: string | null; created_at: string; + timestamp?: number; }>; comments: Array<{ type: string; @@ -120,6 +134,7 @@ const props = defineProps<{ content?: string; url: string | null; created_at: string; + timestamp?: number; }>; upvotes?: Array<{ type: string; @@ -127,6 +142,7 @@ const props = defineProps<{ subtitle?: string; url: string | null; created_at: string; + timestamp?: number; }>; appreciations?: Array<{ type: string; @@ -134,6 +150,7 @@ const props = defineProps<{ username?: string; url: string | null; created_at: string; + timestamp?: number; }>; }; suggestedUsers?: Array<{ @@ -145,7 +162,7 @@ const props = defineProps<{ image_url: string | null; image_path: string | null; about: string | null; - roles?: Array<{ id: number; name: string }>; + is_verified?: boolean; }>; }>(); @@ -163,6 +180,34 @@ const activeTab = ref<'completed' | 'blogs' | 'activity'>( : 'completed', ); +interface ActivityItem { + type: string; + title?: string | null; + subtitle?: string | null; + content?: string | null; + username?: string | null; + resource_type?: string | null; + url: string | null; + created_at: string; + timestamp?: number; +} + +const sortedActivities = computed(() => { + const list: ActivityItem[] = [ + ...((props.recentActivities.folders || []) as ActivityItem[]), + ...((props.recentActivities.uploads || []) as ActivityItem[]), + ...((props.recentActivities.completions || []) as ActivityItem[]), + ...((props.recentActivities.reactions || []) as ActivityItem[]), + ...((props.recentActivities.comments || []) as ActivityItem[]), + ...((props.recentActivities.upvotes || []) as ActivityItem[]), + ...((props.recentActivities.appreciations || []) as ActivityItem[]), + ]; + + return list.sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)); +}); + +const totalActivitiesCount = computed(() => sortedActivities.value.length); + const localIsAppreciated = ref(props.isAppreciated); const localAppreciationsCount = ref(props.appreciationsCount); const showAppreciatorsModal = ref(false); @@ -219,45 +264,6 @@ const handleAppreciate = () => { }, ); }; - -const getRoleBadge = (roles: string[]) => { - if (roles.includes('admin')) { - return { - label: 'Admin', - class: 'bg-rose-50 text-rose-700 border-rose-200/80 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-800/60', - }; - } - - if (roles.includes('editor')) { - return { - label: 'Editor', - class: 'bg-purple-50 text-purple-700 border-purple-200/80 dark:bg-purple-950/40 dark:text-purple-300 dark:border-purple-800/60', - }; - } - - if (roles.includes('manager')) { - return { - label: 'Staff', - class: 'bg-amber-50 text-amber-700 border-amber-200/80 dark:bg-amber-950/40 dark:text-amber-300 dark:border-amber-800/60', - }; - } - - return { - label: 'Student', - class: 'bg-blue-50 text-blue-700 border-blue-200/80 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-800/60', - }; -}; - -const roleInfo = computed(() => getRoleBadge(props.profileUser.roles)); - -const totalActivitiesCount = computed( - () => - (props.recentActivities.uploads?.length || 0) + - (props.recentActivities.reactions?.length || 0) + - (props.recentActivities.comments?.length || 0) + - (props.recentActivities.upvotes?.length || 0) + - (props.recentActivities.appreciations?.length || 0), -);