diff --git a/projects/random/animated-typing/index.html b/projects/random/animated-typing/index.html
new file mode 100644
index 0000000..2a1af1c
--- /dev/null
+++ b/projects/random/animated-typing/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+ Typewriter Animation Effect with HTML, CSS, and JavaScript
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/random/animated-typing/readme.md b/projects/random/animated-typing/readme.md
new file mode 100644
index 0000000..f5a872d
--- /dev/null
+++ b/projects/random/animated-typing/readme.md
@@ -0,0 +1,4 @@
+Just some quick notes. This project comes from the video [Typewriter Animation Effect with HTML, CSS, and JavaScript](https://www.youtube.com/watch?v=7uDdiMCVwJk). It appealed because it was showing the sort of typing effect without using any frameworks, sdks or anything else.
+
+
+
diff --git a/projects/random/animated-typing/script.js b/projects/random/animated-typing/script.js
new file mode 100644
index 0000000..3884cea
--- /dev/null
+++ b/projects/random/animated-typing/script.js
@@ -0,0 +1,67 @@
+class Typewriter {
+ constructor(el, options) {
+ this.el = el;
+ this.words = [...this.el.dataset.typewriter.split(",")];
+
+ this.speed = options?.speed || 100;
+ this.delay = options?.delay || 1000;
+ this.repeat = options?.repeat || false;
+
+ this.initTyping();
+ }
+
+ wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
+
+ toggleTyping = () => this.el.classList.toggle("typing");
+
+ async typewrite(word) {
+ await this.wait(this.delay);
+ this.toggleTyping();
+
+ for (const letter of word.split("")) {
+ this.el.textContent += letter;
+ await this.wait(this.speed);
+ }
+
+ this.toggleTyping();
+ await this.wait(this.delay);
+
+ this.toggleTyping();
+ while (this.el.textContent.length !== 0) {
+ this.el.textContent = this.el.textContent.slice(0, -1);
+ await this.wait(this.speed);
+ }
+
+ this.toggleTyping();
+ }
+
+ async initTyping() {
+ for (const word of this.words) {
+ await this.typewrite(word);
+ }
+
+ if (this.repeat) {
+ await this.initTyping();
+ } else {
+ this.el.style.animation = "none";
+ }
+
+ // can't use forEach with await
+ // this.words.forEach((word) => {
+ // await this.typewrite(word);
+ // });
+ }
+}
+
+// const el1 = new Typewriter(document.querySelector("[data-typewriter]"), {
+// speed: 10,
+// delay: 10,
+// repeat: true,
+// });
+// console.log(el1);
+
+document.querySelectorAll("[data-typewriter]").forEach((el) => {
+ new Typewriter(el, {
+ repeat: true,
+ });
+});
diff --git a/projects/random/animated-typing/style.css b/projects/random/animated-typing/style.css
new file mode 100644
index 0000000..ed38cab
--- /dev/null
+++ b/projects/random/animated-typing/style.css
@@ -0,0 +1,44 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+main {
+ display: grid;
+ align-items: center;
+ padding: 4rem;
+ min-height: 100vh;
+ background: linear-gradient(#eff0f3, #efe0ef);
+}
+
+.container {
+ display: grid;
+ justify-items: start;
+ gap: 2rem;
+}
+
+[data-typewriter] {
+ font-family: system-UI;
+ font-weight: bold;
+ font-size: 4.5rem;
+ color: #d9376e;
+ height: 6rem;
+ border-right: 0.8rem solid transparent;
+ padding: 0.6rem;
+}
+
+[data-typewriter]:not(.typing) {
+ animation: blink-cursor 1.1s step-end infinite;
+}
+
+@keyframes blink-cursor {
+ 0%,
+ 100% {
+ border-color: transparent;
+ }
+
+ 50% {
+ border-color: #ff8e3c;
+ }
+}
diff --git a/projects/random/using-template/css/style.css b/projects/random/using-template/css/style.css
new file mode 100644
index 0000000..1dd3887
--- /dev/null
+++ b/projects/random/using-template/css/style.css
@@ -0,0 +1,35 @@
+.search-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+input {
+ font-size: 1rem;
+}
+
+.user-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
+ gap: 0.25rem;
+ margin-top: 1rem;
+}
+
+.card {
+ border: 1px solid black;
+ background-color: whitesmoke;
+ padding: 0.5rem;
+}
+
+.card > .name {
+ margin-bottom: 0.25rem;
+}
+
+.card > .email {
+ font-size: 0.8rem;
+ columns: #777;
+}
+
+.hide {
+ display: none;
+}
diff --git a/projects/random/using-template/index.html b/projects/random/using-template/index.html
new file mode 100644
index 0000000..18b9125
--- /dev/null
+++ b/projects/random/using-template/index.html
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+ Using Template with Search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
My Name
+
My email address
+
+
+
+
+
+
\ No newline at end of file
diff --git a/projects/random/using-template/js/script.js b/projects/random/using-template/js/script.js
new file mode 100644
index 0000000..a02ada3
--- /dev/null
+++ b/projects/random/using-template/js/script.js
@@ -0,0 +1,63 @@
+// * https://jsonplaceholder.typicode.com/users
+
+const userCardTemplate = document.querySelector("[data-user-template]");
+// console.log(userCardTemplate);
+
+const userCardsContainer = document.querySelector(
+ "[data-user-cards-container]"
+);
+// console.log(userCardsContainer);
+
+// Original code from video used the attribute for the query rather
+// than the ID that he had setup. So I swapped it over.
+// const searchInput = document.querySelector("[data-search]");
+const searchInput = document.querySelector("#search");
+// console.log(searchInput);
+
+let users = []; // filed from user data retrieved from server
+
+searchInput.addEventListener("input", (event) => {
+ const value = event.target.value.toLowerCase();
+ // console.log(users);
+ users.forEach((user) => {
+ // this seems wasteful to run toLowerCase each time you type a letter
+ // surely better to just store a lowercase version when saving original data
+ // const isVisible =
+ // user.name.toLowerCase().includes(value) ||
+ // user.email.toLowerCase().includes(value);
+
+ const isVisible =
+ user.lowercaseName.includes(value) ||
+ user.lowercaseEmail.includes(value);
+
+ user.element.classList.toggle("hide", !isVisible);
+ });
+});
+
+fetch("https://jsonplaceholder.typicode.com/users")
+ .then((res) => res.json())
+ .then((data) => {
+ users = data.map((user) => {
+ const card = userCardTemplate.content.cloneNode(true).children[0];
+
+ const name = card.querySelector("[data-name]");
+ const email = card.querySelector("[data-email]");
+
+ name.textContent = user.name;
+ email.textContent = user.email;
+
+ // console.log(card);
+
+ userCardsContainer.append(card);
+
+ // Below is original return
+ // return { name: user.name, email: user.email, element: card };
+ return {
+ name: user.name,
+ email: user.email,
+ lowercaseName: user.name.toLowerCase(),
+ lowercaseEmail: user.email.toLowerCase(),
+ element: card,
+ };
+ });
+ });