javascript

exercises

exercises.js
/**
 * ========================================
 * 10.5 Forms and User Input - Exercises
 * ========================================
 *
 * Practice form handling, validation, and user input processing.
 * Complete each exercise by filling in the code.
 */

/**
 * EXERCISE 1: Form Value Reader
 *
 * Create a function that extracts all values from a form
 * and returns them as an object.
 *
 * Requirements:
 * - Handle text, email, password inputs
 * - Handle checkboxes (return array of checked values for same-name checkboxes)
 * - Handle radio buttons (return selected value)
 * - Handle select elements (single and multiple)
 * - Handle textarea
 */

function getFormValues(form) {
  // Your code here
}

// Test HTML for Exercise 1:
/*
<form id="testForm">
    <input type="text" name="username" value="john_doe">
    <input type="email" name="email" value="john@example.com">
    <input type="checkbox" name="skills" value="js" checked>
    <input type="checkbox" name="skills" value="python" checked>
    <input type="checkbox" name="skills" value="java">
    <input type="radio" name="level" value="junior">
    <input type="radio" name="level" value="senior" checked>
    <select name="country">
        <option value="us" selected>USA</option>
        <option value="uk">UK</option>
    </select>
    <textarea name="bio">Hello world</textarea>
</form>
*/

// Expected output:
// {
//     username: "john_doe",
//     email: "john@example.com",
//     skills: ["js", "python"],
//     level: "senior",
//     country: "us",
//     bio: "Hello world"
// }

/**
 * EXERCISE 2: Email Validator
 *
 * Create a function that validates an email input field
 * and returns an object with validation details.
 */

function validateEmail(inputElement) {
  // Your code here
  // Return: { valid: boolean, errors: string[] }
}

// Example usage:
// const emailInput = document.querySelector('#email');
// emailInput.value = "invalid-email";
// validateEmail(emailInput);
// → { valid: false, errors: ["Invalid email format"] }

/**
 * EXERCISE 3: Password Strength Checker
 *
 * Create a function that checks password strength and returns
 * a score from 0-5 with feedback.
 *
 * Criteria (1 point each):
 * - At least 8 characters
 * - Contains lowercase letter
 * - Contains uppercase letter
 * - Contains number
 * - Contains special character (!@#$%^&*)
 */

function checkPasswordStrength(password) {
  // Your code here
  // Return: { score: number, strength: string, feedback: string[] }
}

// Example:
// checkPasswordStrength("Pass123!")
// → { score: 5, strength: "strong", feedback: [] }
//
// checkPasswordStrength("abc")
// → { score: 1, strength: "weak", feedback: ["Add uppercase", "Add number", "Add special character", "Make it 8+ chars"] }

/**
 * EXERCISE 4: Form Validator
 *
 * Create a function that validates a complete form based on
 * validation rules object.
 */

function validateForm(form, rules) {
  // Your code here
  // Return: { valid: boolean, errors: { fieldName: string[] } }
}

// Example rules object:
const validationRules = {
  username: {
    required: true,
    minLength: 3,
    maxLength: 20,
    pattern: /^[a-zA-Z0-9_]+$/,
  },
  email: {
    required: true,
    type: 'email',
  },
  age: {
    required: false,
    type: 'number',
    min: 18,
    max: 120,
  },
  password: {
    required: true,
    minLength: 8,
  },
};

// Expected output:
// {
//     valid: false,
//     errors: {
//         username: ["Username must be at least 3 characters"],
//         password: ["Password is required"]
//     }
// }

/**
 * EXERCISE 5: Live Search Input
 *
 * Create a function that sets up a search input with debouncing.
 * It should call the callback only after the user stops typing.
 */

function setupLiveSearch(inputElement, callback, delay = 300) {
  // Your code here
  // Should call callback(searchQuery) after user stops typing
}

// Example usage:
// const searchInput = document.querySelector('#search');
// setupLiveSearch(searchInput, (query) => {
//     console.log('Searching for:', query);
// }, 500);

/**
 * EXERCISE 6: Character Counter
 *
 * Create a function that adds a live character counter
 * to a textarea element.
 */

function addCharacterCounter(textarea, maxLength) {
  // Your code here
  // Should create a counter element below the textarea
  // Update counter on input: "45 / 280 characters"
  // Add warning style when near limit (90%+)
  // Prevent typing when limit reached
}

// Example usage:
// addCharacterCounter(document.querySelector('#tweet'), 280);

/**
 * EXERCISE 7: Form Data Serializer
 *
 * Create a function that converts FormData to different formats.
 */

function serializeForm(form, format = 'object') {
  // Your code here
  // format: 'object' | 'json' | 'urlencoded' | 'formdata'
}

