anyone make jotai work on a single html #3058
Answered
by
gengjiawen
gengjiawen
asked this question in
Q&A
-
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Todo App</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
transitionProperty: {
'height': 'height',
},
keyframes: {
'fade-in': {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
}
},
animation: {
'fade-in': 'fade-in 0.3s ease-in-out',
}
}
}
}
</script>
<!-- React and ReactDOM CDN -->
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/[email protected]/esm/react.mjs"></script> -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="m-0 bg-gradient-to-br from-indigo-600 to-purple-600 min-h-screen font-[Inter,system-ui,sans-serif]">
<div id="root"></div>
<script type="text/babel">
const { atom, useAtom } = window.jotai;
// Jotai atom for todos
const todosAtom = atom(JSON.parse(localStorage.getItem('todos')) || []);
// Save todos to localStorage whenever they change
const TodosProvider = ({ children }) => {
const [todos, setTodos] = useAtom(todosAtom);
React.useEffect(() => {
localStorage.setItem('todos', JSON.stringify(todos));
}, [todos]);
return children;
};
function TodoApp() {
const [todos, setTodos] = useAtom(todosAtom);
const [newTodo, setNewTodo] = React.useState('');
// Add new todo
const addTodo = () => {
if (newTodo.trim()) {
const todo = {
id: Date.now(),
text: newTodo,
completed: false,
createdAt: new Date().toISOString()
};
setTodos([...todos, todo]);
setNewTodo('');
}
};
// Toggle completion status
const toggleTodo = (id) => {
setTodos(
todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
// Delete todo
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
// Calculate stats
const completedCount = todos.filter(todo => todo.completed).length;
const pendingCount = todos.length - completedCount;
return (
<div className="flex justify-center items-center min-h-screen p-4 md:p-6">
<div className="w-full max-w-2xl bg-white rounded-2xl shadow-2xl overflow-hidden animate-fade-in">
{/* Header with gradient */}
<div className="bg-gradient-to-r from-indigo-500 to-purple-600 p-6 text-white">
<h1 className="text-3xl font-bold mb-2">Todo Manager</h1>
<p className="text-indigo-100">Stay organized and boost your productivity</p>
<div className="flex gap-4 mt-4 text-sm">
<div className="bg-white/20 px-3 py-1 rounded-lg">
Total: <span className="font-bold">{todos.length}</span>
</div>
<div className="bg-white/20 px-3 py-1 rounded-lg">
Completed: <span className="font-bold">{completedCount}</span>
</div>
<div className="bg-white/20 px-3 py-1 rounded-lg">
Pending: <span className="font-bold">{pendingCount}</span>
</div>
</div>
</div>
{/* Todo list */}
<div className="max-h-80 overflow-y-auto p-2 [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-track]:bg-gray-100 [&::-webkit-scrollbar-track]:rounded-xl [&::-webkit-scrollbar-thumb]:bg-indigo-200 [&::-webkit-scrollbar-thumb]:rounded-xl [&::-webkit-scrollbar-thumb:hover]:bg-indigo-300">
{todos.length === 0 ? (
<div className="py-8 text-center text-gray-500">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<p className="mt-2">Your todo list is empty</p>
<p className="text-sm">Add a new task to get started!</p>
</div>
) : (
todos.map(todo => (
<div
key={todo.id}
className={`flex items-center justify-between p-4 mb-2 rounded-xl border-l-4 transition-all duration-300 ease-in-out ${
todo.completed
? 'bg-green-50 border-green-400 text-green-700'
: 'bg-gray-50 border-indigo-400'
}`}
>
<div className="flex items-center gap-3 flex-1">
<button
onClick={() => toggleTodo(todo.id)}
className={`w-6 h-6 rounded-full border flex items-center justify-center transition-all duration-200 ease hover:scale-105 ${
todo.completed
? 'bg-green-500 border-green-500 text-white'
: 'bg-white border-gray-300'
}`}
aria-label={todo.completed ? "Mark as incomplete" : "Mark as complete"}
>
{todo.completed && (
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 11.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</button>
<span className={`text-gray-800 flex-1 ${
todo.completed ? 'line-through text-green-700' : ''
}`}>
{todo.text}
</span>
</div>
<button
onClick={() => deleteTodo(todo.id)}
className="text-red-500 hover:text-red-700 transition-all duration-200 ease hover:scale-110 hover:opacity-80"
aria-label="Delete todo"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
<path fillRule="evenodd" d="M4 2a2 2 0 00-2 2v3.293A2 2 0 002.293 9L4 10.707V16a2 2 0 002 2h8a2 2 0 002-2v-5.293l1.707-1.707A2 2 0 0018 7.293V4a2 2 0 00-2-2H4z" clipRule="evenodd" />
</svg>
</button>
</div>
))
)}
</div>
{/* Add todo form */}
<div className="p-6 border-t border-gray-100">
<div className="flex gap-3 items-center">
<div className="relative flex-1">
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new task..."
className="w-full px-4 py-3 rounded-xl border border-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent"
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
/>
</div>
<button
onClick={addTodo}
disabled={!newTodo.trim()}
className={`px-6 py-3 rounded-xl font-medium transition-all duration-200 ease hover:scale-105 hover:shadow-lg ${
newTodo.trim()
? 'bg-gradient-to-r from-indigo-500 to-purple-600 text-white'
: 'bg-gray-200 text-gray-500 cursor-not-allowed'
}`}
>
Add
</button>
</div>
</div>
</div>
</div>
);
}
function App() {
return (
<TodosProvider>
<TodoApp />
</TodosProvider>
);
}
// Render the application
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html> |
Beta Was this translation helpful? Give feedback.
Answered by
gengjiawen
Apr 28, 2025
Replies: 2 comments 2 replies
-
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Jotai Todo App</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- React and ReactDOM CDN -->
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- Jotai UMD: load vanilla internals then core then utils, then React bindings -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/vanilla/internals.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/vanilla.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/vanilla/utils.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/umd/react.development.js"></script>
<!-- Babel Standalone for JSX -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div id="root"></div>
<script type="text/babel">
// Destructure hooks and utils from Jotai UMD
const { useAtom } = window.jotaiReact;
const { atomWithStorage } = window.jotaiVanillaUtils;
// Create an atom with localStorage persistence
const todoListAtom = atomWithStorage('todoList', []);
function TodoList() {
const [todos, setTodos] = useAtom(todoListAtom);
// Add a new todo
const addTodo = (text) => {
setTodos((oldTodos) => [
...oldTodos,
{ id: Date.now(), text, completed: false },
]);
};
// Toggle completion status
const toggleTodo = (id) => {
setTodos((oldTodos) =>
oldTodos.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
return (
<div className="bg-white rounded-xl shadow-lg p-8 w-full max-w-md">
<h1 className="text-2xl font-bold mb-4">Jotai Todos</h1>
<TodoInput addTodo={addTodo} />
<ul className="mt-4">
{todos.map((todo) => (
<li
key={todo.id}
onClick={() => toggleTodo(todo.id)}
className={
'cursor-pointer px-4 py-2 rounded mb-2 flex items-center ' +
(todo.completed
? 'bg-green-100 text-green-700 line-through'
: 'bg-gray-100 hover:bg-indigo-100')
}
title="Toggle complete"
>
{todo.text}
</li>
))}
</ul>
</div>
);
}
/**
* @param {{ addTodo: (text: string) => void }} props
*/
function TodoInput({ addTodo }) {
const [input, setInput] = React.useState('');
// Handle form submission
const submit = (e) => {
e.preventDefault();
if (input.trim()) {
addTodo(input);
setInput('');
}
};
return (
<form onSubmit={submit} className="mb-4 flex">
<input
value={input}
onChange={(e) => setInput(e.target.value)}
className="border p-2 rounded-l flex-1 focus:outline-none focus:ring-2 focus:ring-indigo-400"
placeholder="Add a new todo..."
/>
<button
type="submit"
className="bg-indigo-500 text-white px-4 py-2 rounded-r hover:bg-indigo-600 disabled:bg-gray-300 disabled:text-gray-500"
disabled={!input.trim()}
>
Add
</button>
</form>
);
}
function JotaiTodo() {
return (
<div>
<TodoList />
</div>
);
}
// Render the app
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<JotaiTodo />);
</script>
</body>
</html> this works. But this looks very cumbersome and error-prone, is it possible I can import a single file @dai-shi ?
|
Beta Was this translation helpful? Give feedback.
2 replies
-
This looks works. Do you think this should be add to docs ? <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React Jotai Todo App (Tailwind)</title>
<!-- 1. Load Tailwind CSS via Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Optional: Keep custom keyframes if needed, Tailwind transitions cover most cases -->
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-15px); }
to { opacity: 1; transform: translateX(0); }
}
/* Add custom Tailwind animation definitions if desired, or use default/custom CSS */
.animate-fade-in { animation: fadeIn 0.5s ease-out forwards; }
.animate-slide-in { animation: slideIn 0.3s ease-out forwards; }
</style>
<!-- 2. Load Babel Standalone FIRST -->
<script src="https://unpkg.com/@babel/[email protected]/babel.min.js"></script>
</head>
<body class="bg-gradient-to-br from-blue-400 to-indigo-600 min-h-screen flex justify-center items-center p-4 sm:p-6">
<div id="root"></div>
<!-- 3. Use type="text/babel" and data-type="module" for React code -->
<script type="text/babel" data-type="module">
import React, { useState } from 'https://esm.sh/[email protected]';
import { createRoot } from 'https://esm.sh/[email protected]/client';
import { atom, useAtom } from 'https://esm.sh/[email protected][email protected]';
// Jotai atom
const todosAtom = atom([]);
const TodoApp = () => {
const [todos, setTodos] = useAtom(todosAtom);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos(prevTodos => [
...prevTodos,
{ id: Date.now(), text: inputValue, completed: false }
]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
};
const deleteTodo = (id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
addTodo();
}
};
return (
// Apply Tailwind classes to the container
<div className="bg-white/95 rounded-2xl shadow-xl max-w-lg w-full p-6 sm:p-8 backdrop-blur-sm animate-fade-in">
<h1 className="text-center text-gray-800 mb-6 text-3xl sm:text-4xl font-semibold tracking-wide">
Todo Master
</h1>
<div className="flex gap-3 mb-6">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Add a new task..." // English placeholder
aria-label="New todo input" // English aria-label
// Apply Tailwind classes to the input
className="flex-grow p-3 sm:p-4 border-2 border-gray-300 rounded-xl text-base outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/30 transition duration-300"
/>
<button
onClick={addTodo}
aria-label="Add todo" // English aria-label
// Apply Tailwind classes to the button
className="px-5 sm:px-6 py-3 sm:py-4 border-none rounded-xl bg-indigo-600 text-white text-base cursor-pointer transition duration-300 ease-in-out hover:bg-indigo-700 hover:-translate-y-0.5 hover:shadow-lg active:translate-y-px flex items-center justify-center"
>
Add {/* English button text */}
</button>
</div>
<ul className="list-none space-y-3"> {/* Add space between items */}
{todos.length === 0 ? (
// Apply Tailwind classes to the empty state message
<li className="text-center text-gray-600 text-lg sm:text-xl py-8 px-4 bg-gray-50 rounded-xl animate-fade-in">
No tasks yet. Add one above! {/* English text */}
</li>
) : (
todos.map((todo) => (
<li
key={todo.id}
// Apply Tailwind classes dynamically based on completion status
className={`flex items-center p-3 sm:p-4 rounded-xl transition duration-200 ease-out animate-slide-in hover:translate-x-1 hover:shadow-sm ${
todo.completed
? 'bg-gray-200 line-through opacity-70' // Completed style
: 'bg-gray-50 hover:bg-gray-100' // Normal style
}`}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
aria-label={`Mark "${todo.text}" as completed`} // English aria-label
// Apply Tailwind classes to checkbox
className="w-5 h-5 sm:w-6 sm:h-6 mr-4 cursor-pointer accent-indigo-600 flex-shrink-0"
/>
{/* Apply Tailwind classes to the text span */}
<span className={`flex-1 text-base sm:text-lg break-words ${todo.completed ? 'text-gray-500' : 'text-gray-800'}`}>
{todo.text}
</span>
<button
onClick={() => deleteTodo(todo.id)}
aria-label={`Delete "${todo.text}"`} // English aria-label
// Apply Tailwind classes to the delete button
className="ml-3 flex-shrink-0 bg-red-500 w-8 h-8 sm:w-10 sm:h-10 p-0 rounded-lg sm:rounded-xl text-white text-lg flex items-center justify-center transition duration-300 hover:bg-red-600 opacity-60 hover:opacity-100"
>
🗑️
</button>
</li>
))
)}
</ul>
{/* Apply Tailwind classes to the footer */}
<div className="text-center mt-8 text-gray-500/90 text-sm">
Built with React & Jotai | World Programming Contest 2025 {/* English text */}
</div>
</div>
);
};
// Rendering the app
const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
root.render(<TodoApp />);
} else {
console.error("Root container element not found.");
}
</script>
</body>
</html> |
Beta Was this translation helpful? Give feedback.
0 replies
Answer selected by
gengjiawen
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This looks works. Do you think this should be add to docs ?