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
10 changes: 5 additions & 5 deletions app/Http/Controllers/BlogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function index(Request $request)
'views',
'created_at',
])
->with('user:id,name,username')
->with(['user:id,name,username', 'user.roles:id,name'])
->withCount(['reactions', 'comments'])
->where('is_published', true);

Expand Down Expand Up @@ -56,13 +56,13 @@ public function show(Blog $blog)
{
abort_unless($blog->is_published, 404);

$blog->load('user:id,name,username,image_path');
$blog->load(['user:id,name,username,image_path', 'user.roles:id,name']);
$blog->increment('views');

$reactionsCount = $blog->reactions()->count();

$reactors = $blog->reactions()
->with('user:id,name,username,image_path,institution')
->with(['user:id,name,username,image_path,institution', 'user.roles:id,name'])
->latest('id')
->limit(50)
->get()
Expand All @@ -71,7 +71,7 @@ public function show(Blog $blog)
->values();

$comments = $blog->comments()
->with('user:id,name,username,image_path,institution')
->with(['user:id,name,username,image_path,institution', 'user.roles:id,name'])
->latest()
->get();

Expand Down Expand Up @@ -131,7 +131,7 @@ public function storeComment(Request $request, Blog $blog)
'content' => trim($validated['content']),
]);

$comment->load('user:id,name,username,image_path,institution');
$comment->load(['user:id,name,username,image_path,institution', 'user.roles:id,name']);
$blog->loadMissing('user:id,name,email,receive_emails');

if ($blog->user && $blog->user_id !== $userId && $blog->user->email && $blog->user->receive_emails !== false) {
Expand Down
2 changes: 1 addition & 1 deletion app/Http/Controllers/ResourceController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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')
->with(['user:id,name,username,image_path,institution', 'user.roles:id,name'])
->latest()
->take(10)
->get()
Expand Down
24 changes: 24 additions & 0 deletions app/Http/Controllers/UserProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ public function show(string $username)
])
->filter(fn ($item) => $item['title'] !== null);

// Suggested / Discover community members: 2 contributors + 2 general users
$contributorUsers = User::where('id', '!=', $user->id)
->whereNotNull('username')
->whereHas('roles')
->select(['id', 'name', 'username', 'title', 'institution', 'image_path', 'about'])
->with('roles:id,name')
->inRandomOrder()
->take(2)
->get();

$excludedIds = $contributorUsers->pluck('id')->push($user->id)->all();
$remainingNeeded = 4 - $contributorUsers->count();

$randomUsers = User::whereNotIn('id', $excludedIds)
->whereNotNull('username')
->select(['id', 'name', 'username', 'title', 'institution', 'image_path', 'about'])
->with('roles:id,name')
->inRandomOrder()
->take($remainingNeeded)
->get();

$suggestedUsers = $contributorUsers->concat($randomUsers)->shuffle()->values();

return Inertia::render('User/Show', [
'profileUser' => [
'id' => $user->id,
Expand Down Expand Up @@ -125,6 +148,7 @@ public function show(string $username)
'reactions' => $recentReactions->values(),
'comments' => $recentComments->values(),
],
'suggestedUsers' => $suggestedUsers,
]);
}
}
1 change: 1 addition & 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
'instagram',
'github',
];