// Examples:
// serializeForm(form, 'object')
// → { name: "John", email: "john@example.com" }
//
// serializeForm(form, 'json')
// → '{"name":"John","email":"john@example.com"}'
//
// serializeForm(form, 'urlencoded')
// → "name=John&email=john%40example.com"
//
// serializeForm(form, 'formdata')
// → FormData object

/**
 * EXERCISE 8: Input Mask
 *
 * Create a function that applies an input mask to a text field.
 * Mask format: # = digit, A = letter, * = any character
 */

function applyInputMask(input, mask) {
  // Your code here
  // mask examples:
  // Phone: "(###) ###-####"
  // Date: "##/##/####"
  // Credit card: "#### #### #### ####"
}

// Example usage:
// applyInputMask(phoneInput, "(###) ###-####");
// User types "1234567890"
// Display shows "(123) 456-7890"

/**
 * EXERCISE 9: Auto-save Form
 *
 * Create a function that auto-saves form data to localStorage
 * and restores it on page load.
 */

function setupAutoSave(form, storageKey, interval = 5000) {
  // Your code here
  // Should:
  // - Save form data to localStorage every `interval` ms
  // - Save on any input change (debounced)
  // - Restore form data on load
  // - Clear saved data on successful submit
  // - Return methods: { save(), restore(), clear() }
}

// Example usage:
// const autosave = setupAutoSave(
//     document.querySelector('#draftForm'),
//     'draft-post',
//     3000
// );
// autosave.save();    // Manual save
// autosave.restore(); // Manual restore
// autosave.clear();   // Clear saved data

/**
 * EXERCISE 10: Multi-step Form
 *
 * Create a class that manages a multi-step form wizard.
 */

class FormWizard {
  constructor(formElement, steps) {
    // steps: array of step configurations
    // { id: string, validate: function }
    // Your code here
  }

  getCurrentStep() {
    // Return current step index
  }

  nextStep() {
    // Validate current step and move to next
    // Return: { success: boolean, errors?: string[] }
  }

  prevStep() {
    // Move to previous step
  }

  goToStep(index) {
    // Go to specific step (if allowed)
  }

  submit() {
    // Validate all steps and submit
  }
}

// Example usage:
/*
const wizard = new FormWizard(document.querySelector('#multiStepForm'), [
    {
        id: 'personal-info',
        validate: (data) => data.name && data.email
    },
    {
        id: 'address',
        validate: (data) => data.street && data.city
    },
    {
        id: 'payment',
        validate: (data) => data.cardNumber
    }
]);

wizard.nextStep(); // Validates step 1, moves to step 2
wizard.prevStep(); // Moves back to step 1
wizard.submit();   // Submits if all valid
*/

/**
 * EXERCISE 11: Dependent Dropdowns
 *
 * Create a function that sets up cascading/dependent select dropdowns.
 */

function setupDependentDropdowns(config) {
  // Your code here
  // config: { parentSelect, childSelect, options }
  // options: { parentValue: [childOptions] }
}

// Example:
// setupDependentDropdowns({
//     parentSelect: document.querySelector('#country'),
//     childSelect: document.querySelector('#city'),
//     options: {
//         'us': [
//             { value: 'nyc', label: 'New York' },
//             { value: 'la', label: 'Los Angeles' }
//         ],
//         'uk': [
//             { value: 'london', label: 'London' },
//             { value: 'manchester', label: 'Manchester' }
//         ]
//     }
// });

/**
 * EXERCISE 12: File Upload with Preview
 *
 * Create a function that sets up file upload with preview and validation.
 */

function setupFileUpload(inputElement, options = {}) {
  // options: {
  //     maxSize: number (bytes),
  //     allowedTypes: string[],
  //     maxFiles: number,
  //     previewContainer: Element,
  //     onValidationError: function,
  //     onFilesSelected: function
  // }
  // Your code here
  // Should:
  // - Validate file size and type
  // - Show image previews
  // - Show file info for non-images
  // - Allow removing selected files
}

// Example usage:
// setupFileUpload(document.querySelector('#imageUpload'), {
//     maxSize: 5 * 1024 * 1024, // 5MB
//     allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],
//     maxFiles: 5,
//     previewContainer: document.querySelector('#previews'),
//     onValidationError: (errors) => console.log(errors),
//     onFilesSelected: (files) => console.log('Selected:', files)
// });

/**
 * EXERCISE 13: Form Field Array
 *
 * Create a function that manages dynamic array fields
 * (add/remove/reorder items).
 */

function createFieldArray(containerElement, fieldTemplate, options = {}) {
  // options: {
  //     minItems: number,
  //     maxItems: number,
  //     onAdd: function,
  //     onRemove: function,
  //     sortable: boolean
  // }
  // Your code here
  // Return: { add(), remove(index), getValues(), setValues(array) }
}

