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() {
|
void main() {
|
||||||
auto provider = new shared DefaultProvider(true, Levels.INFO);
|
auto provider = new shared DefaultProvider(true, Levels.INFO);
|
||||||
|
provider.getLoggerFactory().setModuleLevelPrefix("handy_httpd", Levels.WARN);
|
||||||
configureLoggingProvider(provider);
|
configureLoggingProvider(provider);
|
||||||
|
|
||||||
HttpServer server = initServer();
|
HttpServer server = initServer();
|
||||||
|
@ -23,6 +24,7 @@ private HttpServer initServer() {
|
||||||
config.connectionQueueSize = 10;
|
config.connectionQueueSize = 10;
|
||||||
config.defaultHeaders["Access-Control-Allow-Origin"] = "*";
|
config.defaultHeaders["Access-Control-Allow-Origin"] = "*";
|
||||||
config.defaultHeaders["Access-Control-Allow-Credentials"] = "true";
|
config.defaultHeaders["Access-Control-Allow-Credentials"] = "true";
|
||||||
|
config.defaultHeaders["Access-Control-Allow-Methods"] = "*";
|
||||||
config.defaultHeaders["Vary"] = "origin";
|
config.defaultHeaders["Vary"] = "origin";
|
||||||
config.defaultHeaders["Access-Control-Allow-Headers"] = "Authorization";
|
config.defaultHeaders["Access-Control-Allow-Headers"] = "Authorization";
|
||||||
|
|
||||||
|
@ -44,7 +46,10 @@ private HttpServer initServer() {
|
||||||
|
|
||||||
mainHandler.addMapping(Method.GET, "/lists", &getNoteLists);
|
mainHandler.addMapping(Method.GET, "/lists", &getNoteLists);
|
||||||
mainHandler.addMapping(Method.POST, "/lists", &createNoteList);
|
mainHandler.addMapping(Method.POST, "/lists", &createNoteList);
|
||||||
|
mainHandler.addMapping(Method.GET, "/lists/{id}", &getNoteList);
|
||||||
mainHandler.addMapping(Method.DELETE, "/lists/{id}", &deleteNoteList);
|
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);
|
return new HttpServer(mainHandler, config);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,9 @@ static UserDataSource userDataSource;
|
||||||
|
|
||||||
static this() {
|
static this() {
|
||||||
userDataSource = new FsSqliteDataSource();
|
userDataSource = new FsSqliteDataSource();
|
||||||
|
import slf4d;
|
||||||
|
import d2sqlite3.library;
|
||||||
|
infoF!"Sqlite version %s"(versionString());
|
||||||
}
|
}
|
||||||
|
|
||||||
struct User {
|
struct User {
|
||||||
|
@ -42,9 +45,12 @@ interface UserDataSource {
|
||||||
void deleteUser(string username);
|
void deleteUser(string username);
|
||||||
Nullable!User getUser(string username);
|
Nullable!User getUser(string username);
|
||||||
NoteList[] getLists(string username);
|
NoteList[] getLists(string username);
|
||||||
|
Nullable!NoteList getList(string username, ulong id);
|
||||||
NoteList createNoteList(string username, string name, string description = null);
|
NoteList createNoteList(string username, string name, string description = null);
|
||||||
void deleteNoteList(string username, ulong id);
|
void deleteNoteList(string username, ulong id);
|
||||||
|
NoteList updateNoteList(string username, ulong id, NoteList newData);
|
||||||
Note createNote(string username, ulong noteListId, string content);
|
Note createNote(string username, ulong noteListId, string content);
|
||||||
|
Note updateNote(string username, ulong id, Note newData);
|
||||||
void deleteNote(string username, ulong id);
|
void deleteNote(string username, ulong id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,13 +113,26 @@ class FsSqliteDataSource : UserDataSource {
|
||||||
return lists;
|
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) {
|
NoteList createNoteList(string username, string name, string description = null) {
|
||||||
Database db = getDb(username);
|
Database db = getDb(username);
|
||||||
|
|
||||||
Statement existsStatement = db.prepare("SELECT COUNT(name) FROM note_list WHERE name = ?");
|
Statement existsStatement = db.prepare("SELECT COUNT(name) FROM note_list WHERE name = ?");
|
||||||
existsStatement.bind(1, name);
|
existsStatement.bind(1, name);
|
||||||
ResultRange existsResult = existsStatement.execute();
|
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);
|
Nullable!uint ordResult = db.execute("SELECT MAX(ordinality) + 1 FROM note_list").oneValue!(Nullable!uint);
|
||||||
uint ordinality = 0;
|
uint ordinality = 0;
|
||||||
|
@ -128,12 +147,47 @@ class FsSqliteDataSource : UserDataSource {
|
||||||
|
|
||||||
void deleteNoteList(string username, ulong id) {
|
void deleteNoteList(string username, ulong id) {
|
||||||
Database db = getDb(username);
|
Database db = getDb(username);
|
||||||
Statement stmt1 = db.prepare("DELETE FROM note WHERE note_list_id = ?");
|
db.begin();
|
||||||
stmt1.bind(1, id);
|
db.execute("DELETE FROM note WHERE note_list_id = ?", id);
|
||||||
stmt1.execute();
|
Nullable!uint ordinality = db.execute(
|
||||||
Statement stmt2 = db.prepare("DELETE FROM note_list WHERE id = ?");
|
"SELECT ordinality FROM note_list WHERE id = ?", id
|
||||||
stmt2.bind(1, id);
|
).oneValue!(Nullable!uint)();
|
||||||
stmt2.execute();
|
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) {
|
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) {
|
void deleteNote(string username, ulong id) {
|
||||||
Database db = getDb(username);
|
Database db = getDb(username);
|
||||||
Statement stmt = db.prepare("DELETE FROM note WHERE id = ?");
|
db.begin();
|
||||||
stmt.bind(1, id);
|
Nullable!uint ordinality = db.execute(
|
||||||
stmt.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) {
|
private NoteList parseNoteList(Row row) {
|
||||||
|
|
|
@ -2,6 +2,8 @@ module lists;
|
||||||
|
|
||||||
import handy_httpd;
|
import handy_httpd;
|
||||||
import std.json;
|
import std.json;
|
||||||
|
import std.typecons;
|
||||||
|
import std.string;
|
||||||
|
|
||||||
import auth;
|
import auth;
|
||||||
import data;
|
import data;
|
||||||
|
@ -17,22 +19,68 @@ void getNoteLists(ref HttpRequestContext ctx) {
|
||||||
ctx.response.writeBodyString(listsArray.toString(), "application/json");
|
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) {
|
void createNoteList(ref HttpRequestContext ctx) {
|
||||||
if (!validateAuthenticatedRequest(ctx)) return;
|
if (!validateAuthenticatedRequest(ctx)) return;
|
||||||
AuthContext auth = AuthContextHolder.getOrThrow();
|
AuthContext auth = AuthContextHolder.getOrThrow();
|
||||||
JSONValue requestBody = ctx.request.readBodyAsJson();
|
JSONValue requestBody = ctx.request.readBodyAsJson();
|
||||||
string listName = requestBody.object["name"].str;
|
if ("name" !in requestBody.object) {
|
||||||
string description = requestBody.object["description"].str;
|
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);
|
NoteList list = userDataSource.createNoteList(auth.user.username, listName, description);
|
||||||
ctx.response.writeBodyString(serializeList(list).toString(), "application/json");
|
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) {
|
void deleteNoteList(ref HttpRequestContext ctx) {
|
||||||
if (!validateAuthenticatedRequest(ctx)) return;
|
if (!validateAuthenticatedRequest(ctx)) return;
|
||||||
AuthContext auth = AuthContextHolder.getOrThrow();
|
AuthContext auth = AuthContextHolder.getOrThrow();
|
||||||
userDataSource.deleteNoteList(auth.user.username, ctx.request.getPathParamAs!ulong("id"));
|
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) {
|
private JSONValue serializeList(NoteList list) {
|
||||||
JSONValue listObj = JSONValue(string[string].init);
|
JSONValue listObj = JSONValue(string[string].init);
|
||||||
listObj.object["id"] = JSONValue(list.id);
|
listObj.object["id"] = JSONValue(list.id);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { RouterView } from 'vue-router'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -5,6 +5,10 @@ export interface User {
|
||||||
email: string
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function emptyUser(): User {
|
||||||
|
return {username: "", email: ""}
|
||||||
|
}
|
||||||
|
|
||||||
export interface LoginInfo {
|
export interface LoginInfo {
|
||||||
user: User
|
user: User
|
||||||
token: string
|
token: string
|
||||||
|
|
|
@ -4,16 +4,30 @@ export interface Note {
|
||||||
id: number
|
id: number
|
||||||
ordinality: number
|
ordinality: number
|
||||||
content: string
|
content: string
|
||||||
noteListName: string
|
noteListId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NoteList {
|
export interface NoteList {
|
||||||
|
id: number
|
||||||
name: string
|
name: string
|
||||||
ordinality: number
|
ordinality: number
|
||||||
description: string
|
description: string
|
||||||
notes: Note[]
|
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[]> {
|
export async function getNoteLists(token: string): Promise<NoteList[]> {
|
||||||
const response = await fetch(API_URL + "/lists", {
|
const response = await fetch(API_URL + "/lists", {
|
||||||
headers: {"Authorization": "Bearer " + token}
|
headers: {"Authorization": "Bearer " + token}
|
||||||
|
@ -25,3 +39,41 @@ export async function getNoteLists(token: string): Promise<NoteList[]> {
|
||||||
return []
|
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 { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import "./assets/base.css"
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,12 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import LoginView from "@/views/LoginView.vue";
|
import LoginView from "@/views/LoginView.vue";
|
||||||
import ListsView from "@/views/ListsView.vue";
|
import ListsView from "@/views/ListsView.vue";
|
||||||
import {useAuthStore} from "@/stores/auth";
|
import {useAuthStore} from "@/stores/auth";
|
||||||
|
import SingleListView from "@/views/SingleListView.vue";
|
||||||
|
|
||||||
|
function checkAuth() {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
if (!authStore.authenticated) return "login"
|
||||||
|
}
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
@ -10,22 +16,22 @@ const router = createRouter({
|
||||||
path: "/",
|
path: "/",
|
||||||
name: "home-redirect",
|
name: "home-redirect",
|
||||||
redirect: to => {
|
redirect: to => {
|
||||||
return "login"
|
return "/login"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
name: "login",
|
|
||||||
component: LoginView
|
component: LoginView
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/lists",
|
path: "/lists",
|
||||||
name: "lists",
|
|
||||||
component: ListsView,
|
component: ListsView,
|
||||||
beforeEnter: (to, from) => {
|
beforeEnter: checkAuth
|
||||||
const authStore = useAuthStore()
|
},
|
||||||
if (!authStore.authenticated) return "login"
|
{
|
||||||
}
|
path: "/lists/:id",
|
||||||
|
component: SingleListView,
|
||||||
|
beforeEnter: checkAuth
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import {defineStore} from "pinia";
|
import {defineStore} from "pinia";
|
||||||
import {type Ref, ref} from "vue";
|
import {type Ref, ref} from "vue";
|
||||||
import type {User} from "@/api/auth";
|
import type {User} from "@/api/auth";
|
||||||
|
import {emptyUser} from "@/api/auth";
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", () => {
|
export const useAuthStore = defineStore("auth", () => {
|
||||||
const authenticated: Ref<boolean> = ref(false)
|
const authenticated: Ref<boolean> = ref(false)
|
||||||
const user: Ref<User | null> = ref(null)
|
const user: Ref<User> = ref(emptyUser())
|
||||||
const token: Ref<string | null> = ref(null)
|
const token: Ref<string> = ref("")
|
||||||
|
|
||||||
function logIn(newToken: string, newUser: User) {
|
function logIn(newToken: string, newUser: User) {
|
||||||
authenticated.value = true
|
authenticated.value = true
|
||||||
|
@ -15,8 +16,8 @@ export const useAuthStore = defineStore("auth", () => {
|
||||||
|
|
||||||
function logOut() {
|
function logOut() {
|
||||||
authenticated.value = false
|
authenticated.value = false
|
||||||
user.value = null
|
user.value = emptyUser()
|
||||||
token.value = null
|
token.value = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return {authenticated, user, token, logIn, logOut}
|
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 {useAuthStore} from "@/stores/auth";
|
||||||
import {onMounted, ref, type Ref} from "vue";
|
import {onMounted, ref, type Ref} from "vue";
|
||||||
import type {NoteList} from "@/api/lists";
|
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 authStore = useAuthStore()
|
||||||
|
const router = useRouter()
|
||||||
const noteLists: Ref<NoteList[]> = ref([])
|
const noteLists: Ref<NoteList[]> = ref([])
|
||||||
|
|
||||||
|
const creatingNewList: Ref<boolean> = ref(false)
|
||||||
|
const newListModel = ref({
|
||||||
|
name: "",
|
||||||
|
description: ""
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
noteLists.value = await getNoteLists(authStore.token)
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<h1>
|
<header>
|
||||||
Lists
|
<h1>Lists</h1>
|
||||||
</h1>
|
<div>
|
||||||
<p>
|
<button @click="toggleCreatingNewList()">
|
||||||
Here are your lists!
|
Create New List
|
||||||
</p>
|
</button>
|
||||||
<div v-for="list in noteLists" :key="list.id">
|
</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>
|
<h3 v-text="list.name"></h3>
|
||||||
<ul>
|
<p v-text="list.description"></p>
|
||||||
<li v-for="note in list.notes" :key="note.id" v-text="note.content"></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
|
@ -29,20 +29,66 @@ async function doLogin() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="doLogin" @reset="resetLogin">
|
|
||||||
<h1>LiteList</h1>
|
<h1>LiteList</h1>
|
||||||
<label>
|
<form @submit.prevent="doLogin" @reset="resetLogin">
|
||||||
Username:
|
<div class="form-row">
|
||||||
<input type="text" name="username" required v-model="loginModel.username"/>
|
<label for="username-input">Username</label>
|
||||||
</label>
|
<input
|
||||||
<label>
|
id="username-input"
|
||||||
Password:
|
type="text"
|
||||||
<input type="password" name="password" required v-model="loginModel.password"/>
|
name="username"
|
||||||
</label>
|
required
|
||||||
<button type="submit">Submit</button>
|
v-model="loginModel.username"
|
||||||
</form>
|
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>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</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