generated from tod/odin-webpack-template
			Finished - tailwind, code split, elements in HTML
This commit is contained in:
		
							parent
							
								
									9778f1aa1e
								
							
						
					
					
						commit
						27117cd641
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -19,6 +19,9 @@
 | 
			
		|||
    "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
    "html-webpack-plugin": "^5.6.0",
 | 
			
		||||
    "image-minimizer-webpack-plugin": "^4.0.0",
 | 
			
		||||
    "postcss": "^8.4.35",
 | 
			
		||||
    "postcss-loader": "^8.1.0",
 | 
			
		||||
    "postcss-preset-env": "^9.4.0",
 | 
			
		||||
    "prettier": "3.2.5",
 | 
			
		||||
    "style-loader": "^3.3.4",
 | 
			
		||||
    "webpack": "^5.90.1",
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +31,9 @@
 | 
			
		|||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "countries-list": "^3.0.6",
 | 
			
		||||
    "normalize.css": "^8.0.1"
 | 
			
		||||
    "imask": "^7.4.0",
 | 
			
		||||
    "js-confetti": "^0.12.0",
 | 
			
		||||
    "normalize.css": "^8.0.1",
 | 
			
		||||
    "tailwindcss": "^3.4.1"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
module.exports = {
 | 
			
		||||
  plugins: {
 | 
			
		||||
    tailwindcss: {},
 | 
			
		||||
    autoprefixer: {},
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +1,47 @@
 | 
			
		|||
input {
 | 
			
		||||
  display: block;
 | 
			
		||||
}
 | 
			
		||||
@tailwind base;
 | 
			
		||||
@tailwind components;
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
 | 
			
		||||
html, body {
 | 
			
		||||
  font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer components {
 | 
			
		||||
  #form {
 | 
			
		||||
    @apply flex flex-col gap-2 mx-auto;
 | 
			
		||||
    width: min(98vw, 300px);
 | 
			
		||||
  }
 | 
			
		||||
  input {
 | 
			
		||||
    @apply block p-2 rounded-lg border border-slate-600 bg-slate-50 transition-all;
 | 
			
		||||
  }
 | 
			
		||||
  input:focus {
 | 
			
		||||
    transform: scale(1.015);
 | 
			
		||||
  }
 | 
			
		||||
  input:valid,
 | 
			
		||||
  input[input-valid=true] {
 | 
			
		||||
    @apply border-green-500;
 | 
			
		||||
  }
 | 
			
		||||
  input[unfocused]:invalid,
 | 
			
		||||
  input[input-valid=false] {
 | 
			
		||||
    @apply border-red-500;
 | 
			
		||||
  }
 | 
			
		||||
  #password-check {
 | 
			
		||||
    @apply w-fit p-1.5 rounded-lg mx-auto *:p-0.5 *:bg-red-500 *:text-slate-50 space-y-0.5;
 | 
			
		||||
    background: conic-gradient(black, #888, black, #888, black);
 | 
			
		||||
  }
 | 
			
		||||
  #password-check > *:first-child {
 | 
			
		||||
    @apply rounded-t-lg;
 | 
			
		||||
  }
 | 
			
		||||
  #password-check > *:last-child {
 | 
			
		||||
    @apply rounded-b-lg;
 | 
			
		||||
  }
 | 
			
		||||
  button#submit {
 | 
			
		||||
    @apply cursor-pointer p-1.5 rounded-lg border border-black bg-slate-300 transition-all;
 | 
			
		||||
  }
 | 
			
		||||
  button#submit:hover {
 | 
			
		||||
    transform: scale(1.02);
 | 
			
		||||
  }
 | 
			
		||||
  button#submit:active {
 | 
			
		||||
    transform: scale(0.98);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
import { countries } from "countries-list";
 | 
			
		||||
import { countriesList } from "./domelements";
 | 
			
		||||
 | 
			
		||||
const countriesSorted = [];
 | 
			
		||||
Object.values(countries).forEach((country) => {
 | 
			
		||||
  countriesSorted.push(country.name);
 | 
			
		||||
});
 | 
			
		||||
countriesSorted.sort();
 | 
			
		||||
let countryPattern = "";
 | 
			
		||||
 | 
			
		||||
