Finished - tailwind, code split, elements in HTML

This commit is contained in:
NetMan 2024-02-23 13:56:10 +01:00
parent 9778f1aa1e
commit 27117cd641
11 changed files with 3059 additions and 131 deletions

2850
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -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);
}
}

25
src/country.js Normal file
View File

@ -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;

25
src/domelements.js Normal file
View File

@ -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,
};

View File

@ -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>

View File

@ -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());
}),
);
});

63
src/password.js Normal file
View File

@ -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 };

8
tailwind.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{html,js,ts,jsx,tsx,vue,css}"],
theme: {
extend: {},
},
plugins: [],
};

View File

@ -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,