// Example:
// const phones = createFieldArray(
//     document.querySelector('#phoneNumbers'),
//     '<input type="tel" name="phones[]" placeholder="Phone number">',
//     { minItems: 1, maxItems: 5 }
// );
//
// phones.add();          // Add new phone field
// phones.remove(1);      // Remove second phone field
// phones.getValues();    // Get all phone values as array
// phones.setValues(['+1234567890', '+0987654321']); // Set values

/**
 * EXERCISE 14: Credit Card Form
 *
 * Create a credit card input component with:
 * - Card number formatting (4 groups of 4)
 * - Expiry date formatting (MM/YY)
 * - CVV input (3-4 digits)
 * - Card type detection (Visa, Mastercard, Amex)
 * - Luhn algorithm validation
 */

class CreditCardForm {
  constructor(formElement) {
    // Your code here
  }

  detectCardType(number) {
    // Return: 'visa' | 'mastercard' | 'amex' | 'discover' | 'unknown'
  }

  validateCardNumber(number) {
    // Use Luhn algorithm
    // Return: boolean
  }

  validateExpiry(expiry) {
    // Check if date is in future
    // Return: boolean
  }

  formatCardNumber(number) {
    // Add spaces every 4 digits
    // Return: formatted string
  }

  getCardData() {
    // Return: { number, expiry, cvv, type, valid }
  }
}

/**
 * EXERCISE 15: Form Submission Handler
 *
 * Create a robust form submission handler with:
 * - Loading state
 * - Error handling
 * - Retry logic
 * - Success/error callbacks
 */

function createFormSubmitHandler(form, options) {
  // options: {
  //     url: string,
  //     method: 'POST' | 'PUT',
  //     headers: object,
  //     transformData: function,
  //     validate: function,
  //     onSubmitStart: function,
  //     onSuccess: function,
  //     onError: function,
  //     onComplete: function,
  //     retries: number,
  //     retryDelay: number
  // }
  // Your code here
  // Return: { submit(), abort() }
}

// Example usage:
/*
const handler = createFormSubmitHandler(
    document.querySelector('#contactForm'),
    {
        url: '/api/contact',
        method: 'POST',
        validate: (data) => {
            if (!data.email) return { valid: false, error: 'Email required' };
            return { valid: true };
        },
        transformData: (formData) => JSON.stringify(Object.fromEntries(formData)),
        headers: { 'Content-Type': 'application/json' },
        onSubmitStart: () => showSpinner(),
        onSuccess: (response) => showMessage('Sent!'),
        onError: (error) => showError(error.message),
        onComplete: () => hideSpinner(),
        retries: 3,
        retryDelay: 1000
    }
);

// handler.submit(); // Submit form
// handler.abort();  // Cancel ongoing submission
*/

// ============================================
// SOLUTIONS (Hidden - Scroll to reveal)
// ============================================

