Add note and add list functionality.
This commit is contained in:
parent
69ea579ea3
commit
3a1092d468
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -5,6 +5,10 @@ export interface User {
|
|||
email: string
|
||||
}
|
||||
|
||||
export function emptyUser(): User {
|
||||
return {username: "", email: ""}
|
||||
}
|
||||
|
||||
export interface LoginInfo {
|
||||
user: User
|
||||
token: string
|
||||
|
|
|
@ -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<NoteList> {
|
||||
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<NoteList[]> {
|
||||
const response = await fetch(API_URL + "/lists", {
|
||||
headers: {"Authorization": "Bearer " + token}
|
||||
|
@ -24,4 +38,42 @@ export async function getNoteLists(token: string): Promise<NoteList[]> {
|
|||
console.error(response)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNoteList(token: string, id: number): Promise<NoteList | null> {
|
||||
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<void> {
|
||||
await fetch(API_URL + "/lists/" + id, {
|
||||
method: "DELETE",
|
||||
headers: {"Authorization": "Bearer " + token}
|
||||
})
|
||||
}
|
||||
|
||||
export async function createNote(token: string, listId: number, content: string): Promise<Note> {
|
||||
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<void> {
|
||||
await fetch(API_URL + "/lists/" + listId + "/notes/" + id, {
|
||||
method: "DELETE",
|
||||
headers: {"Authorization": "Bearer " + token}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
|
||||
<g>
|
||||
<ellipse style="fill:#B9E4EA;" cx="63.94" cy="104.89" rx="35" ry="13.61"/>
|
||||
<path style="fill:#94D1E0;" d="M29.98,110.19c0-7.13,15.2-12.04,33.96-12.04s33.96,4.91,33.96,12.04s-15.2,13.53-33.96,13.53
|
||||
S29.98,117.32,29.98,110.19z"/>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="64.1107" y1="89.9664" x2="64.1107" y2="147.6283">
|
||||
<stop offset="0" style="stop-color:#82AFC1"/>
|
||||
<stop offset="1" style="stop-color:#2F7889"/>
|
||||
</linearGradient>
|
||||
<path style="fill:url(#SVGID_1_);" d="M108.51,32.83l-2.26,12.33l-6.61-6.61l3.44-3.44l-9.75,2.84l0.6,0.6l-8.09,8.09l-6.54-6.54
|
||||
l-9.63,0.82l-5.72,5.72l-6.2-6.2l-8.96-0.52l-6.72,6.72l-8.09-8.09l0.83-0.83l-9.36-1.98l2.81,2.81l-6.39,6.39L19.63,32.6
|
||||
l-4.56-2.58l14.51,80.37C30.7,118.02,45.29,124,64.05,124s33.08-5.98,34.51-13.61l14.6-80.45L108.51,32.83z M84.06,110.53
|
||||
l-6.32-6.32l8.09-8.09l8.09,8.09l0,0l-4.72,4.72C87.58,109.51,85.86,110.04,84.06,110.53z M39.21,109.46l-5.25-5.25l0,0l8.09-8.09
|
||||
l8.09,8.09l-6.51,6.51C42.09,110.34,40.61,109.91,39.21,109.46z M72.03,104.22l-8.09,8.09l-8.09-8.09l8.09-8.09L72.03,104.22z
|
||||
M66.8,93.27l8.09-8.09l8.09,8.09l-8.09,8.09L66.8,93.27z M52.99,101.36l-8.09-8.09l8.09-8.09l8.09,8.09L52.99,101.36z
|
||||
M52.99,107.07l6.13,6.13c-3.65-0.25-7.33-0.75-10.84-1.43L52.99,107.07z M68.76,113.2l6.13-6.13l4.58,4.58
|
||||
C75.99,112.39,72.36,112.94,68.76,113.2z M96.07,100.65l-7.38-7.38l8.09-8.09l1.8,1.8L96.07,100.65z M100.67,75.57l-3.89,3.89
|
||||
l-8.09-8.09l8.09-8.09l5.19,5.19L100.67,75.57z M93.92,82.32l-8.09,8.09l-8.09-8.09l8.09-8.09L93.92,82.32z M74.88,79.47
|
||||
l-8.09-8.09l8.09-8.09l8.09,8.09L74.88,79.47z M72.03,82.32l-8.09,8.09l-8.09-8.09l8.09-8.09L72.03,82.32z M52.99,79.47l-8.09-8.09
|
||||
l8.09-8.09l8.09,8.09L52.99,79.47z M50.13,82.32l-8.09,8.09l-8.09-8.09l8.09-8.09L50.13,82.32z M31.1,79.47l-3.72-3.72l-1.33-7.4
|
||||
l5.05-5.05l8.09,8.09L31.1,79.47z M31.1,85.18l8.09,8.09l-7.35,7.35L29.38,86.9L31.1,85.18z M102.85,63.65l-3.22-3.22l4.67-4.67
|
||||
L102.85,63.65z M96.78,41.4l8.09,8.09l-8.09,8.09l-8.09-8.09L96.78,41.4z M85.83,52.34l8.09,8.09l-8.09,8.09l-8.09-8.09
|
||||
L85.83,52.34z M74.88,41.4l8.09,8.09l-8.09,8.09l-8.09-8.09L74.88,41.4z M72.03,60.43l-8.09,8.09l-8.09-8.09l8.09-8.09L72.03,60.43
|
||||
z M52.99,41.4l8.09,8.09l-8.09,8.09l-8.09-8.09L52.99,41.4z M50.13,60.43l-8.09,8.09l-8.09-8.09l8.09-8.09L50.13,60.43z M31.1,41.4
|
||||
l8.09,8.09l-8.09,8.09l-8.09-8.09L31.1,41.4z M28.24,60.43l-3.06,3.06l-1.34-7.47L28.24,60.43z"/>
|
||||
|
||||
<radialGradient id="SVGID_2_" cx="65.5303" cy="12.9983" r="52.279" gradientTransform="matrix(1 0 0 0.4505 0 7.1421)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.7216" style="stop-color:#94D1E0"/>
|
||||
<stop offset="1" style="stop-color:#94D1E0;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
<path style="fill:url(#SVGID_2_);" d="M107.47,24.48l-8.06-8.06l2.29-2.29c-1.08-0.97-3.87-1.84-3.87-1.84l-1.27,1.27l-2.07-2.07
|
||||
c-4.25-1.51-7.07-1.35-7.07-1.35l6.28,6.28l-8.09,8.09l-8.09-8.09l6.66-6.66c-2.61-0.8-5.06-0.66-5.06-0.66l-4.46,4.46L69.5,8.41
|
||||
l-5.57,0.15l7.86,7.86l-8.09,8.09l-8.09-8.09l7.88-7.88l-5.94,0.22l-4.8,4.8l-4.72-4.72L43,9.51l6.91,6.91l-8.09,8.09l-8.09-8.09
|
||||
l6.31-6.31c0,0-5.64,0.76-7.28,1.56l-1.89,1.89l-1.18-1.18c0,0-2.25,0.34-4.09,1.63l2.41,2.41l-7.24,7.24c0,0,0.42,1.65,2.81,2.9
|
||||
l7.29-7.29l8.09,8.09l-4.22,4.22c0,0,2.74,1.55,4.75,0.97l2.33-2.33l5.87,5.87l9.87,0.29l6.15-6.15l5.98,5.98l10.29-0.36l5.62-5.62
|
||||
l2.5,2.5c2.67,0.26,4.81-0.9,4.81-0.9l-4.45-4.45l8.09-8.09l8.09,8.09C104.64,27.37,107.12,25.86,107.47,24.48z M52.77,35.46
|
||||
l-8.09-8.09l8.09-8.09l8.09,8.09L52.77,35.46z M74.66,35.46l-8.09-8.09l8.09-8.09l8.09,8.09L74.66,35.46z"/>
|
||||
<path style="fill:#84B0C1;" d="M64,4C34.17,4,9.99,9.9,9.99,22.74c0,10.24,24.18,18.74,54.01,18.74c29.83,0,54.01-8.5,54.01-18.74
|
||||
C118.01,11.29,93.83,4,64,4z M64,34.36c-24.01,0-43.47-5.98-43.47-13.35c0-7.37,19.46-11.69,43.47-11.69
|
||||
c24.01,0,43.47,4.32,43.47,11.69C107.47,28.38,88.01,34.36,64,34.36z"/>
|
||||
<path style="fill:#A8E3F0;" d="M107.47,15.75c2.07,1.65,3.91,4.42,1.7,6.98c-1.95,2.26-1.41,2.81-0.24,2.51
|
||||
c2.2-0.56,5.84-3.03,4.61-7.19c-1.25-4.2-8.44-7-13.26-7.99c-1.31-0.27-3.5-0.56-3.89,0C96.01,10.63,102.77,12,107.47,15.75z"/>
|
||||
<g>
|
||||
<path style="fill:#A8E3F0;" d="M37.24,35.27c-4.64-0.47-16.02-1.62-22.14-9.69c-2.24-2.96-2.06-7.28,0.44-9.75
|
||||
c4.34-4.27,10.01-4.41,8.72-3.62c-3.45,2.11-10.3,5.44-4.58,12.31c5.85,7.03,20.26,8.86,22.61,9.22S44.76,36.02,37.24,35.27z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]
|
||||
})
|
||||
|
|
|
@ -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<boolean> = ref(false)
|
||||
const user: Ref<User | null> = ref(null)
|
||||
const token: Ref<string | null> = ref(null)
|
||||
const user: Ref<User> = ref(emptyUser())
|
||||
const token: Ref<string> = 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}
|
||||
|
|
|
@ -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}%)`;
|
||||
}
|
|
@ -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<NoteList[]> = ref([])
|
||||
|
||||
const creatingNewList: Ref<boolean> = 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)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>
|
||||
Lists
|
||||
</h1>
|
||||
<p>
|
||||
Here are your lists!
|
||||
</p>
|
||||
<div v-for="list in noteLists" :key="list.id">
|
||||
<header>
|
||||
<h1>Lists</h1>
|
||||
<div>
|
||||
<button @click="toggleCreatingNewList()">
|
||||
Create New List
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form v-if="creatingNewList" class="new-list-form" @submit.prevent="createList()">
|
||||
<div class="form-row">
|
||||
<label for="list-name">Name</label>
|
||||
<input type="text" id="list-name" required minlength="3" v-model="newListModel.name"/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="list-description">Description</label>
|
||||
<input type="text" id="list-description" v-model="newListModel.description"/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<button type="submit">Create List</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div
|
||||
class="note-list-item"
|
||||
v-for="list in noteLists"
|
||||
:key="list.id"
|
||||
@click="goToList(list.id)"
|
||||
:style="{'background-color': stringToColor(list.name, 100, 90)}"
|
||||
>
|
||||
<h3 v-text="list.name"></h3>
|
||||
<ul>
|
||||
<li v-for="note in list.notes" :key="note.id" v-text="note.content"></li>
|
||||
</ul>
|
||||
<p v-text="list.description"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
header {
|
||||
max-width: 50ch;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.note-list-item {
|
||||
display: block;
|
||||
max-width: 50ch;
|
||||
margin: 1rem auto;
|
||||
padding: 1rem;
|
||||
border-radius: 1rem;
|
||||
border: 2px solid black;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-list-item:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.note-list-item h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.note-list-item p {
|
||||
margin: 0.5rem 0;
|
||||
font-style: italic;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.new-list-form {
|
||||
max-width: 50ch;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
|
@ -29,20 +29,66 @@ async function doLogin() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent="doLogin" @reset="resetLogin">
|
||||
<h1>LiteList</h1>
|
||||
<label>
|
||||
Username:
|
||||
<input type="text" name="username" required v-model="loginModel.username"/>
|
||||
</label>
|
||||
<label>
|
||||
Password:
|
||||
<input type="password" name="password" required v-model="loginModel.password"/>
|
||||
</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
<form @submit.prevent="doLogin" @reset="resetLogin">
|
||||
<div class="form-row">
|
||||
<label for="username-input">Username</label>
|
||||
<input
|
||||
id="username-input"
|
||||
type="text"
|
||||
name="username"
|
||||
required
|
||||
v-model="loginModel.username"
|
||||
minlength="3"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="password-input">Password</label>
|
||||
<input
|
||||
id="password-input"
|
||||
type="password"
|
||||
name="password"
|
||||
required
|
||||
v-model="loginModel.password"
|
||||
minlength="8"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<button type="submit">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
form {
|
||||
max-width: 50ch;
|
||||
margin: 0 auto;
|
||||
background-color: #efefef;
|
||||
padding: 1rem;
|
||||
border: 3px solid black;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-row input {
|
||||
width: 75%;
|
||||
padding: 0.5rem;
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.form-row button {
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,164 @@
|
|||
<script setup lang="ts">
|
||||
import type {NoteList} from "@/api/lists";
|
||||
import {onMounted, ref, type Ref} from "vue";
|
||||
import {useAuthStore} from "@/stores/auth";
|
||||
import {createNote, deleteNote, deleteNoteList, getNoteList} from "@/api/lists";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const list: Ref<NoteList | null> = ref(null)
|
||||
|
||||
const creatingNote: Ref<boolean> = ref(false)
|
||||
const newNoteText: Ref<string> = ref("")
|
||||
|
||||
onMounted(async () => {
|
||||
const listId = parseInt(route.params.id)
|
||||
// If no valid list id could be found, go back.
|
||||
if (!listId) {
|
||||
await router.push("/lists")
|
||||
return
|
||||
}
|
||||
list.value = await getNoteList(authStore.token, listId)
|
||||
// If no such list could be found, go back to the page showing all lists.
|
||||
if (list.value === null) {
|
||||
await router.push("/lists")
|
||||
}
|
||||
})
|
||||
|
||||
async function deleteNoteAndRefresh(id: number) {
|
||||
if (!list.value) return
|
||||
await deleteNote(authStore.token, list.value.id, id)
|
||||
list.value = await getNoteList(authStore.token, list.value.id)
|
||||
}
|
||||
|
||||
async function deleteList(id: number) {
|
||||
const dialog: HTMLDialogElement = document.getElementById("list-delete-dialog")
|
||||
dialog.showModal()
|
||||
const confirmButton: HTMLButtonElement = document.getElementById("delete-confirm-button")
|
||||
confirmButton.onclick = async () => {
|
||||
dialog.close()
|
||||
await deleteNoteList(authStore.token, id)
|
||||
await router.push("/lists")
|
||||
}
|
||||
const cancelButton: HTMLButtonElement = document.getElementById("delete-cancel-button")
|
||||
cancelButton.onclick = async () => {
|
||||
dialog.close()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCreatingNewNote() {
|
||||
if (!creatingNote.value) {
|
||||
newNoteText.value = ""
|
||||
}
|
||||
creatingNote.value = !creatingNote.value
|
||||
}
|
||||
|
||||
async function createNoteAndRefresh() {
|
||||
if (!list.value) return
|
||||
await createNote(authStore.token, list.value.id, newNoteText.value)
|
||||
creatingNote.value = false
|
||||
newNoteText.value = ""
|
||||
list.value = await getNoteList(authStore.token, list.value.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="list">
|
||||
<header>
|
||||
<h1 v-text="list.name"></h1>
|
||||
<p><em v-text="list.description"></em></p>
|
||||
<div class="buttons-list">
|
||||
<button @click="toggleCreatingNewNote()">Add Note</button>
|
||||
<button @click="deleteList(list.id)">
|
||||
Delete this List
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form v-if="creatingNote" @submit.prevent="createNoteAndRefresh()" class="new-note-form">
|
||||
<div class="form-row">
|
||||
<label for="note-content">Text</label>
|
||||
<input type="text" id="note-content" required minlength="1" v-model="newNoteText"/>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<button type="submit">Add</button>
|
||||
<button @click="toggleCreatingNewNote()">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="note-item" v-for="note in list.notes" :key="note.id">
|
||||
<p class="note-item-text" v-text="note.content"></p>
|
||||
<img
|
||||
class="trash-button"
|
||||
alt="Delete button"
|
||||
src="@/assets/trash-emoji.svg"
|
||||
@click="deleteNoteAndRefresh(note.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="list-delete-dialog">
|
||||
<form method="dialog">
|
||||
<p>
|
||||
Are you sure you want to delete this list? All notes in it will be deleted.
|
||||
</p>
|
||||
<div>
|
||||
<button id="delete-cancel-button" value="cancel" formmethod="dialog">Cancel</button>
|
||||
<button id="delete-confirm-button" value="default">Confirm</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
header {
|
||||
max-width: 50ch;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.buttons-list button {
|
||||
margin-right: 1rem;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
max-width: 50ch;
|
||||
margin: 1rem auto;
|
||||
border-bottom: 1px solid black;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.note-item-text {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.trash-button {
|
||||
width: 32px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.trash-button:hover {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.new-note-form {
|
||||
max-width: 50ch;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue