diff --git a/app/Http/Controllers/BlogController.php b/app/Http/Controllers/BlogController.php index e97160e..8e620b6 100644 --- a/app/Http/Controllers/BlogController.php +++ b/app/Http/Controllers/BlogController.php @@ -25,7 +25,7 @@ public function index(Request $request) 'views', 'created_at', ]) - ->with('user:id,name') + ->with('user:id,name,username') ->withCount(['reactions', 'comments']) ->where('is_published', true); @@ -56,13 +56,13 @@ public function show(Blog $blog) { abort_unless($blog->is_published, 404); - $blog->load('user:id,name'); + $blog->load('user:id,name,username'); $blog->increment('views'); $reactionsCount = $blog->reactions()->count(); $reactors = $blog->reactions() - ->with('user:id,name,image_path,institution') + ->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,image_path,institution') + ->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 80f4b2e..91c1f5c 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,image_path,institution') + ->with('user:id,name,username,image_path,institution') ->latest() ->take(10) ->get() diff --git a/app/Http/Controllers/SubjectController.php b/app/Http/Controllers/SubjectController.php index 7f2456b..d398d6f 100644 --- a/app/Http/Controllers/SubjectController.php +++ b/app/Http/Controllers/SubjectController.php @@ -27,7 +27,7 @@ public function index($course) $featuredBlogs = Blog::where('is_featured', true) ->where('is_published', true) - ->with('user:id,name') + ->with('user:id,name,username') ->withCount(['reactions', 'comments']) ->inRandomOrder() ->limit(3) diff --git a/app/Http/Requests/Profile/UpdateProfileRequest.php b/app/Http/Requests/Profile/UpdateProfileRequest.php index 867d782..a503ee0 100644 --- a/app/Http/Requests/Profile/UpdateProfileRequest.php +++ b/app/Http/Requests/Profile/UpdateProfileRequest.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class UpdateProfileRequest extends FormRequest { @@ -24,6 +25,14 @@ public function rules(): array { return [ 'name' => ['required', 'string', 'max:255'], + 'username' => [ + 'sometimes', + 'string', + 'min:3', + 'max:30', + 'regex:/^[a-zA-Z0-9_]+$/', + Rule::unique('users', 'username')->ignore($this->user()->id), + ], 'file' => ['sometimes', 'nullable', 'image', 'max:2048'], 'about' => ['sometimes', 'nullable', 'string'], 'institution' => ['sometimes', 'nullable', 'string', 'max:255'], diff --git a/app/Http/Requests/User/StoreUserRequest.php b/app/Http/Requests/User/StoreUserRequest.php index 00cd66a..4146eb5 100644 --- a/app/Http/Requests/User/StoreUserRequest.php +++ b/app/Http/Requests/User/StoreUserRequest.php @@ -25,6 +25,7 @@ public function rules(): array return [ 'name' => ['required', 'string', 'max:255'], + 'username' => ['nullable', 'string', 'min:3', 'max:30', 'regex:/^[a-zA-Z0-9_]+$/', 'unique:users,username'], 'email' => ['required', 'email', 'unique:users,email'], 'role' => ['required', 'string', 'exists:roles,name'], 'permissions' => ['required', 'array'], diff --git a/app/Http/Requests/User/UpdateUserRequest.php b/app/Http/Requests/User/UpdateUserRequest.php index 84638ae..a16983a 100644 --- a/app/Http/Requests/User/UpdateUserRequest.php +++ b/app/Http/Requests/User/UpdateUserRequest.php @@ -26,6 +26,7 @@ public function rules(): array $rules = [ 'name' => ['sometimes', 'string', 'max:255'], + 'username' => ['sometimes', 'string', 'min:3', 'max:30', 'regex:/^[a-zA-Z0-9_]+$/', 'unique:users,username,'.$user->id], 'email' => ['sometimes', 'email', 'unique:users,email,'.$user->id], 'file' => ['sometimes', 'nullable', 'image', 'max:2048'], 'about' => ['sometimes', 'nullable', 'string'], diff --git a/app/Models/User.php b/app/Models/User.php index 206f211..107ea09 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -16,6 +16,7 @@ /** * @property int $id * @property string $name + * @property string|null $username * @property string $email * @property Carbon|null $email_verified_at * @property string $password @@ -38,6 +39,7 @@ class User extends Authenticatable protected $fillable = [ 'name', + 'username', 'email', 'receive_emails', 'google_id', @@ -51,6 +53,16 @@ class User extends Authenticatable 'github', ]; + protected static function booted(): void + { + static::created(function (User $user) { + if (! $user->username) { + $user->updateQuietly(['username' => "student_{$user->id}"]); + $user->username = "student_{$user->id}"; + } + }); + } + protected $appends = [ 'image_url', ]; diff --git a/database/migrations/2026_08_26_120400_add_username_to_users_table.php b/database/migrations/2026_08_26_120400_add_username_to_users_table.php new file mode 100644 index 0000000..fc37b18 --- /dev/null +++ b/database/migrations/2026_08_26_120400_add_username_to_users_table.php @@ -0,0 +1,33 @@ +string('username')->nullable()->unique()->after('name'); + }); + + User::whereNull('username')->each(function ($user) { + $user->updateQuietly(['username' => "student_{$user->id}"]); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('username'); + }); + } +}; diff --git a/resources/js/components/NavBar.vue b/resources/js/components/NavBar.vue index d741a54..f774881 100644 --- a/resources/js/components/NavBar.vue +++ b/resources/js/components/NavBar.vue @@ -219,7 +219,7 @@ onBeforeUnmount(() => {
- {{ user.email }} + {{ user.username ? '@' + user.username : user.email }}
@@ -323,15 +323,19 @@ onBeforeUnmount(() => { v-if="user" class="mb-3 flex items-center gap-3 rounded-2xl border border-slate-200/80 bg-slate-50/80 p-3 dark:border-gray-800 dark:bg-gray-900/80" > -- {{ user.email }} + {{ user.username ? '@' + user.username : user.email }}
diff --git a/resources/js/components/admin/UserRow.vue b/resources/js/components/admin/UserRow.vue index 4c00b17..fe24f10 100644 --- a/resources/js/components/admin/UserRow.vue +++ b/resources/js/components/admin/UserRow.vue @@ -61,6 +61,12 @@ const deleteUser = (id: number) => { class="truncate text-sm font-semibold text-gray-900 dark:text-gray-100" >{{ user.name }} + + @{{ user.username }} + { ++ {{ form.errors.username }} +
++ Letters, numbers, and underscores (3–30 chars). +
+