diff --git a/litelist-api/source/app.d b/litelist-api/source/app.d
index 7351874..2fb91cc 100644
--- a/litelist-api/source/app.d
+++ b/litelist-api/source/app.d
@@ -4,6 +4,7 @@ import slf4d.default_provider;
void main() {
auto provider = new shared DefaultProvider(true, Levels.INFO);
+ provider.getLoggerFactory().setModuleLevelPrefix("handy_httpd", Levels.WARN);
configureLoggingProvider(provider);
HttpServer server = initServer();
@@ -23,6 +24,7 @@ private HttpServer initServer() {
config.connectionQueueSize = 10;
config.defaultHeaders["Access-Control-Allow-Origin"] = "*";
config.defaultHeaders["Access-Control-Allow-Credentials"] = "true";
+ config.defaultHeaders["Access-Control-Allow-Methods"] = "*";
config.defaultHeaders["Vary"] = "origin";
config.defaultHeaders["Access-Control-Allow-Headers"] = "Authorization";
@@ -44,7 +46,10 @@ private HttpServer initServer() {
mainHandler.addMapping(Method.GET, "/lists", &getNoteLists);
mainHandler.addMapping(Method.POST, "/lists", &createNoteList);
+ mainHandler.addMapping(Method.GET, "/lists/{id}", &getNoteList);
mainHandler.addMapping(Method.DELETE, "/lists/{id}", &deleteNoteList);
+ mainHandler.addMapping(Method.POST, "/lists/{listId}/notes", &createNote);
+ mainHandler.addMapping(Method.DELETE, "/lists/{listId}/notes/{noteId}", &deleteNote);
return new HttpServer(mainHandler, config);
}
diff --git a/litelist-api/source/data.d b/litelist-api/source/data.d
index 207aa48..0435084 100644
--- a/litelist-api/source/data.d
+++ b/litelist-api/source/data.d
@@ -14,6 +14,9 @@ static UserDataSource userDataSource;
static this() {
userDataSource = new FsSqliteDataSource();
+ import slf4d;
+ import d2sqlite3.library;
+ infoF!"Sqlite version %s"(versionString());
}
struct User {
@@ -42,9 +45,12 @@ interface UserDataSource {
void deleteUser(string username);
Nullable!User getUser(string username);
NoteList[] getLists(string username);
+ Nullable!NoteList getList(string username, ulong id);
NoteList createNoteList(string username, string name, string description = null);
void deleteNoteList(string username, ulong id);
+ NoteList updateNoteList(string username, ulong id, NoteList newData);
Note createNote(string username, ulong noteListId, string content);
+ Note updateNote(string username, ulong id, Note newData);
void deleteNote(string username, ulong id);
}
@@ -107,13 +113,26 @@ class FsSqliteDataSource : UserDataSource {
return lists;
}
+ Nullable!NoteList getList(string username, ulong id) {
+ Database db = getDb(username);
+ ResultRange results = db.execute("SELECT * FROM note_list WHERE id = ?", id);
+ if (results.empty()) return Nullable!NoteList.init;
+ NoteList list = parseNoteList(results.front());
+ ResultRange noteResults = db.execute("SELECT * FROM note WHERE note_list_id = ? ORDER BY ordinality ASC", id);
+ foreach (Row row; noteResults) {
+ list.notes ~= parseNote(row);
+ }
+ return nullable(list);
+ }
+
NoteList createNoteList(string username, string name, string description = null) {
Database db = getDb(username);
-
Statement existsStatement = db.prepare("SELECT COUNT(name) FROM note_list WHERE name = ?");
existsStatement.bind(1, name);
ResultRange existsResult = existsStatement.execute();
- if (existsResult.oneValue!int() > 0) throw new HttpStatusException(HttpStatus.BAD_REQUEST, "List already exists.");
+ if (existsResult.oneValue!int() > 0) {
+ throw new HttpStatusException(HttpStatus.BAD_REQUEST, "List already exists.");
+ }
Nullable!uint ordResult = db.execute("SELECT MAX(ordinality) + 1 FROM note_list").oneValue!(Nullable!uint);
uint ordinality = 0;
@@ -128,12 +147,47 @@ class FsSqliteDataSource : UserDataSource {
void deleteNoteList(string username, ulong id) {
Database db = getDb(username);
- Statement stmt1 = db.prepare("DELETE FROM note WHERE note_list_id = ?");
- stmt1.bind(1, id);
- stmt1.execute();
- Statement stmt2 = db.prepare("DELETE FROM note_list WHERE id = ?");
- stmt2.bind(1, id);
- stmt2.execute();
+ db.begin();
+ db.execute("DELETE FROM note WHERE note_list_id = ?", id);
+ Nullable!uint ordinality = db.execute(
+ "SELECT ordinality FROM note_list WHERE id = ?", id
+ ).oneValue!(Nullable!uint)();
+ db.execute("DELETE FROM note_list WHERE id = ?", id);
+ if (!ordinality.isNull) {
+ db.execute("UPDATE note_list SET ordinality = ordinality - 1 WHERE ordinality > ?", ordinality.get);
+ }
+ db.commit();
+ }
+
+ NoteList updateNoteList(string username, ulong id, NoteList newData) {
+ Database db = getDb(username);
+ ResultRange result = db.execute("SELECT * FROM note_list WHERE id = ?", id);
+ if (result.empty()) throw new HttpStatusException(HttpStatus.NOT_FOUND);
+ NoteList list = parseNoteList(result.front());
+ db.begin();
+ if (list.ordinality != newData.ordinality) {
+ if (newData.ordinality > list.ordinality) {
+ // Decrement all lists between the old index and the new one.
+ db.execute(
+ "UPDATE note_list SET ordinality = ordinality - 1 WHERE ordinality > ? AND ordinality <= ?",
+ list.ordinality,
+ newData.ordinality
+ );
+ } else {
+ // Increment all lists between the old index and the new one.
+ db.execute(
+ "UPDATE note_list SET ordinality = ordinality + 1 WHERE ordinality >= ? AND ordinality < ?",
+ newData.ordinality,
+ list.ordinality
+ );
+ }
+ }
+ db.execute(
+ "UPDATE note_list SET name = ?, description = ?, ordinality = ? WHERE id = ?",
+ newData.name, newData.description, newData.ordinality, id
+ );
+ db.commit();
+ return NoteList(id, newData.name, newData.ordinality, newData.description, []);
}
Note createNote(string username, ulong noteListId, string content) {
@@ -158,11 +212,50 @@ class FsSqliteDataSource : UserDataSource {
);
}
+ Note updateNote(string username, ulong id, Note newData) {
+ Database db = getDb(username);
+ ResultRange result = db.execute("SELECT * FROM note WHERE id = ?", id);
+ if (result.empty()) throw new HttpStatusException(HttpStatus.NOT_FOUND);
+ Note note = parseNote(result.front());
+ db.begin();
+ if (note.ordinality != newData.ordinality) {
+ if (newData.ordinality > note.ordinality) {
+ // Decrement all notes between the old index and the new one.
+ db.execute(
+ "UPDATE note SET ordinality = ordinality - 1 WHERE ordinality > ? AND ordinality <= ?",
+ note.ordinality,
+ newData.ordinality
+ );
+ } else {
+ // Increment all notes between the old index and the new one.
+ db.execute(
+ "UPDATE note SET ordinality = ordinality + 1 WHERE ordinality >= ? AND ordinality < ?",
+ newData.ordinality,
+ note.ordinality
+ );
+ }
+ }
+ db.execute(
+ "UPDATE note SET ordinality = ?, content = ? WHERE id = ?",
+ newData.ordinality,
+ newData.content,
+ id
+ );
+ db.commit();
+ return Note(id, note.noteListId, newData.ordinality, newData.content);
+ }
+
void deleteNote(string username, ulong id) {
Database db = getDb(username);
- Statement stmt = db.prepare("DELETE FROM note WHERE id = ?");
- stmt.bind(1, id);
- stmt.execute();
+ db.begin();
+ Nullable!uint ordinality = db.execute(
+ "SELECT ordinality FROM note WHERE id = ?", id
+ ).oneValue!(Nullable!uint)();
+ db.execute("DELETE FROM note WHERE id = ?", id);
+ if (!ordinality.isNull) {
+ db.execute("UPDATE note SET ordinality = ordinality - 1 WHERE ordinality > ?", ordinality.get);
+ }
+ db.commit();
}
private NoteList parseNoteList(Row row) {
diff --git a/litelist-api/source/lists.d b/litelist-api/source/lists.d
index d619206..649ec87 100644
--- a/litelist-api/source/lists.d
+++ b/litelist-api/source/lists.d
@@ -2,6 +2,8 @@ module lists;
import handy_httpd;
import std.json;
+import std.typecons;
+import std.string;
import auth;
import data;
@@ -17,22 +19,68 @@ void getNoteLists(ref HttpRequestContext ctx) {
ctx.response.writeBodyString(listsArray.toString(), "application/json");
}
+void getNoteList(ref HttpRequestContext ctx) {
+ if (!validateAuthenticatedRequest(ctx)) return;
+ AuthContext auth = AuthContextHolder.getOrThrow();
+ ulong id = ctx.request.getPathParamAs!ulong("id");
+ Nullable!NoteList optionalList = userDataSource.getList(auth.user.username, id);
+ if (!optionalList.isNull) {
+ ctx.response.writeBodyString(serializeList(optionalList.get()).toString(), "application/json");
+ } else {
+ ctx.response.setStatus(HttpStatus.NOT_FOUND);
+ }
+}
+
void createNoteList(ref HttpRequestContext ctx) {
if (!validateAuthenticatedRequest(ctx)) return;
AuthContext auth = AuthContextHolder.getOrThrow();
JSONValue requestBody = ctx.request.readBodyAsJson();
- string listName = requestBody.object["name"].str;
- string description = requestBody.object["description"].str;
+ if ("name" !in requestBody.object) {
+ ctx.response.setStatus(HttpStatus.BAD_REQUEST);
+ ctx.response.writeBodyString("Missing required name for creating a new list.");
+ return;
+ }
+ string listName = strip(requestBody.object["name"].str);
+ if (listName.length < 3) {
+ ctx.response.setStatus(HttpStatus.BAD_REQUEST);
+ ctx.response.writeBodyString("List name is too short. Should be at least 3 characters.");
+ }
+ string description = null;
+ if ("description" in requestBody.object) {
+ description = strip(requestBody.object["description"].str);
+ }
NoteList list = userDataSource.createNoteList(auth.user.username, listName, description);
ctx.response.writeBodyString(serializeList(list).toString(), "application/json");
}
+void createNote(ref HttpRequestContext ctx) {
+ if (!validateAuthenticatedRequest(ctx)) return;
+ AuthContext auth = AuthContextHolder.getOrThrow();
+ ulong listId = ctx.request.getPathParamAs!ulong("listId");
+ JSONValue requestBody = ctx.request.readBodyAsJson();
+ if ("content" !in requestBody || requestBody.object["content"].type != JSONType.STRING || requestBody.object["content"].str.length < 1) {
+ ctx.response.setStatus(HttpStatus.BAD_REQUEST);
+ ctx.response.writeBodyString("Missing string content.");
+ return;
+ }
+ string content = requestBody.object["content"].str;
+ Note note = userDataSource.createNote(auth.user.username, listId, content);
+ ctx.response.writeBodyString(serializeNote(note).toString(), "application/json");
+}
+
void deleteNoteList(ref HttpRequestContext ctx) {
if (!validateAuthenticatedRequest(ctx)) return;
AuthContext auth = AuthContextHolder.getOrThrow();
userDataSource.deleteNoteList(auth.user.username, ctx.request.getPathParamAs!ulong("id"));
}
+void deleteNote(ref HttpRequestContext ctx) {
+ if (!validateAuthenticatedRequest(ctx)) return;
+ AuthContext auth = AuthContextHolder.getOrThrow();
+ ulong noteId = ctx.request.getPathParamAs!ulong("noteId");
+ userDataSource.deleteNote(auth.user.username, noteId);
+}
+
private JSONValue serializeList(NoteList list) {
JSONValue listObj = JSONValue(string[string].init);
listObj.object["id"] = JSONValue(list.id);
diff --git a/litelist-app/src/App.vue b/litelist-app/src/App.vue
index d2c1397..d6a3e5e 100644
--- a/litelist-app/src/App.vue
+++ b/litelist-app/src/App.vue
@@ -1,5 +1,5 @@
diff --git a/litelist-app/src/api/auth.ts b/litelist-app/src/api/auth.ts
index 77c44a0..7a82133 100644
--- a/litelist-app/src/api/auth.ts
+++ b/litelist-app/src/api/auth.ts
@@ -5,6 +5,10 @@ export interface User {
email: string
}
+export function emptyUser(): User {
+ return {username: "", email: ""}
+}
+
export interface LoginInfo {
user: User
token: string
diff --git a/litelist-app/src/api/lists.ts b/litelist-app/src/api/lists.ts
index a555928..1a1d772 100644
--- a/litelist-app/src/api/lists.ts
+++ b/litelist-app/src/api/lists.ts
@@ -4,16 +4,30 @@ export interface Note {
id: number
ordinality: number
content: string
- noteListName: string
+ noteListId: number
}
export interface NoteList {
+ id: number
name: string
ordinality: number
description: string
notes: Note[]
}
+export async function createNoteList(token: string, name: string, description: string | null): Promise {
+ const response = await fetch(API_URL + "/lists", {
+ method: "POST",
+ headers: {"Authorization": "Bearer " + token},
+ body: JSON.stringify({name: name, description: description})
+ })
+ if (response.ok) {
+ return await response.json()
+ } else {
+ throw response
+ }
+}
+
export async function getNoteLists(token: string): Promise {
const response = await fetch(API_URL + "/lists", {
headers: {"Authorization": "Bearer " + token}
@@ -24,4 +38,42 @@ export async function getNoteLists(token: string): Promise {
console.error(response)
return []
}
-}
\ No newline at end of file
+}
+
+export async function getNoteList(token: string, id: number): Promise {
+ const response = await fetch(API_URL + "/lists/" + id, {
+ headers: {"Authorization": "Bearer " + token}
+ })
+ if (response.ok) {
+ return await response.json()
+ } else {
+ return null
+ }
+}
+
+export async function deleteNoteList(token: string, id: number): Promise {
+ await fetch(API_URL + "/lists/" + id, {
+ method: "DELETE",
+ headers: {"Authorization": "Bearer " + token}
+ })
+}
+
+export async function createNote(token: string, listId: number, content: string): Promise {
+ const response = await fetch(API_URL + "/lists/" + listId + "/notes", {
+ method: "POST",
+ headers: {"Authorization": "Bearer " + token},
+ body: JSON.stringify({content: content})
+ })
+ if (response.ok) {
+ return await response.json()
+ } else {
+ throw response
+ }
+}
+
+export async function deleteNote(token: string, listId: number, id: number): Promise {
+ await fetch(API_URL + "/lists/" + listId + "/notes/" + id, {
+ method: "DELETE",
+ headers: {"Authorization": "Bearer " + token}
+ })
+}
diff --git a/litelist-app/src/assets/base.css b/litelist-app/src/assets/base.css
new file mode 100644
index 0000000..08c11b9
--- /dev/null
+++ b/litelist-app/src/assets/base.css
@@ -0,0 +1,4 @@
+body {
+ font-family: sans-serif;
+ margin: 0;
+}
\ No newline at end of file
diff --git a/litelist-app/src/assets/trash-emoji.svg b/litelist-app/src/assets/trash-emoji.svg
new file mode 100644
index 0000000..979e85a
--- /dev/null
+++ b/litelist-app/src/assets/trash-emoji.svg
@@ -0,0 +1,50 @@
+
+
diff --git a/litelist-app/src/main.ts b/litelist-app/src/main.ts
index fda1e6e..36a0e42 100644
--- a/litelist-app/src/main.ts
+++ b/litelist-app/src/main.ts
@@ -1,6 +1,7 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
+import "./assets/base.css"
import App from './App.vue'
import router from './router'
diff --git a/litelist-app/src/router/index.ts b/litelist-app/src/router/index.ts
index 91d4dbc..53db10e 100644
--- a/litelist-app/src/router/index.ts
+++ b/litelist-app/src/router/index.ts
@@ -2,6 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router'
import LoginView from "@/views/LoginView.vue";
import ListsView from "@/views/ListsView.vue";
import {useAuthStore} from "@/stores/auth";
+import SingleListView from "@/views/SingleListView.vue";
+
+function checkAuth() {
+ const authStore = useAuthStore()
+ if (!authStore.authenticated) return "login"
+}
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -10,22 +16,22 @@ const router = createRouter({
path: "/",
name: "home-redirect",
redirect: to => {
- return "login"
+ return "/login"
}
},
{
path: "/login",
- name: "login",
component: LoginView
},
{
path: "/lists",
- name: "lists",
component: ListsView,
- beforeEnter: (to, from) => {
- const authStore = useAuthStore()
- if (!authStore.authenticated) return "login"
- }
+ beforeEnter: checkAuth
+ },
+ {
+ path: "/lists/:id",
+ component: SingleListView,
+ beforeEnter: checkAuth
}
]
})
diff --git a/litelist-app/src/stores/auth.ts b/litelist-app/src/stores/auth.ts
index bcef4f1..9dbab7c 100644
--- a/litelist-app/src/stores/auth.ts
+++ b/litelist-app/src/stores/auth.ts
@@ -1,11 +1,12 @@
import {defineStore} from "pinia";
import {type Ref, ref} from "vue";
import type {User} from "@/api/auth";
+import {emptyUser} from "@/api/auth";
export const useAuthStore = defineStore("auth", () => {
const authenticated: Ref = ref(false)
- const user: Ref = ref(null)
- const token: Ref = ref(null)
+ const user: Ref = ref(emptyUser())
+ const token: Ref = ref("")
function logIn(newToken: string, newUser: User) {
authenticated.value = true
@@ -15,8 +16,8 @@ export const useAuthStore = defineStore("auth", () => {
function logOut() {
authenticated.value = false
- user.value = null
- token.value = null
+ user.value = emptyUser()
+ token.value = ""
}
return {authenticated, user, token, logIn, logOut}
diff --git a/litelist-app/src/util.ts b/litelist-app/src/util.ts
new file mode 100644
index 0000000..79c8146
--- /dev/null
+++ b/litelist-app/src/util.ts
@@ -0,0 +1,8 @@
+export function stringToColor(str: string, saturation: number = 100, lightness: number = 75): string {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ hash = hash & hash;
+ }
+ return `hsl(${(hash % 360)}, ${saturation}%, ${lightness}%)`;
+}
\ No newline at end of file
diff --git a/litelist-app/src/views/ListsView.vue b/litelist-app/src/views/ListsView.vue
index 568d8ba..9e26ee6 100644
--- a/litelist-app/src/views/ListsView.vue
+++ b/litelist-app/src/views/ListsView.vue
@@ -2,31 +2,125 @@
import {useAuthStore} from "@/stores/auth";
import {onMounted, ref, type Ref} from "vue";
import type {NoteList} from "@/api/lists";
-import {getNoteLists} from "@/api/lists";
+import {createNoteList, getNoteLists} from "@/api/lists";
+import {useRouter} from "vue-router";
+import {stringToColor} from "@/util";
const authStore = useAuthStore()
+const router = useRouter()
const noteLists: Ref = ref([])
+const creatingNewList: Ref = ref(false)
+const newListModel = ref({
+ name: "",
+ description: ""
+})
+
onMounted(async () => {
noteLists.value = await getNoteLists(authStore.token)
})
+
+function toggleCreatingNewList() {
+ if (!creatingNewList.value) {
+ newListModel.value.name = ""
+ newListModel.value.description = ""
+ }
+ creatingNewList.value = !creatingNewList.value
+}
+
+async function goToList(id: number) {
+ await router.push("/lists/" + id)
+}
+
+async function createList() {
+ const noteList = await createNoteList(
+ authStore.token,
+ newListModel.value.name,
+ newListModel.value.description
+ )
+ await router.push("/lists/" + noteList.id)
+}
-
- Lists
-
-
- Here are your lists!
-
-
+
+
Lists
+
+
+
+
+
+
+
+
-
-
-
+
\ No newline at end of file
diff --git a/litelist-app/src/views/LoginView.vue b/litelist-app/src/views/LoginView.vue
index 9c1d566..483ed90 100644
--- a/litelist-app/src/views/LoginView.vue
+++ b/litelist-app/src/views/LoginView.vue
@@ -29,20 +29,66 @@ async function doLogin() {
-
+
\ No newline at end of file
diff --git a/litelist-app/src/views/SingleListView.vue b/litelist-app/src/views/SingleListView.vue
new file mode 100644
index 0000000..efbfa92
--- /dev/null
+++ b/litelist-app/src/views/SingleListView.vue
@@ -0,0 +1,164 @@
+
+
+
+