Skip to content

Instantly share code, notes, and snippets.

@CamKem
Last active April 10, 2023 13:00
Show Gist options
  • Save CamKem/8e9373e747886f4bfaee93920167aadf to your computer and use it in GitHub Desktop.
Save CamKem/8e9373e747886f4bfaee93920167aadf to your computer and use it in GitHub Desktop.
Breeze / Jetstream Registration with live username check.
  1. You need to add the new route to your web.php in the Routes folder.

  2. create a Users Controller if you don't have one already. you can place this logic elsewhere & update the route if you want

  3. Update you users migration and add in the "username" column.

  4. Replace the content of Auth/Register.vue with contents of the file above, make sure that you install axios, lodash & heroicons packages for this to work.

If you have any questions, leave a comment & I will respond.

<?php
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name')->nullable();
$table->string('username')->unique();
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
<script setup>
import {Link, useForm} from '@inertiajs/vue3';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import AppHead from "@/Layouts/AppHead.vue";
import {reactive, ref, toRefs} from "vue";
import axios from "axios";
import debounce from "lodash/debounce";
import CheckBadgeIcon from "@heroicons/vue/24/solid/CheckBadgeIcon.js";
import XCircleIcon from "@heroicons/vue/24/solid/XCircleIcon.js";
const form = useForm({
email: '',
username: '',
password: '',
password_confirmation: '',
terms: false,
});
const state = reactive({
available: false,
unavailable: false
});
let message = ref('');
// create a function that lists all the usernames that are not allowed
const unavailableUsernames = ['admin', 'root', 'administrator'];
const checkUsername = () => {
if (form.username.length < 4) {
state.available = false;
state.unavailable = false;
message = '';
form.errors.username = null;
return;
} else if (form.username.length > 20) {
state.available = false;
state.unavailable = true;
message = '';
form.errors.username = 'Username must be less than 20 characters';
return;
} else if (unavailableUsernames.includes(form.username)) {
state.available = false;
state.unavailable = true;
form.errors.username = 'Username is forbidden';
return;
} else {
axios.get(route('users.exists', form.username))
.then(response => {
if (response.data.exists) {
state.available = false;
state.unavailable = true;
message = '';
form.errors.username = 'The username has already been taken.';
} else if (!response.data.exists) {
state.available = true;
state.unavailable = false;
message = 'Username is available';
form.errors.username = null;
}
}).catch(error => {
console.error(error);
});
}
};
// set debounce delay to 500ms
const checkUsernameDebounced = debounce(checkUsername, 500);
const handleUsernameInput = () => {
// update form state
form.username = event.target.value;
// debounce the checkUsername function
checkUsernameDebounced();
};
const {available, unavailable} = toRefs(state);
const submit = () => {
form.post(route('register'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<AppHead title="Register">
<meta content="Register" name="description"/>
</AppHead>
<form @submit.prevent="submit">
<div class="m-2">
<InputLabel for="username" value="Username"/>
<div class="flex items-center justify-end">
<TextInput
id="username"
v-model="form.username"
autofocus
class="mt-1 block w-full"
type="text"
@input="handleUsernameInput"
/>
<div class="absolute">
<CheckBadgeIcon v-show="available" class="z-50 mt-1 mr-2 text-green-500 h-[28px] w-[28px]"/>
<XCircleIcon v-show="unavailable" class="z-50 mt-1 mr-2 text-red-500 h-[28px] w-[28px]"/>
</div>
</div>
<div v-show="message" class="mt-2 text-sm text-green-600">{{ message }}</div>
<InputError :message="form.errors.username" class="mt-2"/>
</div>
<div class="m-2 mt-4">
<InputLabel for="email" value="Email"/>
<TextInput
id="email"
v-model="form.email"
autocomplete="email"
class="mt-1 block w-full"
type="email"
/>
<InputError :message="form.errors.email" class="mt-2"/>
</div>
<div class="m-2 mt-4">
<InputLabel for="password" value="Password"/>
<TextInput
id="password"
v-model="form.password"
autocomplete="new-password"
class="mt-1 block w-full"
type="password"
/>
<InputError :message="form.errors.password" class="mt-2"/>
</div>
<div class="m-2 mt-4">
<InputLabel for="password_confirmation" value="Confirm Password"/>
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
autocomplete="new-password"
class="mt-1 block w-full"
type="password"
/>
<InputError :message="form.errors.password_confirmation" class="mt-2"/>
</div>
<div class="mt-4 flex items-center justify-end">
<Link :href="route('login')" class="text-sm text-gray-600 underline hover:text-gray-900">
Already registered?
</Link>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing" class="ml-4">
Register
</PrimaryButton>
</div>
<div class="m-2 mt-4 border-t border-gray-500 pt-4">
<p class="text-center text-xs text-gray-500">
Social Login Coming Soon here...
</p>
</div>
</form>
</template>
<style>
.right-inner-addon {
position: relative;
}
.right-inner-addon input {
padding-right: 30px;
width: 170px; /* 200px - 30px */
}
.right-inner-addon svg {
position: absolute;
right: -10px;
padding: 10px 12px;
pointer-events: none;
top: -8px;
}
</style>
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\User;
class UsersController extends Controller
{
public function usernameExists($username)
{
$exists = User::where('username', $username)->exists();
return ['exists' => $exists];
}
}
<?php
Route::get('/users/{user:username}/exists', [UsersController::class, 'usernameExists'])->name('users.exists');
@DevShaded
Copy link

Nice!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment