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