/** @jsx m */
import { m, mount, redraw } from "umai";
// Utility function to debounce inputs
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function sleep(ms) {
return new Promise((res) => {
setTimeout(res, ms);
});
}
async function getRandomUsers() {
// Simulate network delay
await sleep(State.delay);
// Fetch random users from the Random User API
const response = await fetch("https://randomuser.me/api/?results=5");
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
return data.results.map((user, index) => ({
id: index + 1,
name: `${user.name.first} ${user.name.last}`,
email: user.email,
picture: user.picture.medium,
}));
}
let State = {
users: [],
delay: 200,
loading: false,
error: null,
};
function saveUsers(users) {
State.users = users;
}
function clearUsers() {
State.users = [];
redraw();
}
async function fetchUsers(node) {
State.loading = true;
State.error = null;
redraw();
clearUsers();
try {
let users = await getRandomUsers();
saveUsers(users);
} catch (error) {
console.error("Failed to fetch users:", error);
State.error = "Failed to fetch users. Please try again.";
} finally {
State.loading = false;
redraw();
}
}
// User item component for clarity and reuse
const UserItem = ({ user }) => (
<li class="user-item">
<img src={user.picture} alt={user.name} class="user-avatar" />
<div class="user-details">
<p><strong>{user.name}</strong></p>
<p>{user.email}</p>
</div>
</li>
);
const Users = () => {
return (
<div>
{State.loading && <Loader />}
{State.error && <p class="error-message">{State.error}</p>}
{!State.loading && State.users.length === 0 && (
<p class="no-users-message">No users available. Click "Load Users" to fetch.</p>
)}
<ul aria-live="polite" class="user-list">
{State.users.map((user) => (
<UserItem user={user} key={user.id} />
))}
</ul>
</div>
);
};
const Timer = () => {
const handleInput = debounce((e) => {
State.delay = parseInt(e.target.value, 10) || 0;
redraw();
}, 300);
return (
<div class="timer">
<label for="delay-input">Delay fetch for: </label>
<input
type="number"
id="delay-input"
value={State.delay}
oninput={handleInput}
aria-label="Fetch delay in milliseconds"
/>
<span> ms</span>
</div>
);
};
const Loader = () => {
return (
<div class="loader">
Loading...
</div>
);
};
function setup(node) {
let btnLoader = node.querySelector("button");
btnLoader.addEventListener("click", () => fetchUsers(node));
}
const App = () => (
<div dom={setup}>
<Timer />
<button class="load-button">Load Users</button>
<Users />
</div>
);
export { App };
mount(document.body, App)
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.timer {
margin-bottom: 20px;
}
label {
margin-right: 10px;
}
input {
width: 80px;
padding: 5px;
font-size: 14px;
}
.load-button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
.load-button:hover {
background-color: #0056b3;
}
.loader {
text-align: center;
font-weight: bold;
margin-bottom: 20px;
}
.error-message {
color: red;
text-align: center;
margin-bottom: 10px;
}
.no-users-message {
text-align: center;
margin-bottom: 20px;
}
.user-list {
list-style: none;
padding: 0;
}
.user-item {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.user-avatar {
border-radius: 50%;
margin-right: 15px;
}
.user-details p {
margin: 0;
}
You can test it here :
Top comments (0)