Skip to content

Commit 6ce0e03

Browse files
Match assignment requirements
Add required IDs and classes, tableName configuration, form-based editing and contribution link.
1 parent 2926968 commit 6ce0e03

4 files changed

Lines changed: 63 additions & 49 deletions

File tree

‎README.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Vistula Task Manager
1+
# Task Manager
22

33
A browser-based task manager for the Vistula University team assignment. The application uses the Supabase REST API and supports public create, read, update and delete operations on the `tasks` table.
44

@@ -13,7 +13,7 @@ A browser-based task manager for the Vistula University team assignment. The app
1313

1414
- `index.html` contains the responsive user interface.
1515
- `app.js` calls the Supabase REST API and implements CRUD operations.
16-
- `config.json` contains only the project URL and publishable key.
16+
- `config.json` contains the project URL, publishable key and required `tasks` table name.
1717
- `README.md` documents the project and collaboration workflow.
1818

1919
## Data model
@@ -51,6 +51,8 @@ Each student keeps a separate `vistula-task-manager` repository and a separate S
5151

5252
This repository belongs only to student `79841`. A partner must sign in to their own GitHub and Supabase accounts before any work is done in their repository.
5353

54+
The two collaboration Pull Request links will be added here after both students complete and merge the required contributions. The repository owner must not create `contribution.html` or `contribution.json` directly.
55+
5456
## Local use
5557

5658
Serve the folder through a local web server because browsers can block `fetch()` from `file://` pages:

‎app.js‎

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
const form = document.querySelector('#task-form');
2-
const titleInput = document.querySelector('#title');
3-
const descriptionInput = document.querySelector('#description');
2+
const titleInput = document.querySelector('#task-title');
3+
const descriptionInput = document.querySelector('#task-description');
4+
const submitButton = document.querySelector('#task-submit');
45
const taskList = document.querySelector('#task-list');
5-
const statusElement = document.querySelector('#status');
6+
const statusElement = document.querySelector('#status-message');
67

78
let apiUrl;
89
let apiKey;
10+
let tableName;
911
let tasks = [];
12+
let editingTaskId = null;
1013

1114
function setStatus(message, isError = false) {
1215
statusElement.textContent = message;
@@ -31,7 +34,7 @@ async function request(path = '', options = {}) {
3134
...options.headers,
3235
};
3336

34-
const response = await fetch(`${apiUrl}/rest/v1/tasks${path}`, {
37+
const response = await fetch(`${apiUrl}/rest/v1/${tableName}${path}`, {
3538
...options,
3639
headers,
3740
});
@@ -61,25 +64,31 @@ function renderTasks() {
6164

6265
for (const task of tasks) {
6366
const item = document.createElement('li');
64-
item.className = `task${task.completed ? ' completed' : ''}`;
67+
item.className = `task-item${task.completed ? ' completed' : ''}`;
68+
item.dataset.id = task.id;
6569

66-
const toggle = actionButton(task.completed ? '✓' : '○', 'toggle', () => {
70+
const toggle = actionButton(task.completed ? '✓' : '○', 'toggle toggle-task', () => {
6771
updateTask(task.id, { completed: !task.completed });
6872
});
6973
toggle.setAttribute('aria-label', task.completed ? `Mark ${task.title} as open` : `Mark ${task.title} as completed`);
7074

7175
const copy = document.createElement('div');
7276
const heading = document.createElement('h2');
77+
heading.className = 'task-title';
7378
heading.textContent = task.title;
7479
const description = document.createElement('p');
80+
description.className = 'task-description';
7581
description.textContent = task.description || 'No description';
76-
copy.append(heading, description);
82+
const taskStatus = document.createElement('p');
83+
taskStatus.className = 'task-status';
84+
taskStatus.textContent = task.completed ? 'Completed' : 'Open';
85+
copy.append(heading, description, taskStatus);
7786

7887
const actions = document.createElement('div');
7988
actions.className = 'task-actions';
8089
actions.append(
81-
actionButton('Edit', '', () => editTask(task)),
82-
actionButton('Delete', 'danger', () => deleteTask(task)),
90+
actionButton('Edit', 'edit-task', () => startEditing(task)),
91+
actionButton('Delete', 'danger delete-task', () => deleteTask(task)),
8392
);
8493

8594
item.append(toggle, copy, actions);
@@ -100,23 +109,29 @@ async function loadTasks() {
100109

101110
async function createTask(event) {
102111
event.preventDefault();
103-
const submitButton = form.querySelector('button[type="submit"]');
104112
submitButton.disabled = true;
105113

106114
try {
107-
await request('', {
108-
method: 'POST',
115+
const task = {
116+
title: titleInput.value.trim(),
117+
description: descriptionInput.value.trim(),
118+
completed: editingTaskId === null
119+
? false
120+
: Boolean(tasks.find(({ id }) => id === editingTaskId)?.completed),
121+
};
122+
123+
await request(editingTaskId === null ? '' : `?id=eq.${encodeURIComponent(editingTaskId)}`, {
124+
method: editingTaskId === null ? 'POST' : 'PATCH',
109125
headers: { Prefer: 'return=representation' },
110-
body: JSON.stringify({
111-
title: titleInput.value.trim(),
112-
description: descriptionInput.value.trim(),
113-
completed: false,
114-
}),
126+
body: JSON.stringify(task),
115127
});
128+
const wasEditing = editingTaskId !== null;
129+
editingTaskId = null;
116130
form.reset();
131+
submitButton.textContent = 'Add task';
117132
titleInput.focus();
118133
await loadTasks();
119-
setStatus('Task added.');
134+
setStatus(wasEditing ? 'Task updated.' : 'Task added.');
120135
} catch (error) {
121136
setStatus(error.message, true);
122137
} finally {
@@ -139,20 +154,13 @@ async function updateTask(id, changes) {
139154
}
140155
}
141156

142-
function editTask(task) {
143-
const title = window.prompt('Task title', task.title);
144-
if (title === null) return;
145-
146-
const trimmedTitle = title.trim();
147-
if (!trimmedTitle) {
148-
setStatus('A task title is required.', true);
149-
return;
150-
}
151-
152-
const description = window.prompt('Task description', task.description || '');
153-
if (description === null) return;
154-
155-
updateTask(task.id, { title: trimmedTitle, description: description.trim() });
157+
function startEditing(task) {
158+
editingTaskId = task.id;
159+
titleInput.value = task.title;
160+
descriptionInput.value = task.description || '';
161+
submitButton.textContent = 'Save task';
162+
titleInput.focus();
163+
setStatus(`Editing “${task.title}”.`);
156164
}
157165

158166
async function deleteTask(task) {
@@ -174,15 +182,16 @@ async function initialise() {
174182
const config = await readResponse(response);
175183
apiUrl = config.supabaseUrl.replace(/\/$/, '');
176184
apiKey = config.supabasePublishableKey;
185+
tableName = config.tableName;
177186

178-
if (!apiUrl || !apiKey?.startsWith('sb_publishable_')) {
187+
if (!apiUrl || !apiKey?.startsWith('sb_publishable_') || tableName !== 'tasks') {
179188
throw new Error('Supabase configuration is missing or invalid.');
180189
}
181190

182191
form.addEventListener('submit', createTask);
183192
await loadTasks();
184193
} catch (error) {
185-
form.querySelector('button[type="submit"]').disabled = true;
194+
submitButton.disabled = true;
186195
setStatus(error.message, true);
187196
}
188197
}

‎config.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
22
"supabaseUrl": "https://vuqcmmgozrcangvfeilb.supabase.co",
3-
"supabasePublishableKey": "sb_publishable_UnOvcqYWK7x4360OSMwSQQ_9-NwqCOH"
3+
"supabasePublishableKey": "sb_publishable_UnOvcqYWK7x4360OSMwSQQ_9-NwqCOH",
4+
"tableName": "tasks"
45
}

‎index.html‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1">
66
<meta name="description" content="A small task manager powered by the Supabase REST API.">
77
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'self' https://vuqcmmgozrcangvfeilb.supabase.co; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:; base-uri 'self'; form-action 'self'">
8-
<title>Vistula Task Manager</title>
8+
<title>Task Manager</title>
99
<style>
1010
:root {
1111
color-scheme: light;
@@ -122,7 +122,7 @@
122122
list-style: none;
123123
}
124124

125-
.task {
125+
.task-item {
126126
display: grid;
127127
grid-template-columns: auto 1fr auto;
128128
gap: 14px;
@@ -133,44 +133,46 @@
133133
background: var(--paper);
134134
}
135135

136-
.task h2 { margin: 0 0 4px; font-size: 1rem; }
137-
.task p { margin: 0; color: var(--muted); line-height: 1.45; }
138-
.task.completed h2 { text-decoration: line-through; color: var(--muted); }
136+
.task-item h2 { margin: 0 0 4px; font-size: 1rem; }
137+
.task-item p { margin: 0; color: var(--muted); line-height: 1.45; }
138+
.task-item.completed h2 { text-decoration: line-through; color: var(--muted); }
139+
.task-status { margin-top: 6px !important; font-size: 0.82rem; font-weight: 700; }
139140
.task-actions { display: flex; gap: 8px; }
140141
.toggle { min-width: 42px; padding: 0; font-size: 1.15rem; }
141142
.empty { padding: 28px 0 10px; text-align: center; color: var(--muted); }
142143

143144
@media (max-width: 680px) {
144145
main { padding-top: 32px; }
145146
form { grid-template-columns: 1fr; }
146-
.task { grid-template-columns: auto 1fr; }
147+
.task-item { grid-template-columns: auto 1fr; }
147148
.task-actions { grid-column: 2; flex-wrap: wrap; }
148149
}
149150
</style>
150151
</head>
151152
<body>
152153
<main>
153154
<header>
154-
<h1>Vistula Task Manager</h1>
155-
<p>Create, update and finish tasks stored in Supabase.</p>
155+
<h1>Task Manager</h1>
156+
<p>Vistula student ID: <strong id="student-id">79841</strong></p>
156157
</header>
157158

158159
<section class="panel" aria-labelledby="task-form-title">
159160
<h2 id="task-form-title" hidden>Add a task</h2>
160161
<form id="task-form">
161162
<label>
162163
Title
163-
<input id="title" name="title" maxlength="120" required autocomplete="off">
164+
<input id="task-title" name="title" maxlength="120" required autocomplete="off">
164165
</label>
165166
<label>
166167
Description
167-
<input id="description" name="description" maxlength="500" autocomplete="off">
168+
<input id="task-description" name="description" maxlength="500" autocomplete="off">
168169
</label>
169-
<button class="primary" type="submit">Add task</button>
170+
<button id="task-submit" class="primary" type="submit">Add task</button>
170171
</form>
171172

172-
<p id="status" class="status" role="status" aria-live="polite">Loading tasks…</p>
173+
<p id="status-message" class="status" role="status" aria-live="polite">Loading tasks…</p>
173174
<ul id="task-list" aria-label="Tasks"></ul>
175+
<p><a id="contribution-link" href="contribution.html">Open team contribution</a></p>
174176
</section>
175177
</main>
176178

0 commit comments

Comments
 (0)