/*
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 * SOLUTIONS BELOW
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

// SOLUTION 1: Form Value Reader
function getFormValuesSolution(form) {
  const result = {};
  const formData = new FormData(form);

  // Group values by name
  const valueGroups = {};
  for (const [name, value] of formData.entries()) {
    if (!valueGroups[name]) {
      valueGroups[name] = [];
    }
    valueGroups[name].push(value);
  }

  // Process each field
  for (const [name, values] of Object.entries(valueGroups)) {
    const element = form.elements[name];

    // Check if it's a multi-value field (checkboxes or multi-select)
    if (element instanceof RadioNodeList && element[0]?.type === 'checkbox') {
      result[name] = values;
    } else if (element instanceof HTMLSelectElement && element.multiple) {
      result[name] = values;
    } else {
      result[name] = values.length === 1 ? values[0] : values;
    }
  }

  return result;
}

// SOLUTION 2: Email Validator
function validateEmailSolution(inputElement) {
  const errors = [];
  const value = inputElement.value.trim();

  if (inputElement.required && !value) {
    errors.push('Email is required');
  }

  if (value && inputElement.validity.typeMismatch) {
    errors.push('Invalid email format');
  }

  // Additional custom validation
  if (value) {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(value)) {
      if (!errors.includes('Invalid email format')) {
        errors.push('Invalid email format');
      }
    }

    // Check for common issues
    if (value.includes('..')) {
      errors.push('Email cannot contain consecutive dots');
    }
  }

  return {
    valid: errors.length === 0,
    errors,
  };
}

// SOLUTION 3: Password Strength Checker
function checkPasswordStrengthSolution(password) {
  const feedback = [];
  let score = 0;

  // Check length
  if (password.length >= 8) {
    score++;
  } else {
    feedback.push('Make it 8+ characters');
  }

  // Check lowercase
  if (/[a-z]/.test(password)) {
    score++;
  } else {
    feedback.push('Add lowercase letter');
  }

  // Check uppercase
  if (/[A-Z]/.test(password)) {
    score++;
  } else {
    feedback.push('Add uppercase letter');
  }

  // Check number
  if (/[0-9]/.test(password)) {
    score++;
  } else {
    feedback.push('Add number');
  }

  // Check special character
  if (/[!@#$%^&*]/.test(password)) {
    score++;
  } else {
    feedback.push('Add special character (!@#$%^&*)');
  }

  // Determine strength label
  let strength;
  if (score <= 1) strength = 'very weak';
  else if (score === 2) strength = 'weak';
  else if (score === 3) strength = 'fair';
  else if (score === 4) strength = 'strong';
  else strength = 'very strong';

  return { score, strength, feedback };
}

// SOLUTION 4: Form Validator
function validateFormSolution(form, rules) {
  const errors = {};
  let valid = true;

  for (const [fieldName, fieldRules] of Object.entries(rules)) {
    const element = form.elements[fieldName];
    const value = element?.value?.trim() ?? '';
    const fieldErrors = [];

    // Required check
    if (fieldRules.required && !value) {
      fieldErrors.push(`${fieldName} is required`);
    }

    if (value) {
      // Min length
      if (fieldRules.minLength && value.length < fieldRules.minLength) {
        fieldErrors.push(
          `${fieldName} must be at least ${fieldRules.minLength} characters`
        );
      }

      // Max length
      if (fieldRules.maxLength && value.length > fieldRules.maxLength) {
        fieldErrors.push(
          `${fieldName} must be at most ${fieldRules.maxLength} characters`
        );
      }

      // Pattern
      if (fieldRules.pattern && !fieldRules.pattern.test(value)) {
        fieldErrors.push(`${fieldName} format is invalid`);
      }

      // Type validation
      if (fieldRules.type === 'email') {
        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        if (!emailRegex.test(value)) {
          fieldErrors.push(`${fieldName} must be a valid email`);
        }
      }

      if (fieldRules.type === 'number') {
        const num = Number(value);
        if (isNaN(num)) {
          fieldErrors.push(`${fieldName} must be a number`);
        } else {
          if (fieldRules.min !== undefined && num < fieldRules.min) {
            fieldErrors.push(`${fieldName} must be at least ${fieldRules.min}`);
          }
          if (fieldRules.max !== undefined && num > fieldRules.max) {
            fieldErrors.push(`${fieldName} must be at most ${fieldRules.max}`);
          }
        }
      }
    }

    if (fieldErrors.length > 0) {
      errors[fieldName] = fieldErrors;
      valid = false;
    }
  }

  return { valid, errors };
}

// SOLUTION 5: Live Search Input
function setupLiveSearchSolution(inputElement, callback, delay = 300) {
  let timeoutId = null;

  inputElement.addEventListener('input', function (e) {
    const query = e.target.value.trim();

    // Clear previous timeout
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    // Set new timeout
    timeoutId = setTimeout(() => {
      callback(query);
    }, delay);
  });

  // Return cleanup function
  return function cleanup() {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
  };
}

// SOLUTION 6: Character Counter
function addCharacterCounterSolution(textarea, maxLength) {
  // Create counter element
  const counter = document.createElement('div');
  counter.className = 'char-counter';
  counter.style.cssText = 'font-size: 12px; color: #666; margin-top: 4px;';
  textarea.parentNode.insertBefore(counter, textarea.nextSibling);

  function updateCounter() {
    const currentLength = textarea.value.length;
    const remaining = maxLength - currentLength;

    counter.textContent = `${currentLength} / ${maxLength} characters`;

    // Warning when near limit (90%+)
    if (currentLength >= maxLength * 0.9) {
      counter.style.color = '#e74c3c';
      counter.style.fontWeight = 'bold';
    } else {
      counter.style.color = '#666';
      counter.style.fontWeight = 'normal';
    }
  }

  // Prevent exceeding limit
  textarea.addEventListener('input', function () {
    if (this.value.length > maxLength) {
      this.value = this.value.substring(0, maxLength);
    }
    updateCounter();
  });

  // Also handle paste
  textarea.addEventListener('paste', function () {
    setTimeout(() => {
      if (this.value.length > maxLength) {
        this.value = this.value.substring(0, maxLength);
      }
      updateCounter();
    }, 0);
  });

  // Initial update
  updateCounter();

  return counter;
}

// SOLUTION 7: Form Data Serializer
function serializeFormSolution(form, format = 'object') {
  const formData = new FormData(form);

  switch (format) {
    case 'formdata':
      return formData;

    case 'object': {
      const obj = {};
      for (const [key, value] of formData.entries()) {
        if (obj[key]) {
          // Handle multiple values for same key
          if (Array.isArray(obj[key])) {
            obj[key].push(value);
          } else {
            obj[key] = [obj[key], value];
          }
        } else {
          obj[key] = value;
        }
      }
      return obj;
    }

    case 'json':
      return JSON.stringify(serializeFormSolution(form, 'object'));

    case 'urlencoded': {
      const params = new URLSearchParams();
      for (const [key, value] of formData.entries()) {
        params.append(key, value);
      }
      return params.toString();
    }

    default:
      throw new Error(`Unknown format: ${format}`);
  }
}

// SOLUTION 8: Input Mask
function applyInputMaskSolution(input, mask) {
  function formatValue(value) {
    // Remove all non-alphanumeric
    const clean = value.replace(/[^a-zA-Z0-9]/g, '');
    let result = '';
    let valueIndex = 0;

    for (let i = 0; i < mask.length && valueIndex < clean.length; i++) {
      const maskChar = mask[i];

      if (maskChar === '#') {
        // Digit only
        if (/\d/.test(clean[valueIndex])) {
          result += clean[valueIndex];
          valueIndex++;
        } else {
          valueIndex++;
          i--; // Try again with next char
        }
      } else if (maskChar === 'A') {
        // Letter only
        if (/[a-zA-Z]/.test(clean[valueIndex])) {
          result += clean[valueIndex];
          valueIndex++;
        } else {
          valueIndex++;
          i--;
        }
      } else if (maskChar === '*') {
        // Any character
        result += clean[valueIndex];
        valueIndex++;
      } else {
        // Literal character from mask
        result += maskChar;
      }
    }

    return result;
  }

  input.addEventListener('input', function (e) {
    const cursorPos = this.selectionStart;
    const oldLength = this.value.length;

    this.value = formatValue(this.value);

    const newLength = this.value.length;
    const newCursorPos = cursorPos + (newLength - oldLength);

    this.setSelectionRange(newCursorPos, newCursorPos);
  });

  input.addEventListener('keydown', function (e) {
    // Allow navigation and editing keys
    const allowedKeys = [
      'Backspace',
      'Delete',
      'ArrowLeft',
      'ArrowRight',
      'Tab',
    ];
    if (allowedKeys.includes(e.key)) {
      return;
    }

    // Prevent if max length reached
    const clean = this.value.replace(/[^a-zA-Z0-9]/g, '');
    const maxLength = mask.replace(/[^#A*]/g, '').length;

    if (
      clean.length >= maxLength &&
      this.selectionStart === this.selectionEnd
    ) {
      e.preventDefault();
    }
  });
}

// SOLUTION 9: Auto-save Form
function setupAutoSaveSolution(form, storageKey, interval = 5000) {
  let intervalId = null;
  let debounceId = null;

  function save() {
    const formData = new FormData(form);
    const data = {};

    for (const [key, value] of formData.entries()) {
      if (data[key]) {
        if (Array.isArray(data[key])) {
          data[key].push(value);
        } else {
          data[key] = [data[key], value];
        }
      } else {
        data[key] = value;
      }
    }

    localStorage.setItem(storageKey, JSON.stringify(data));
    console.log('Form auto-saved');
  }

  function restore() {
    const saved = localStorage.getItem(storageKey);
    if (!saved) return false;

    try {
      const data = JSON.parse(saved);

      for (const [name, value] of Object.entries(data)) {
        const elements = form.elements[name];

        if (!elements) continue;

        if (elements instanceof RadioNodeList) {
          // Handle radio/checkbox groups
          for (const element of elements) {
            if (element.type === 'checkbox') {
              element.checked = Array.isArray(value)
                ? value.includes(element.value)
                : value === element.value;
            } else if (element.type === 'radio') {
              element.checked = element.value === value;
            }
          }
        } else if (elements.type === 'checkbox') {
          elements.checked = Boolean(value);
        } else {
          elements.value = value;
        }
      }

      console.log('Form restored from auto-save');
      return true;
    } catch (e) {
      console.error('Failed to restore form:', e);
      return false;
    }
  }

  function clear() {
    localStorage.removeItem(storageKey);
    console.log('Auto-save cleared');
  }

  // Set up interval saving
  intervalId = setInterval(save, interval);

  // Set up debounced input saving
  form.addEventListener('input', function () {
    if (debounceId) clearTimeout(debounceId);
    debounceId = setTimeout(save, 1000);
  });

  // Clear on successful submit
  form.addEventListener('submit', function () {
    clear();
    if (intervalId) clearInterval(intervalId);
  });

  // Restore on load
  restore();

  return { save, restore, clear };
}

// SOLUTION 10: Multi-step Form Wizard
class FormWizardSolution {
  constructor(formElement, steps) {
    this.form = formElement;
    this.steps = steps;
    this.currentStepIndex = 0;

    this._setupSteps();
    this._showStep(0);
  }

  _setupSteps() {
    // Hide all steps except first
    this.steps.forEach((step, index) => {
      const stepElement = this.form.querySelector(`#${step.id}`);
      if (stepElement) {
        stepElement.style.display = index === 0 ? 'block' : 'none';
      }
    });
  }

  _showStep(index) {
    this.steps.forEach((step, i) => {
      const stepElement = this.form.querySelector(`#${step.id}`);
      if (stepElement) {
        stepElement.style.display = i === index ? 'block' : 'none';
      }
    });
  }

  _getStepData(stepIndex) {
    const step = this.steps[stepIndex];
    const stepElement = this.form.querySelector(`#${step.id}`);
    if (!stepElement) return {};

    const inputs = stepElement.querySelectorAll('input, select, textarea');
    const data = {};

    inputs.forEach((input) => {
      if (input.name) {
        if (input.type === 'checkbox') {
          data[input.name] = input.checked;
        } else if (input.type === 'radio') {
          if (input.checked) data[input.name] = input.value;
        } else {
          data[input.name] = input.value;
        }
      }
    });

    return data;
  }

  getCurrentStep() {
    return this.currentStepIndex;
  }

  nextStep() {
    const currentStep = this.steps[this.currentStepIndex];
    const data = this._getStepData(this.currentStepIndex);

    // Validate current step
    if (currentStep.validate && !currentStep.validate(data)) {
      return { success: false, errors: ['Validation failed'] };
    }

    if (this.currentStepIndex < this.steps.length - 1) {
      this.currentStepIndex++;
      this._showStep(this.currentStepIndex);
      return { success: true };
    }

    return { success: false, errors: ['Already at last step'] };
  }

  prevStep() {
    if (this.currentStepIndex > 0) {
      this.currentStepIndex--;
      this._showStep(this.currentStepIndex);
      return true;
    }
    return false;
  }

  goToStep(index) {
    // Can only go to completed or current steps
    if (
      index <= this.currentStepIndex &&
      index >= 0 &&
      index < this.steps.length
    ) {
      this.currentStepIndex = index;
      this._showStep(index);
      return true;
    }
    return false;
  }

  submit() {
    // Validate all steps
    for (let i = 0; i <= this.currentStepIndex; i++) {
      const step = this.steps[i];
      const data = this._getStepData(i);

      if (step.validate && !step.validate(data)) {
        return {
          success: false,
          errors: [`Step ${i + 1} validation failed`],
          failedStep: i,
        };
      }
    }

    // Collect all data
    const allData = {};
    this.steps.forEach((_, index) => {
      Object.assign(allData, this._getStepData(index));
    });

    return { success: true, data: allData };
  }
}

// SOLUTION 11: Dependent Dropdowns
function setupDependentDropdownsSolution(config) {
  const { parentSelect, childSelect, options } = config;

  function updateChildOptions() {
    const parentValue = parentSelect.value;
    const childOptions = options[parentValue] || [];

    // Clear existing options
    childSelect.innerHTML = '<option value="">Select...</option>';

    // Add new options
    childOptions.forEach((opt) => {
      const option = document.createElement('option');
      option.value = opt.value;
      option.textContent = opt.label;
      childSelect.appendChild(option);
    });

    // Enable/disable based on options
    childSelect.disabled = childOptions.length === 0;
  }

  parentSelect.addEventListener('change', updateChildOptions);

  // Initialize
  updateChildOptions();

  return { update: updateChildOptions };
}

// SOLUTION 12: File Upload with Preview
function setupFileUploadSolution(inputElement, options = {}) {
  const {
    maxSize = Infinity,
    allowedTypes = [],
    maxFiles = Infinity,
    previewContainer = null,
    onValidationError = () => {},
    onFilesSelected = () => {},
  } = options;

  let selectedFiles = [];

  function validateFile(file) {
    const errors = [];

    if (file.size > maxSize) {
      errors.push(
        `${file.name} exceeds max size (${(maxSize / 1024 / 1024).toFixed(
          1
        )}MB)`
      );
    }

    if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
      errors.push(`${file.name} has invalid type (${file.type})`);
    }

    return errors;
  }

  function createPreview(file, index) {
    const preview = document.createElement('div');
    preview.className = 'file-preview';
    preview.style.cssText =
      'display: inline-block; margin: 10px; position: relative;';

    if (file.type.startsWith('image/')) {
      const img = document.createElement('img');
      img.style.cssText = 'max-width: 150px; max-height: 150px;';

      const reader = new FileReader();
      reader.onload = (e) => {
        img.src = e.target.result;
      };
      reader.readAsDataURL(file);

      preview.appendChild(img);
    } else {
      const info = document.createElement('div');
      info.style.cssText =
        'padding: 20px; background: #f0f0f0; border-radius: 4px;';
      info.textContent = `📄 ${file.name}`;
      preview.appendChild(info);
    }

    // Remove button
    const removeBtn = document.createElement('button');
    removeBtn.textContent = '×';
    removeBtn.style.cssText =
      'position: absolute; top: -5px; right: -5px; ' +
      'background: red; color: white; border: none; border-radius: 50%; ' +
      'width: 20px; height: 20px; cursor: pointer;';
    removeBtn.onclick = () => {
      selectedFiles.splice(index, 1);
      updatePreviews();
    };
    preview.appendChild(removeBtn);

    return preview;
  }

  function updatePreviews() {
    if (!previewContainer) return;

    previewContainer.innerHTML = '';
    selectedFiles.forEach((file, index) => {
      previewContainer.appendChild(createPreview(file, index));
    });
  }

  inputElement.addEventListener('change', function () {
    const files = Array.from(this.files);
    const errors = [];

    // Check max files
    if (files.length > maxFiles) {
      errors.push(`Maximum ${maxFiles} files allowed`);
    }

    // Validate each file
    const validFiles = [];
    files.forEach((file) => {
      const fileErrors = validateFile(file);
      if (fileErrors.length > 0) {
        errors.push(...fileErrors);
      } else {
        validFiles.push(file);
      }
    });

    if (errors.length > 0) {
      onValidationError(errors);
    }

    selectedFiles = validFiles.slice(0, maxFiles);
    updatePreviews();
    onFilesSelected(selectedFiles);
  });

  return {
    getFiles: () => selectedFiles,
    clear: () => {
      selectedFiles = [];
      inputElement.value = '';
      updatePreviews();
    },
  };
}

// SOLUTION 13: Form Field Array
function createFieldArraySolution(
  containerElement,
  fieldTemplate,
  options = {}
) {
  const {
    minItems = 0,
    maxItems = Infinity,
    onAdd = () => {},
    onRemove = () => {},
    sortable = false,
  } = options;

  let itemCount = 0;

  function createItem() {
    const wrapper = document.createElement('div');
    wrapper.className = 'field-array-item';
    wrapper.dataset.index = itemCount++;
    wrapper.innerHTML = fieldTemplate;

    // Add remove button
    const removeBtn = document.createElement('button');
    removeBtn.type = 'button';
    removeBtn.textContent = 'Remove';
    removeBtn.className = 'remove-item';
    wrapper.appendChild(removeBtn);

    return wrapper;
  }

  function add() {
    const items = containerElement.querySelectorAll('.field-array-item');
    if (items.length >= maxItems) {
      console.log(`Maximum ${maxItems} items allowed`);
      return false;
    }

    const newItem = createItem();
    containerElement.appendChild(newItem);
    onAdd(newItem);
    return true;
  }

  function remove(index) {
    const items = containerElement.querySelectorAll('.field-array-item');
    if (items.length <= minItems) {
      console.log(`Minimum ${minItems} items required`);
      return false;
    }

    const item = items[index];
    if (item) {
      onRemove(item);
      item.remove();
      return true;
    }
    return false;
  }

  function getValues() {
    const items = containerElement.querySelectorAll('.field-array-item');
    return Array.from(items).map((item) => {
      const input = item.querySelector('input, select, textarea');
      return input ? input.value : null;
    });
  }

  function setValues(values) {
    containerElement.innerHTML = '';
    itemCount = 0;

    values.forEach((value) => {
      const item = createItem();
      const input = item.querySelector('input, select, textarea');
      if (input) input.value = value;
      containerElement.appendChild(item);
    });
  }

  // Event delegation for remove buttons
  containerElement.addEventListener('click', function (e) {
    if (e.target.classList.contains('remove-item')) {
      const item = e.target.closest('.field-array-item');
      const items = Array.from(
        containerElement.querySelectorAll('.field-array-item')
      );
      const index = items.indexOf(item);
      remove(index);
    }
  });

  // Initialize with minimum items
  for (let i = 0; i < minItems; i++) {
    add();
  }

  return { add, remove, getValues, setValues };
}

// SOLUTION 14: Credit Card Form (partial)
class CreditCardFormSolution {
  constructor(formElement) {
    this.form = formElement;
    this.cardNumberInput = formElement.querySelector('[name="cardNumber"]');
    this.expiryInput = formElement.querySelector('[name="expiry"]');
    this.cvvInput = formElement.querySelector('[name="cvv"]');

    this._setupEventListeners();
  }

  _setupEventListeners() {
    if (this.cardNumberInput) {
      this.cardNumberInput.addEventListener('input', (e) => {
        e.target.value = this.formatCardNumber(e.target.value);
      });
    }

    if (this.expiryInput) {
      this.expiryInput.addEventListener('input', (e) => {
        let value = e.target.value.replace(/\D/g, '');
        if (value.length >= 2) {
          value = value.slice(0, 2) + '/' + value.slice(2, 4);
        }
        e.target.value = value;
      });
    }

    if (this.cvvInput) {
      this.cvvInput.addEventListener('input', (e) => {
        e.target.value = e.target.value.replace(/\D/g, '').slice(0, 4);
      });
    }
  }

  detectCardType(number) {
    const clean = number.replace(/\s/g, '');

    if (/^4/.test(clean)) return 'visa';
    if (/^5[1-5]/.test(clean)) return 'mastercard';
    if (/^3[47]/.test(clean)) return 'amex';
    if (/^6011/.test(clean)) return 'discover';

    return 'unknown';
  }

  validateCardNumber(number) {
    const clean = number.replace(/\s/g, '');
    if (!/^\d+$/.test(clean)) return false;

    // Luhn algorithm
    let sum = 0;
    let isEven = false;

    for (let i = clean.length - 1; i >= 0; i--) {
      let digit = parseInt(clean[i], 10);

      if (isEven) {
        digit *= 2;
        if (digit > 9) digit -= 9;
      }

      sum += digit;
      isEven = !isEven;
    }

    return sum % 10 === 0;
  }

  validateExpiry(expiry) {
    const match = expiry.match(/^(\d{2})\/(\d{2})$/);
    if (!match) return false;

    const month = parseInt(match[1], 10);
    const year = parseInt('20' + match[2], 10);

    if (month < 1 || month > 12) return false;

    const now = new Date();
    const currentMonth = now.getMonth() + 1;
    const currentYear = now.getFullYear();

    if (year < currentYear) return false;
    if (year === currentYear && month < currentMonth) return false;

    return true;
  }

  formatCardNumber(number) {
    const clean = number.replace(/\D/g, '');
    const groups = clean.match(/.{1,4}/g) || [];
    return groups.join(' ').slice(0, 19);
  }

  getCardData() {
    const number = this.cardNumberInput?.value || '';
    const expiry = this.expiryInput?.value || '';
    const cvv = this.cvvInput?.value || '';

    return {
      number: number.replace(/\s/g, ''),
      expiry,
      cvv,
      type: this.detectCardType(number),
      valid:
        this.validateCardNumber(number) &&
        this.validateExpiry(expiry) &&
        cvv.length >= 3,
    };
  }
}

// SOLUTION 15: Form Submission Handler
function createFormSubmitHandlerSolution(form, options) {
  const {
    url,
    method = 'POST',
    headers = {},
    transformData = (data) => data,
    validate = () => ({ valid: true }),
    onSubmitStart = () => {},
    onSuccess = () => {},
    onError = () => {},
    onComplete = () => {},
    retries = 0,
    retryDelay = 1000,
  } = options;

  let abortController = null;

  async function submit() {
    const formData = new FormData(form);

    // Validate
    const validation = validate(Object.fromEntries(formData));
    if (!validation.valid) {
      onError(new Error(validation.error || 'Validation failed'));
      return;
    }

    onSubmitStart();
    abortController = new AbortController();

    let lastError;

    for (let attempt = 0; attempt <= retries; attempt++) {
      try {
        const response = await fetch(url, {
          method,
          headers,
          body: transformData(formData),
          signal: abortController.signal,
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const result = await response.json();
        onSuccess(result);
        onComplete();
        return result;
      } catch (error) {
        lastError = error;

        if (error.name === 'AbortError') {
          onError(new Error('Request aborted'));
          onComplete();
          return;
        }

        if (attempt < retries) {
          console.log(`Retry ${attempt + 1}/${retries} in ${retryDelay}ms...`);
          await new Promise((resolve) => setTimeout(resolve, retryDelay));
        }
      }
    }

    onError(lastError);
    onComplete();
  }

  function abort() {
    if (abortController) {
      abortController.abort();
      abortController = null;
    }
  }

  // Attach to form submit
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    submit();
  });

  return { submit, abort };
}

console.log('Forms and User Input exercises loaded!');
console.log(
  'Complete each exercise and check against solutions at the bottom.'
);
Exercises - JavaScript Tutorial | DeepML