countriesSorted.forEach((country) => {
 | 
			
		||||
  const optionInList = document.createElement("option");
 | 
			
		||||
  optionInList.textContent = country;
 | 
			
		||||
  countriesList.appendChild(optionInList);
 | 
			
		||||
  [...country].forEach((letter) => {
 | 
			
		||||
    countryPattern += `[${letter.toLowerCase()}${letter.toUpperCase()}]`;
 | 
			
		||||
  });
 | 
			
		||||
  countryPattern += "|";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
countryPattern = countryPattern.substring(0, countryPattern.length - 1); // remove "|" at the end to avoid empty match
 | 
			
		||||
 | 
			
		||||
const COUNTRY_REGEX = new RegExp(countryPattern);
 | 
			
		||||
 | 
			
		||||
export default COUNTRY_REGEX;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,25 @@
 | 
			
		|||
const allInputs = document.querySelectorAll("input");
 | 
			
		||||
const [
 | 
			
		||||
  emailInput,
 | 
			
		||||
  countryInput,
 | 
			
		||||
  zipInput,
 | 
			
		||||
  passwordInput,
 | 
			
		||||
  passwordConfirmInput,
 | 
			
		||||
] = allInputs;
 | 
			
		||||
 | 
			
		||||
const submitBtn = document.querySelector("button#submit");
 | 
			
		||||
 | 
			
		||||
const countriesList = document.querySelector("datalist#countries-list");
 | 
			
		||||
const passwordCheck = document.querySelector("#password-check");
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  emailInput,
 | 
			
		||||
  countryInput,
 | 
			
		||||
  zipInput,
 | 
			
		||||
  passwordInput,
 | 
			
		||||
  passwordConfirmInput,
 | 
			
		||||
  allInputs,
 | 
			
		||||
  passwordCheck,
 | 
			
		||||
  countriesList,
 | 
			
		||||
  submitBtn,
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -3,9 +3,33 @@
 | 
			
		|||
<head>
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>Template</title>
 | 
			
		||||
  <title>Form</title>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div id="app"></div>
 | 
			
		||||
  <div id="app">
 | 
			
		||||
    <form id="form">
 | 
			
		||||
      <h1>JS Form validation</h1>
 | 
			
		||||
      <input name="email" id="email" type="email" required placeholder="Email (christopher@example.com)">
 | 
			
		||||
 | 
			
		||||
      <input name="country" id="country" type="text" list="countries-list" required placeholder="Country (United Kingdom)">
 | 
			
		||||
      <output id="country-info"></output>
 | 
			
		||||
      <datalist id="countries-list"></datalist>
 | 
			
		||||
 | 
			
		||||
      <input name="zip" id="zip" type="text" required placeholder="ZIP code (12-345)" pattern="\d{2}-\d{3}">
 | 
			
		||||
 | 
			
		||||
      <input name="password" id="password" type="password" required placeholder="Password (Pa$Sw0rD)">
 | 
			
		||||
      <div id="password-check">
 | 
			
		||||
        <div id="length">Minimum length of 8 characters</div>
 | 
			
		||||
        <div id="lowercase">At least one lowercase letter</div>
 | 
			
		||||
        <div id="uppercase">At least one uppercase letter</div>
 | 
			
		||||
        <div id="number">At least one number</div>
 | 
			
		||||
        <div id="special">At least one special character</div>
 | 
			
		||||
        <div id="match">Passwords match</div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <input id="password-confirm" type="password" required placeholder="Confirm password">
 | 
			
		||||
 | 
			
		||||
      <button id="submit" type="submit">Submit</button>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										125
									
								
								src/main.js
								
								
								
								
							
							
						
						
									
										125
									
								
								src/main.js
								
								
								
								
							| 
						 | 
				
			
			@ -1,59 +1,82 @@
 | 
			
		|||
import { countries } from "countries-list";
 | 
			
		||||
import IMask from "imask";
 | 
			
		||||
import "./assets/style.css";
 | 
			
		||||
import "normalize.css";
 | 
			
		||||
 | 
			
		||||
const app = document.querySelector("#app");
 | 
			
		||||
 | 
			
		||||
const form = document.createElement("form");
 | 
			
		||||
form.addEventListener("submit", (event) => {
 | 
			
		||||
  event.preventDefault();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emailInput = document.createElement("input");
 | 
			
		||||
emailInput.type = "email";
 | 
			
		||||
 | 
			
		||||
const countryInput = document.createElement("input");
 | 
			
		||||
countryInput.setAttribute("list", "countries-list");
 | 
			
		||||
 | 
			
		||||
const countriesList = document.createElement("datalist");
 | 
			
		||||
countriesList.id = "countries-list";
 | 
			
		||||
 | 
			
		||||
let pattern = "";
 | 
			
		||||
 | 
			
		||||
Object.values(countries).forEach((country) => {
 | 
			
		||||
  const optionInList = document.createElement("option");
 | 
			
		||||
  optionInList.textContent = country.name;
 | 
			
		||||
  countriesList.appendChild(optionInList);
 | 
			
		||||
  [...country.name].forEach((letter) => {
 | 
			
		||||
    pattern += `[${letter.toUpperCase() + letter.toLowerCase()}]`;
 | 
			
		||||
  });
 | 
			
		||||
  pattern += "|";
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
pattern = pattern.substring(0, pattern.length - 1); // remove "|" at the end to avoid empty match
 | 
			
		||||
 | 
			
		||||
countryInput.pattern = pattern;
 | 
			
		||||
 | 
			
		||||
const zipInput = document.createElement("input");
 | 
			
		||||
 | 
			
		||||
const passwordInput = document.createElement("input");
 | 
			
		||||
 | 
			
		||||
const passwordConfirmInput = document.createElement("input");
 | 
			
		||||
 | 
			
		||||
const submitBtn = document.createElement("button");
 | 
			
		||||
submitBtn.type = "submit";
 | 
			
		||||
submitBtn.textContent = "Submit";
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", () => {});
 | 
			
		||||
 | 
			
		||||
form.append(
 | 
			
		||||
  emailInput,
 | 
			
		||||
import JSConfetti from "js-confetti";
 | 
			
		||||
import { checkPasswordValidity, checkPasswordMatch } from "./password";
 | 
			
		||||
import {
 | 
			
		||||
  countryInput,
 | 
			
		||||
  countriesList,
 | 
			
		||||
  zipInput,
 | 
			
		||||
  passwordInput,
 | 
			
		||||
  passwordConfirmInput,
 | 
			
		||||
  allInputs,
 | 
			
		||||
  submitBtn,
 | 
			
		||||
);
 | 
			
		||||
} from "./domelements";
 | 
			
		||||
import COUNTRY_REGEX from "./country";
 | 
			
		||||
 | 
			
		||||
app.appendChild(form);
 | 
			
		||||
const jsConfetti = new JSConfetti();
 | 
			
		||||
 | 
			
		||||
function validateForm() {
 | 
			
		||||
  return (
 | 
			
		||||
    countryInput.value.search(COUNTRY_REGEX) >= 0 &&
 | 
			
		||||
    checkPasswordValidity() &&
 | 
			
		||||
    checkPasswordMatch()
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const form = document.querySelector("#form");
 | 
			
		||||
 | 
			
		||||
IMask(zipInput, { mask: "00-000" });
 | 
			
		||||
 | 
			
		||||
function unfocusInputs() {
 | 
			
		||||
  allInputs.forEach((input) => {
 | 
			
		||||
    input.setAttribute("unfocused", 1);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
  form.addEventListener("submit", (event) => {
 | 
			
		||||
    event.preventDefault();
 | 
			
		||||
    if (validateForm()) {
 | 
			
		||||
      jsConfetti.addConfetti();
 | 
			
		||||
      const formData = new FormData(form);
 | 
			
		||||
      const formDataValue = formData.values();
 | 
			
		||||
      const [email, country, zip] = Array(5).fill(formDataValue.next().value);
 | 
			
		||||
      alert(
 | 
			
		||||
        `Success!\nEmail: ${email}\nCountry: ${country}\nZIP: ${zip}\nPassword: [redacted]`,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      alert("Hold up, one or more fields are invalid! Try again");
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  countryInput.addEventListener("input", () => {
 | 
			
		||||
    countryInput.setAttribute(
 | 
			
		||||
      "input-valid",
 | 
			
		||||
      countryInput.value.search(COUNTRY_REGEX) >= 0,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  countryInput.addEventListener("change", () => {
 | 
			
		||||
    document.querySelector("#country-info").textContent =
 | 
			
		||||
      countryInput.value.search(COUNTRY_REGEX) >= 0 ? "" : "Invalid country";
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  passwordInput.addEventListener("input", checkPasswordValidity);
 | 
			
		||||
  passwordConfirmInput.addEventListener("input", checkPasswordValidity);
 | 
			
		||||
 | 
			
		||||
  allInputs.forEach((input) => {
 | 
			
		||||
    input.addEventListener("focusout", (event) => {
 | 
			
		||||
      event.target.setAttribute("unfocused", 1);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  submitBtn.addEventListener("click", unfocusInputs);
 | 
			
		||||
 | 
			
		||||
  passwordInput.addEventListener("input", () => {
 | 
			
		||||
    passwordInput.setAttribute("input-valid", checkPasswordValidity());
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  [passwordInput, passwordConfirmInput].forEach((input) =>
 | 
			
		||||
    input.addEventListener("input", () => {
 | 
			
		||||
      passwordConfirmInput.setAttribute("input-valid", checkPasswordMatch());
 | 
			
		||||
    }),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,63 @@
 | 
			
		|||
import {
 | 
			
		||||
  passwordInput,
 | 
			
		||||
  passwordConfirmInput,
 | 
			
		||||
  passwordCheck,
 | 
			
		||||
} from "./domelements";
 | 
			
		||||
 | 
			
		||||
const [
 | 
			
		||||
  passwordLength,
 | 
			
		||||
  passwordLowercase,
 | 
			
		||||
  passwordUppercase,
 | 
			
		||||
  passwordNumber,
 | 
			
		||||
  passwordSpecial,
 | 
			
		||||
  passwordMatch,
 | 
			
		||||
] = passwordCheck.children;
 | 
			
		||||
 | 
			
		||||
function checkPasswordMatch() {
 | 
			
		||||
  if (
 | 
			
		||||
    passwordInput.value !== passwordConfirmInput.value ||
 | 
			
		||||
    passwordInput.value === ""
 | 
			
		||||
  ) {
 | 
			
		||||
    passwordMatch.style.backgroundColor = "";
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  passwordMatch.style.backgroundColor = "#4b4";
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkPasswordRequirement(e, regex, element) {
 | 
			
		||||
  if (e.value.search(regex) >= 0) {
 | 
			
		||||
    element.style.backgroundColor = "#4b4";
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  element.style.backgroundColor = "";
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkPasswordValidity() {
 | 
			
		||||
  const length = passwordInput.value.length >= 8;
 | 
			
		||||
  if (length) {
 | 
			
		||||
    passwordLength.style.backgroundColor = "#4b4";
 | 
			
		||||
  } else {
 | 
			
		||||
    passwordLength.style.backgroundColor = "";
 | 
			
		||||
  }
 | 
			
		||||
  const lowercase = checkPasswordRequirement(
 | 
			
		||||
    passwordInput,
 | 
			
		||||
    /[a-z]/,
 | 
			
		||||
    passwordLowercase,
 | 
			
		||||
  );
 | 
			
		||||
  const uppercase = checkPasswordRequirement(
 | 
			
		||||
    passwordInput,
 | 
			
		||||
    /[A-Z]/,
 | 
			
		||||
    passwordUppercase,
 | 
			
		||||
  );
 | 
			
		||||
  const number = checkPasswordRequirement(passwordInput, /\d/, passwordNumber);
 | 
			
		||||
  const special = checkPasswordRequirement(
 | 
			
		||||
    passwordInput,
 | 
			
		||||
    /[#?!@$%^&*-]/,
 | 
			
		||||
    passwordSpecial,
 | 
			
		||||
  );
 | 
			
		||||
  return length && lowercase && uppercase && number && special;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { checkPasswordValidity, checkPasswordMatch };
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
/** @type {import('tailwindcss').Config} */
 | 
			
		||||
module.exports = {
 | 
			
		||||
  content: ["./src/**/*.{html,js,ts,jsx,tsx,vue,css}"],
 | 
			
		||||
  theme: {
 | 
			
		||||
    extend: {},
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [],
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -6,7 +6,7 @@ module.exports = {
 | 
			
		|||
    rules: [
 | 
			
		||||
      {
 | 
			
		||||
        test: /\.css$/i,
 | 
			
		||||
        use: ["style-loader", "css-loader"],
 | 
			
		||||
        use: ["style-loader", "css-loader", "postcss-loader"],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        test: /\.(png|gif|jpe?g|webp|svg)$/i,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue