Initial version

This commit is contained in:
2025-06-15 18:40:30 -06:00
parent 78eb6e64df
commit 2f08b94c89
7 changed files with 1682 additions and 0 deletions

257
public/index.html Normal file
View File

@ -0,0 +1,257 @@
<!doctype html>
<html>
<head>
<title>Encrypted Todo List</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="header">
<h1>Encrypted Todo List</h1>
</div>
<div id="user-setup" class="container">
<h3 class="section-title">Create/Login User</h3>
<input type="text" id="userId" placeholder="Enter user ID" />
<button onclick="createUser()">Create User</button>
</div>
<div id="todo-app" style="display: none">
<div class="container">
<h3 class="section-title">Add New Todo</h3>
<div>
<input
type="text"
id="todoText"
placeholder="Enter todo item"
/>
<button onclick="addTodo()">Add Todo</button>
</div>
</div>
<div class="container">
<h3 class="section-title">Your Encrypted Todos</h3>
<div class="toggle-container">
<span>Show Decrypted</span>
<label class="toggle-switch">
<input
type="checkbox"
id="viewToggle"
onchange="toggleView()"
/>
<span class="slider"></span>
</label>
<span>Show Encrypted</span>
</div>
<div id="todos"></div>
</div>
</div>
<script>
let currentUser = null;
let allTodos = [];
let showEncrypted = false;
let allUsers = [];
async function createUser() {
const userId = document.getElementById("userId").value;
if (!userId) return;
try {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userId }),
});
if (response.ok) {
currentUser = userId;
document.getElementById("user-setup").style.display =
"none";
document.getElementById("todo-app").style.display =
"block";
loadUsers();
loadTodos();
} else {
const error = await response.json();
alert(error.error);
}
} catch (error) {
alert("Error creating user: " + error.message);
}
}
async function loadUsers() {
try {
const response = await fetch("/api/users");
allUsers = await response.json();
} catch (error) {
console.error("Error loading users:", error);
}
}
async function addTodo() {
const text = document.getElementById("todoText").value;
if (!text) return;
try {
const response = await fetch(
`/api/users/${currentUser}/todos`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
},
);
if (response.ok) {
document.getElementById("todoText").value = "";
loadTodos();
}
} catch (error) {
alert("Error adding todo: " + error.message);
}
}
async function loadTodos() {
try {
// Get the decrypted todos
const response = await fetch(
`/api/users/${currentUser}/todos`,
);
const todos = await response.json();
// Get the raw encrypted todos
const encryptedResponse = await fetch(
`/api/users/${currentUser}/todos/encrypted`,
);
const encryptedTodos = await encryptedResponse.json();
// Create a map of encrypted todos by ID
const encryptedMap = {};
encryptedTodos.forEach((todo) => {
encryptedMap[todo.id] = todo.encrypted;
});
// Combine the data
allTodos = todos.map((todo) => ({
...todo,
encrypted: encryptedMap[todo.id] || {
body: "Encryption data not available",
},
}));
renderTodos();
} catch (error) {
console.error("Error loading todos:", error);
}
}
function renderTodos() {
const todosDiv = document.getElementById("todos");
if (allTodos.length === 0) {
todosDiv.innerHTML = "<p>No todos yet. Add one above.</p>";
return;
}
todosDiv.innerHTML = allTodos
.map(
(todo) => `
<div class="todo-item">
<div class="todo-content">
${
showEncrypted
? `<div class="encrypted-data">
<strong>Type:</strong> ${todo.encrypted.type}<br>
<strong>Encrypted Data:</strong><br>${todo.encrypted.body}
</div>`
: `<div>
<span>${todo.text}</span>
${todo.sharedBy ? `<span class="shared-badge">Shared by ${todo.sharedBy}</span>` : ""}
</div>`
}
</div>
<div class="todo-actions">
${
!todo.sharedBy
? `
<div class="share-container">
<select id="share-${todo.id}" class="share-select">
<option value="">Share with...</option>
${allUsers
.filter((user) => user !== currentUser)
.map(
(user) =>
`<option value="${user}">${user}</option>`,
)
.join("")}
</select>
<button onclick="shareTodo('${todo.id}')">Share</button>
</div>
`
: ""
}
<button onclick="deleteTodo('${todo.id}')">Delete</button>
</div>
</div>
`,
)
.join("");
}
function toggleView() {
showEncrypted = document.getElementById("viewToggle").checked;
renderTodos();
}
async function shareTodo(todoId) {
const selectElement = document.getElementById(
`share-${todoId}`,
);
const recipientId = selectElement.value;
if (!recipientId) {
alert("Please select a user to share with");
return;
}
try {
const response = await fetch(
`/api/users/${currentUser}/todos/${todoId}/share`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ recipientId }),
},
);
if (response.ok) {
alert(`Todo shared with ${recipientId}`);
selectElement.value = "";
} else {
const error = await response.json();
alert(error.error);
}
} catch (error) {
alert("Error sharing todo: " + error.message);
}
}
async function deleteTodo(todoId) {
try {
const response = await fetch(
`/api/users/${currentUser}/todos/${todoId}`,
{ method: "DELETE" },
);
if (response.ok) {
loadTodos();
}
} catch (error) {
alert("Error deleting todo: " + error.message);
}
}
</script>
</body>
</html>

153
public/styles.css Normal file
View File

@ -0,0 +1,153 @@
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.todo-item {
background: #f5f5f5;
margin: 10px 0;
padding: 15px;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.todo-content {
flex-grow: 1;
}
.todo-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
}
button {
padding: 8px 12px;
margin: 5px;
background-color: #4a6ea9;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: #3a5a89;
}
input,
select {
padding: 10px;
margin: 5px;
width: 300px;
border: 1px solid #ccc;
border-radius: 4px;
}
.toggle-container {
display: flex;
align-items: center;
margin: 15px 0;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 60px;
height: 34px;
margin: 0 10px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 26px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #4a6ea9;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.encrypted-data {
font-family: monospace;
word-break: break-all;
color: #666;
font-size: 0.9em;
background-color: #f0f0f0;
padding: 5px;
border-radius: 3px;
max-height: 100px;
overflow-y: auto;
}
.header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.header h1 {
margin-right: 10px;
margin-bottom: 0;
}
.share-container {
margin-top: 10px;
display: flex;
align-items: center;
}
.shared-badge {
background-color: #4a6ea9;
color: white;
padding: 3px 8px;
border-radius: 10px;
font-size: 0.8em;
margin-left: 10px;
}
.container {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 20px;
}
.section-title {
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-top: 0;
}