protected static function booted(): void
{
static::created(function (User $user) {
Expand Down
20 changes: 15 additions & 5 deletions resources/js/components/BlogCard.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { ArrowRight, Heart, MessageSquare } from 'lucide-vue-next';
import { ArrowRight, Heart, MessageSquare, BadgeCheck } from 'lucide-vue-next';
import { computed } from 'vue';

const props = defineProps({
Expand Down Expand Up @@ -59,15 +59,25 @@ const formattedDate = computed(() => {
<Link
v-if="blog.user?.username"
:href="`/u/${blog.user.username}`"
class="font-medium text-indigo-600 underline transition-colors hover:underline dark:text-indigo-400"
class="inline-flex items-center gap-1 font-medium text-indigo-600 underline transition-colors hover:underline dark:text-indigo-400"
>
{{ blog.user?.name }}
<span>{{ blog.user?.name }}</span>
<BadgeCheck
v-if="blog.user?.roles && blog.user.roles.length > 0"
class="h-3.5 w-3.5 fill-blue-50 stroke-[2.2] text-blue-600 dark:fill-blue-950/60 dark:text-blue-400"
title="Verified Contributor"
/>
</Link>
<span
v-else
class="font-medium text-slate-700 dark:text-gray-300"
class="inline-flex items-center gap-1 font-medium text-slate-700 dark:text-gray-300"
>
{{ blog.user?.name }}
<span>{{ blog.user?.name }}</span>
<BadgeCheck
v-if="blog.user?.roles && blog.user.roles.length > 0"
class="h-3.5 w-3.5 fill-blue-50 stroke-[2.2] text-blue-600 dark:fill-blue-950/60 dark:text-blue-400"
title="Verified Contributor"
/>
</span>

<span
Expand Down
25 changes: 18 additions & 7 deletions resources/js/components/UserCard.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { GraduationCap, ArrowRight } from 'lucide-vue-next';
import { GraduationCap, ArrowRight, BadgeCheck } from 'lucide-vue-next';
import { computed } from 'vue';

const props = defineProps<{
Expand Down Expand Up @@ -81,12 +81,23 @@ const roleInfo = computed(() => {

<!-- Name, Handle & Role -->
<div class="space-y-1">
<Link
:href="profileUrl"
class="text-lg font-black tracking-tight text-slate-900 hover:text-indigo-600 dark:text-gray-100 dark:hover:text-indigo-400"
>
{{ member.name }}
</Link>
<div class="flex items-center justify-center gap-1.5">
<Link
:href="profileUrl"
class="text-lg font-black tracking-tight text-slate-900 hover:text-indigo-600 dark:text-gray-100 dark:hover:text-indigo-400"
>
{{ member.name }}
</Link>
<span
v-if="member.roles && member.roles.length > 0"
class="inline-flex items-center text-blue-600 dark:text-blue-400"
title="Verified HSCStack Contributor"
>
<BadgeCheck
class="h-4.5 w-4.5 fill-blue-50 stroke-[2.2] dark:fill-blue-950/60"
/>
</span>
</div>

<!-- Role Pill -->
<div class="flex items-center justify-center pt-0.5">
Expand Down
88 changes: 88 additions & 0 deletions resources/js/components/UserListItem.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
import { BadgeCheck } from 'lucide-vue-next';
import { computed } from 'vue';

const props = withDefaults(
defineProps<{
user: {
id?: number;
name: string;
username?: string;
image_url?: string | null;
image_path?: string | null;
institution?: string | null;
title?: string | null;
roles?: Array<{ id?: number; name: string }>;
};
theme?: 'indigo' | 'emerald' | 'rose';
subtitle?: string | null;
}>(),
{
theme: 'indigo',
subtitle: null,
},
);

const profileUrl = computed(() => {
return props.user.username ? `/u/${props.user.username}` : '#';
});

const isVerified = computed(() => {
return Boolean(props.user.roles && props.user.roles.length > 0);
});

const avatarBgClass = computed(() => {
if (props.theme === 'rose') {
return 'bg-rose-50 text-rose-600 dark:bg-rose-950/60 dark:text-rose-300 group-hover/user:ring-rose-400';
}

if (props.theme === 'emerald') {
return 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300 group-hover/user:ring-emerald-400';
}

return 'bg-indigo-50 text-indigo-600 dark:bg-indigo-950/60 dark:text-indigo-300 group-hover/user:ring-indigo-400';
});
</script>

<template>
<Link
:href="profileUrl"
class="group/user flex items-center gap-3 py-2.5 transition"
>
<div
class="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full font-semibold transition group-hover/user:ring-2"
:class="avatarBgClass"
>
<img
v-if="user.image_url || user.image_path"
:src="user.image_url || '/storage/' + user.image_path"
:alt="user.name"
class="h-full w-full object-cover"
/>
<span v-else class="text-xs uppercase">
{{ user.name?.charAt(0) || 'U' }}
</span>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1">
<p
class="truncate text-xs font-semibold text-slate-900 group-hover/user:text-indigo-600 dark:text-gray-100 dark:group-hover/user:text-indigo-400"
>
{{ user.name }}
</p>
<BadgeCheck
v-if="isVerified"
class="h-3.5 w-3.5 shrink-0 fill-blue-50 stroke-[2.2] text-blue-600 dark:fill-blue-950/60 dark:text-blue-400"
title="Verified Contributor"
/>
</div>
<p
v-if="subtitle || user.institution || user.title"
class="truncate text-[11px] text-slate-500 dark:text-gray-400"
>
{{ subtitle || user.institution || user.title }}
</p>
</div>
</Link>
</template>
86 changes: 39 additions & 47 deletions resources/js/pages/Blog/Show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import {
LogIn,
X,
Loader2,
BadgeCheck,
} from 'lucide-vue-next';
import { computed, ref, watch } from 'vue';
import UserListItem from '@/components/UserListItem.vue';

const props = defineProps({
blog: {
Expand Down Expand Up @@ -275,9 +277,16 @@ const goBack = () => {
? `/u/${blog.user.username}`
: '#'
"
class="font-medium text-indigo-600 transition-colors hover:text-indigo-800 hover:underline dark:text-indigo-400 dark:hover:text-indigo-300"
class="inline-flex items-center gap-1 font-medium text-indigo-600 transition-colors hover:text-indigo-800 hover:underline dark:text-indigo-400 dark:hover:text-indigo-300"
>
{{ blog.user?.name }}
<span>{{ blog.user?.name }}</span>
<BadgeCheck
v-if="
blog.user?.roles && blog.user.roles.length > 0
"
class="h-4 w-4 fill-blue-50 stroke-[2.2] text-blue-600 dark:fill-blue-950/60 dark:text-blue-400"
title="Verified Contributor"
/>
</Link>
</div>

Expand Down Expand Up @@ -622,16 +631,28 @@ const goBack = () => {
</Link>

<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<div
class="flex flex-wrap items-center gap-1.5"
>
<Link
:href="
comment.user?.username
? `/u/${comment.user.username}`
: '#'
"
class="text-sm font-bold text-slate-900 transition hover:text-indigo-600 dark:text-gray-100 dark:hover:text-indigo-400"
class="inline-flex items-center gap-1 text-sm font-bold text-slate-900 transition hover:text-indigo-600 dark:text-gray-100 dark:hover:text-indigo-400"
>
{{ comment.user?.name || 'Anonymous' }}
<span>{{
comment.user?.name || 'Anonymous'
}}</span>
<BadgeCheck
v-if="
comment.user?.roles &&
comment.user.roles.length > 0
"
class="h-3.5 w-3.5 fill-blue-50 stroke-[2.2] text-blue-600 dark:fill-blue-950/60 dark:text-blue-400"
title="Verified Contributor"
/>
</Link>
<span
v-if="comment.user_id === blog.user_id"
Expand Down Expand Up @@ -699,9 +720,16 @@ const goBack = () => {
? `/u/${blog.user.username}`
: '#'
"
class="mt-1 block text-lg font-semibold text-slate-900 underline transition dark:text-gray-100"
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 }}
<span>{{ blog.user?.name }}</span>
<BadgeCheck
v-if="
blog.user?.roles && blog.user.roles.length > 0
"
class="h-4.5 w-4.5 fill-blue-50 stroke-[2.2] text-blue-600 dark:fill-blue-950/60 dark:text-blue-400"
title="Verified Contributor"
/>
</Link>

<Link
Expand Down Expand Up @@ -763,48 +791,12 @@ const goBack = () => {
No reactions yet.
</div>

<Link
<UserListItem
v-for="reactor in reactors"
:key="reactor.id"
:href="
reactor.username
? `/u/${reactor.username}`
: '#'
"
class="group/user flex items-center gap-3 py-2.5 transition"
>
<div
class="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-rose-50 font-semibold text-rose-600 transition group-hover/user:ring-2 group-hover/user:ring-rose-400 dark:bg-rose-950/60 dark:text-rose-300"
>
<img
v-if="
reactor.image_url || reactor.image_path
"
:src="
reactor.image_url ||
'/storage/' + reactor.image_path
"
:alt="reactor.name"
class="h-full w-full object-cover"
/>
<span v-else class="text-xs uppercase">
{{ reactor.name?.charAt(0) || 'U' }}
</span>
</div>
<div class="min-w-0 flex-1">
<p
class="truncate text-xs font-semibold text-slate-900 group-hover/user:text-indigo-600 dark:text-gray-100 dark:group-hover/user:text-indigo-400"
>
{{ reactor.name }}
</p>
<p
v-if="reactor.institution"
class="truncate text-[11px] text-slate-500 dark:text-gray-400"
>
{{ reactor.institution }}
</p>
</div>
</Link>
:user="reactor"
theme="rose"
/>

<div
v-if="localReactionsCount > reactors.length"
Expand Down
Loading