diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index a0aab43..f831645 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -1,7 +1,7 @@ # app/controllers/repositories_controller.rb class RepositoriesController < ApplicationController before_action :authenticate_user! - before_action :set_user + before_action :set_user, except: [ :check_name ] def index @repositories = @user.repositories @@ -22,6 +22,13 @@ def create end end + def check_name + validator = GithubRepositoryNameValidator.new( + params[:name], + current_user.github_username + ) + render json: { available: validator.valid? } + end private def set_user diff --git a/app/javascript/controllers/repository_name_validator_controller.js b/app/javascript/controllers/repository_name_validator_controller.js new file mode 100644 index 0000000..d81719a --- /dev/null +++ b/app/javascript/controllers/repository_name_validator_controller.js @@ -0,0 +1,90 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["input", "message", "spinner"] + static values = { + checkUrl: String, + debounce: { type: Number, default: 500 } + } + + static classes = ["error", "success"] + + initialize() { + this.timeout = null + this.debouncedValidate = this.debounce( + this.performValidation.bind(this), + this.debounceValue + ) + } + + debounce(func, wait) { + return (...args) => { + clearTimeout(this.timeout) + this.timeout = setTimeout(() => func.apply(this, args), wait) + } + } + + validate() { + this.spinnerTarget.classList.remove('hidden') + this.messageTarget.classList.add('hidden') + this.debouncedValidate() + } + + async performValidation() { + const name = this.inputTarget.value + const valid = this.validateFormat(name) + + this.spinnerTarget.classList.add('hidden') + + if (!valid) { + this.showMessage("Invalid format. Use only letters, numbers, and single hyphens.", "error") + return + } + + try { + const available = await this.checkAvailability(name) + if (!available) { + this.showMessage("Repository name already taken", "error") + return + } + this.showMessage("Name available", "success") + } catch (error) { + // Error message already shown by checkAvailability + return + } + } + + validateFormat(name) { + const regex = /^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/ + return regex.test(name) && !name.includes('--') + } + + async checkAvailability(name) { + try { + const response = await fetch(`${this.checkUrlValue}?name=${encodeURIComponent(name)}`) + if (!response.ok) throw new Error('Network response was not ok') + const data = await response.json() + return data.available + } catch (error) { + console.error('Error checking availability:', error) + this.showMessage("Error checking availability", "error") + throw error + } + } + + showMessage(message, type) { + this.messageTarget.textContent = message + this.messageTarget.classList.remove("hidden", this.errorClasses, this.successClasses) + this.messageTarget.classList.add(type === "error" ? this.errorClasses : this.successClasses) + } + + hideMessage() { + this.messageTarget.classList.add("hidden") + } + + disconnect() { + if (this.timeout) { + clearTimeout(this.timeout) + } + } +} diff --git a/app/services/github_repository_name_validator.rb b/app/services/github_repository_name_validator.rb new file mode 100644 index 0000000..44a9177 --- /dev/null +++ b/app/services/github_repository_name_validator.rb @@ -0,0 +1,32 @@ +class GithubRepositoryNameValidator + VALID_FORMAT = /\A[a-zA-Z0-9][a-zA-Z0-9-]*(?