Compare commits
74 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
809e7ce6d7 | ||
|
|
576e65dca6 | ||
|
|
e0505755da | ||
|
|
4fe86d33b6 | ||
|
|
3cd93ac7da | ||
|
|
7c4ed1d2b2 | ||
|
|
67395c9ebf | ||
|
|
3ef834e103 | ||
|
|
cf9da4c6a0 | ||
|
|
58f2a53539 | ||
|
|
46e891d429 | ||
|
|
01d9377238 | ||
|
|
7819b461b8 | ||
|
|
f0e2c9ca94 | ||
|
|
ad94658aef | ||
|
|
1b52eaac46 | ||
|
|
e03b58057e | ||
|
|
1ffd08082b | ||
|
|
942ee3805d | ||
|
|
75034e0602 | ||
|
|
5bd335dbd6 | ||
|
|
ffce155154 | ||
|
|
245de43c78 | ||
|
|
bb9ee26bd2 | ||
|
|
158d49d221 | ||
|
|
fcd2f16401 | ||
|
|
f1331157fd | ||
|
|
f021209531 | ||
|
|
d5867c4f0d | ||
|
|
b8c3603cbb | ||
|
|
dbde003fc2 | ||
|
|
239c73fb8e | ||
|
|
9589a21f96 | ||
|
|
c1c6804a63 | ||
|
|
08166dda95 | ||
|
|
2e3af63c72 | ||
|
|
d438bc4b14 | ||
|
|
00f74d4228 | ||
|
|
2d06680bab | ||
|
|
39d8a2034f | ||
|
|
8d63c8fd4e | ||
|
|
63e37061de | ||
|
|
319828b8b1 | ||
|
|
952917d415 | ||
|
|
c85fbe915f | ||
|
|
4091f6c8f5 | ||
|
|
e9c21409cd | ||
|
|
7068ae402e | ||
|
|
a9bebd789e | ||
|
|
fa94a99567 | ||
|
|
180a26344a | ||
|
|
f7686371c5 | ||
|
|
51735f79dc | ||
|
|
53854b23cd | ||
|
|
fa91d5f670 | ||
|
|
ad23d05000 | ||
|
|
154d496208 | ||
|
|
36d373cdc3 | ||
|
|
f266f3579c | ||
|
|
6fd7726ae0 | ||
|
|
bee5b04441 | ||
|
|
cdefc2e993 | ||
|
|
7abdbe605b | ||
|
|
2d193969f7 | ||
|
|
22396d1a00 | ||
|
|
031a4e5259 | ||
|
|
03b1695d9a | ||
|
|
03e75ffe2a | ||
|
|
93f9979259 | ||
|
|
4125b433e5 | ||
|
|
d66b9272b7 | ||
|
|
07e014cd8f | ||
|
|
7a5ba150f7 | ||
|
|
2eeda6fc5c |
|
|
@ -1,4 +1,13 @@
|
||||||
# 🚀 Getting started with PomeloNote
|
# 🚀 Getting started with PomeloNote
|
||||||
|
### **THIS REPOSITORY HAS DEPENDENCIES WITH SECURITY VULNERABILITIES. YOU MIGHT WANT TO UPDATE PACKAGES BEFORE USE.**
|
||||||
|
## Setup
|
||||||
|
- run `npm i`
|
||||||
|
- get the .env file and save it to the root directory of the project
|
||||||
|
- set up Strapi
|
||||||
|
- go to `localhost:1337/admin`
|
||||||
|
- register an admin user
|
||||||
|
- go to Settings => Users&Permissions Plugin => Roles => Authenticated => Note => Select all
|
||||||
|
- Save
|
||||||
|
|
||||||
### Starting the container with svelte and strapi:
|
### Starting the container with svelte and strapi:
|
||||||
``docker-compose up --build -d``
|
``docker-compose up --build -d``
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ module.exports = createCoreController(noteUid, ({strapi}) => ({
|
||||||
lastViewed: Date.now()
|
lastViewed: Date.now()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
entry = await strapi.entityService.findOne(noteUid, noteId, {
|
||||||
|
populate: ['owners'],
|
||||||
|
});
|
||||||
return JSON.stringify(entry);
|
return JSON.stringify(entry);
|
||||||
} else {
|
} else {
|
||||||
ctx.response.status = 403;
|
ctx.response.status = 403;
|
||||||
|
|
@ -63,22 +66,26 @@ module.exports = createCoreController(noteUid, ({strapi}) => ({
|
||||||
async update(ctx) {
|
async update(ctx) {
|
||||||
const noteId = getNoteIdFromUrl(ctx.request.url)
|
const noteId = getNoteIdFromUrl(ctx.request.url)
|
||||||
const userId = ctx.state.user.id;
|
const userId = ctx.state.user.id;
|
||||||
const requestBody = ctx.request.body;
|
const requestBody = JSON.parse(ctx.request.body);
|
||||||
|
console.log(JSON.stringify(requestBody, null, 2))
|
||||||
const entry = await strapi.entityService.findOne(noteUid, noteId, {
|
const entry = await strapi.entityService.findOne(noteUid, noteId, {
|
||||||
populate: ['owners'],
|
populate: ['owners'],
|
||||||
});
|
});
|
||||||
const authorized = entry.owners.some(owner => owner.id === userId)
|
const authorized = entry.owners.some(owner => owner.id === userId)
|
||||||
let allPreviousOwnersKept = false;
|
let allPreviousOwnersKept = true;
|
||||||
if (requestBody.data.hasOwnProperty("owners")) {
|
if (requestBody.data.hasOwnProperty("owners")) {
|
||||||
allPreviousOwnersKept = entry.owners.every(owner => requestBody.data.owners.includes(owner));
|
allPreviousOwnersKept = entry.owners.every(owner => requestBody.data.owners.includes(owner));
|
||||||
}
|
}
|
||||||
|
console.log({
|
||||||
|
"auth": authorized,
|
||||||
|
"allprev": allPreviousOwnersKept,
|
||||||
|
})
|
||||||
if (!authorized) {
|
if (!authorized) {
|
||||||
ctx.response.status = 403;
|
ctx.response.status = 403;
|
||||||
} else if (!allPreviousOwnersKept) {
|
} else if (!allPreviousOwnersKept) {
|
||||||
ctx.response.status = 400;
|
ctx.response.status = 400;
|
||||||
} else {
|
|
||||||
return super.update(ctx);
|
|
||||||
}
|
}
|
||||||
|
return await strapi.entityService.update(noteUid, noteId, requestBody);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Creates a new note, automatically sets owners to the user making the request and lastViewed
|
* Creates a new note, automatically sets owners to the user making the request and lastViewed
|
||||||
|
|
@ -87,7 +94,8 @@ module.exports = createCoreController(noteUid, ({strapi}) => ({
|
||||||
*/
|
*/
|
||||||
async create(ctx) {
|
async create(ctx) {
|
||||||
const userId = ctx.state.user.id;
|
const userId = ctx.state.user.id;
|
||||||
const requestBody = ctx.request.body;
|
const requestBody = JSON.parse(ctx.request.body);
|
||||||
|
console.log(requestBody);
|
||||||
const response = await strapi.entityService.create(noteUid, {
|
const response = await strapi.entityService.create(noteUid, {
|
||||||
data: {
|
data: {
|
||||||
title: requestBody.data.title,
|
title: requestBody.data.title,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "up_users",
|
||||||
|
"info": {
|
||||||
|
"name": "user",
|
||||||
|
"description": "",
|
||||||
|
"singularName": "user",
|
||||||
|
"pluralName": "users",
|
||||||
|
"displayName": "User"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": false,
|
||||||
|
"timestamps": true
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"username": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"unique": true,
|
||||||
|
"configurable": false,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"type": "email",
|
||||||
|
"minLength": 6,
|
||||||
|
"configurable": false,
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
"provider": {
|
||||||
|
"type": "string",
|
||||||
|
"configurable": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"type": "password",
|
||||||
|
"minLength": 6,
|
||||||
|
"configurable": false,
|
||||||
|
"private": true
|
||||||
|
},
|
||||||
|
"resetPasswordToken": {
|
||||||
|
"type": "string",
|
||||||
|
"configurable": false,
|
||||||
|
"private": true
|
||||||
|
},
|
||||||
|
"confirmationToken": {
|
||||||
|
"type": "string",
|
||||||
|
"configurable": false,
|
||||||
|
"private": true
|
||||||
|
},
|
||||||
|
"confirmed": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"configurable": false
|
||||||
|
},
|
||||||
|
"blocked": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"configurable": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "manyToOne",
|
||||||
|
"target": "plugin::users-permissions.role",
|
||||||
|
"inversedBy": "users",
|
||||||
|
"configurable": false
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"type": "relation",
|
||||||
|
"relation": "manyToMany",
|
||||||
|
"target": "api::note.note",
|
||||||
|
"inversedBy": "owners"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
docs/_config.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
remote_theme: pages-themes/leap-day@v0.2.0
|
||||||
|
plugins:
|
||||||
|
- jekyll-remote-theme
|
||||||
4
docs/_data/devs.csv
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
name,github,image
|
||||||
|
Jonas Weissengruber,j-weissen,jowei
|
||||||
|
Stefan Prechtler,s-prechtl,stef
|
||||||
|
David Hain,d-hain,dave
|
||||||
|
BIN
docs/images/dave.jpg
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
docs/images/delete.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/images/editor.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/images/jowei.jpg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
docs/images/listing.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/images/login.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/images/register.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/images/stef.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
31
docs/index.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Pomelo Note
|
||||||
|
|
||||||
|
This is the best open source note app you will ever find.
|
||||||
|
|
||||||
|
## Login
|
||||||
|
When first entering the app, you will need to login. If you haven't got an account you may consider [registering](#register), or just not using the app at all.
|
||||||
|
<br /><img src="images/login.png" width="50%" style="margin: auto; display: block;" />
|
||||||
|
|
||||||
|
## Register
|
||||||
|
A username, an email and a password that's all you need. If you are missing one of those, just don't use the app at all.
|
||||||
|
<br /><img src="images/register.png" width="50%" style="margin: auto; display: block;" />
|
||||||
|
|
||||||
|
## Editor
|
||||||
|
You can edit your notes with our minimalistic editor interface.
|
||||||
|
<br /><img src="images/editor.png" width="50%" style="margin: auto; display: block;" />
|
||||||
|
|
||||||
|
## Listing
|
||||||
|
Here you can see all your notes. Click on them to open the editor or hover and press the red "X" to delete them.
|
||||||
|
<br /><img src="images/listing.png" width="50%" style="margin: auto; display: block;" />
|
||||||
|
|
||||||
|
## Delete
|
||||||
|
Confirm the deletion.
|
||||||
|
<br /><img src="images/delete.png" width="50%" style="margin: auto; display: block;" />
|
||||||
|
|
||||||
|
# The Team
|
||||||
|
{% for dev in site.data.devs %}
|
||||||
|
{{ dev.name }}
|
||||||
|
[GitHub](https://github.com/{{ dev.github }})
|
||||||
|

|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
27
frontend/svelte/package-lock.json
generated
|
|
@ -9,7 +9,9 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bootstrap-icons": "^1.9.1",
|
"bootstrap-icons": "^1.9.1",
|
||||||
"nookies": "^2.5.2"
|
"nookies": "^2.5.2",
|
||||||
|
"sv-popup": "^0.2.5",
|
||||||
|
"webworker": "^0.8.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "next",
|
"@sveltejs/adapter-auto": "next",
|
||||||
|
|
@ -2083,6 +2085,11 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/sv-popup": {
|
||||||
|
"version": "0.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sv-popup/-/sv-popup-0.2.5.tgz",
|
||||||
|
"integrity": "sha512-JhBu4piXaauamT4vMEcFCydvxJ8e72G7c9F3caZVAPsiFqWPTYT3JDz99FlR+YCnbOp1emsZqqOPVvCwHgURog=="
|
||||||
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "3.50.1",
|
"version": "3.50.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.50.1.tgz",
|
||||||
|
|
@ -2358,6 +2365,14 @@
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/webworker": {
|
||||||
|
"version": "0.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/webworker/-/webworker-0.8.4.tgz",
|
||||||
|
"integrity": "sha512-zzsVxtHf+mCn0WuYLarSWfRGmX7JiYKkKvso5FYC7rJ9G8svwGQA5a51Sjq9D2c/rKVU6U/kyBcaI7gUTVlsJg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
|
@ -3824,6 +3839,11 @@
|
||||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"sv-popup": {
|
||||||
|
"version": "0.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/sv-popup/-/sv-popup-0.2.5.tgz",
|
||||||
|
"integrity": "sha512-JhBu4piXaauamT4vMEcFCydvxJ8e72G7c9F3caZVAPsiFqWPTYT3JDz99FlR+YCnbOp1emsZqqOPVvCwHgURog=="
|
||||||
|
},
|
||||||
"svelte": {
|
"svelte": {
|
||||||
"version": "3.50.1",
|
"version": "3.50.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.50.1.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.50.1.tgz",
|
||||||
|
|
@ -3981,6 +4001,11 @@
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"webworker": {
|
||||||
|
"version": "0.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/webworker/-/webworker-0.8.4.tgz",
|
||||||
|
"integrity": "sha512-zzsVxtHf+mCn0WuYLarSWfRGmX7JiYKkKvso5FYC7rJ9G8svwGQA5a51Sjq9D2c/rKVU6U/kyBcaI7gUTVlsJg=="
|
||||||
|
},
|
||||||
"whatwg-url": {
|
"whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bootstrap-icons": "^1.9.1",
|
||||||
"nookies": "^2.5.2",
|
"nookies": "^2.5.2",
|
||||||
"bootstrap-icons": "^1.9.1"
|
"sv-popup": "^0.2.5",
|
||||||
|
"webworker": "^0.8.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en" data-theme="emerald">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="../src/resources/images/logo2.svg" />
|
<link rel="icon" href="../src/resources/images/logo2.svg" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="manifest" href="../static/manifest.json">
|
||||||
%sveltekit.head%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
29
frontend/svelte/src/customBootstrap.css
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
|
||||||
|
html,
|
||||||
|
:root {
|
||||||
|
--main-txt-color: black;
|
||||||
|
--cross-txt-color: red;
|
||||||
|
|
||||||
|
--color-primary: #fff494;
|
||||||
|
--color-primary-600: #fff17a;
|
||||||
|
--color-primary-700: #ffec47;
|
||||||
|
--color-primary-800: #ffe714;
|
||||||
|
--color-primary-900: #e0c900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--color-primary-800) !important;
|
||||||
|
border: var(--color-primary-800) !important;
|
||||||
|
color: var(--main-txt-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--color-primary-900) !important;
|
||||||
|
border: var(--color-primary-900) !important;
|
||||||
|
color: var(--main-txt-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:disabled {
|
||||||
|
background-color: var(--color-primary-700) !important;
|
||||||
|
border: var(--color-primary-700) !important;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import {parseCookies} from "nookies";
|
import type {Authentication} from "./authentication";
|
||||||
|
import {createErrorToast} from "./customToasts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capitalises first letter of string.
|
* Capitalises first letter of string.
|
||||||
|
|
@ -22,13 +23,14 @@ export async function bearerFetch(endpoint: string, jwt: string, baseUrl: string
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function handleErrorsFromResponseWithToast(response: Authentication) {
|
||||||
const getJwtCookie = () => {
|
if (response.error != null) {
|
||||||
// @ts-ignore
|
if (response.error.details.errors) {
|
||||||
return parseCookies("/").jwt;
|
for (const error of response.error.details.errors) {
|
||||||
};
|
createErrorToast(error.message);
|
||||||
|
}
|
||||||
/**
|
} else {
|
||||||
* JWT Cookie
|
createErrorToast(response.error.message);
|
||||||
*/
|
}
|
||||||
export const jwt: string = getJwtCookie();
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type {User} from "../../../models/user";
|
import type {User} from "./user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User Login Auth.
|
* User Login Auth.
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type {Note} from "./types";
|
import type {Note} from "../../types";
|
||||||
|
|
||||||
export interface NoteRepository {
|
export interface NoteRepository {
|
||||||
getNotes(): Promise<Note[]>;
|
getNotes(): Promise<Note[]>;
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
import type {Note} from "./types";
|
import type {Note} from "../../types";
|
||||||
import {parseCookies} from "nookies";
|
import {parseCookies} from "nookies";
|
||||||
import type {NoteRepository} from "./NoteRepository";
|
import type {NoteRepository} from "./NoteRepository";
|
||||||
|
import {currentNoteId} from "../../../stores";
|
||||||
|
|
||||||
|
|
||||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
|
|
||||||
|
|
@ -13,11 +15,21 @@ export class StrapiNoteRepository implements NoteRepository {
|
||||||
return this.instance;
|
return this.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {
|
||||||
|
currentNoteId.subscribe((value) => (this._currentNoteId = value));
|
||||||
|
}
|
||||||
|
|
||||||
private currentNoteId: number | undefined;
|
private _currentNoteId: unknown;
|
||||||
private static apiNoteEndpoint: string = "http://localhost:1337/api/notes"
|
private static apiNoteEndpoint: string = "http://localhost:1337/api/notes"
|
||||||
|
|
||||||
|
public set currentNoteId(value: number | undefined) {
|
||||||
|
currentNoteId.set(value || -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get currentNoteId(): number {
|
||||||
|
return <number>this._currentNoteId;
|
||||||
|
}
|
||||||
|
|
||||||
public async getNotes(): Promise<Note[]>{
|
public async getNotes(): Promise<Note[]>{
|
||||||
const response = await StrapiNoteRepository.fetchStrapiNoteEndpoint("/", 'GET');
|
const response = await StrapiNoteRepository.fetchStrapiNoteEndpoint("/", 'GET');
|
||||||
return await response.json();
|
return await response.json();
|
||||||
|
|
@ -29,7 +41,7 @@ export class StrapiNoteRepository implements NoteRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getCurrentNote(): Promise<Note | void> {
|
public async getCurrentNote(): Promise<Note | void> {
|
||||||
if (this.currentNoteId === null || this.currentNoteId === undefined) {
|
if (this._currentNoteId === null || this._currentNoteId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await this.getNote(this.currentNoteId);
|
return await this.getNote(this.currentNoteId);
|
||||||
|
|
@ -53,7 +65,7 @@ export class StrapiNoteRepository implements NoteRepository {
|
||||||
let requestInit: RequestInit = {
|
let requestInit: RequestInit = {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
authorization: StrapiNoteRepository.mockedGetAuthorizationHeader()
|
authorization: StrapiNoteRepository.getAuthorizationHeader()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (body) {
|
if (body) {
|
||||||
|
|
@ -66,8 +78,9 @@ export class StrapiNoteRepository implements NoteRepository {
|
||||||
return "bearer TOKEN"
|
return "bearer TOKEN"
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getAuthorizationHeader() {
|
static getAuthorizationHeader() {
|
||||||
const jwt = parseCookies().jwt;
|
// @ts-ignore
|
||||||
|
const jwt = parseCookies('/').jwt;
|
||||||
return `bearer ${jwt}`
|
return `bearer ${jwt}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
93
frontend/svelte/src/models/repos/user/StrapiUserRepo.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import type {UserRepository} from "./UserRepository";
|
||||||
|
import type {Authentication} from "../../authentication";
|
||||||
|
import type {HttpMethod} from "@sveltejs/kit/types/private";
|
||||||
|
import {StrapiNoteRepository} from "../note/StrapiNoteRepository";
|
||||||
|
import {error} from "@sveltejs/kit";
|
||||||
|
import {User} from "../../user";
|
||||||
|
|
||||||
|
export class StrapiUserRepo implements UserRepository {
|
||||||
|
private static instance: StrapiUserRepo;
|
||||||
|
|
||||||
|
public static getInstance(verification: boolean = true): StrapiUserRepo {
|
||||||
|
if (this.instance === undefined || this.instance === null) {
|
||||||
|
this.instance = new StrapiUserRepo();
|
||||||
|
this.instance.verify().then(() => {
|
||||||
|
if (verification && !this.instance.verified) {
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private verified: boolean = false;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static api: string = "http://localhost:1337/api"
|
||||||
|
|
||||||
|
private static apiUserEndpoint: string = StrapiUserRepo.api + "/auth/local"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the current users jwt.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async verify() {
|
||||||
|
this.verified = false;
|
||||||
|
let result = await this.getMe();
|
||||||
|
if (!result.error) {
|
||||||
|
this.verified = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMe(): Promise<Authentication> {
|
||||||
|
const response = await StrapiUserRepo.fetchStrapi("/me", "GET", null, true, "/users")
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerUser(email: string, username: string, password: string): Promise<Authentication> {
|
||||||
|
const payload = {
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
username: username
|
||||||
|
};
|
||||||
|
const response = await StrapiUserRepo.fetchStrapi("/register", "POST", payload, false);
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginUser(identifier: string, password: string): Promise<Authentication> {
|
||||||
|
const payload = {
|
||||||
|
identifier: identifier,
|
||||||
|
password: password
|
||||||
|
};
|
||||||
|
const response = await StrapiUserRepo.fetchStrapi("/", "POST", payload, false);
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async fetchStrapi(path: string, method: HttpMethod, body: any | null = null, authorization: boolean = true, customPath: any = null): Promise<Response> {
|
||||||
|
let requestInit: RequestInit = {
|
||||||
|
method: method,
|
||||||
|
};
|
||||||
|
if (authorization && body) {
|
||||||
|
requestInit["headers"] = {
|
||||||
|
authorization: StrapiNoteRepository.getAuthorizationHeader() ?? '',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
} else if (authorization) {
|
||||||
|
requestInit["headers"] = {
|
||||||
|
authorization: StrapiNoteRepository.getAuthorizationHeader() ?? '',
|
||||||
|
}
|
||||||
|
} else if (body) {
|
||||||
|
requestInit["headers"] = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (body) {
|
||||||
|
requestInit["body"] = JSON.stringify(body)
|
||||||
|
}
|
||||||
|
return await fetch((customPath) ? (this.api + customPath + path) : StrapiUserRepo.apiUserEndpoint + path, requestInit);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
frontend/svelte/src/models/repos/user/UserRepository.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import type {Authentication} from "../../authentication";
|
||||||
|
|
||||||
|
export interface UserRepository {
|
||||||
|
/**
|
||||||
|
* Registers a new user.
|
||||||
|
* @param email
|
||||||
|
* @param username
|
||||||
|
* @param password
|
||||||
|
*/
|
||||||
|
registerUser(email: string, username: string, password: string): Promise<Authentication>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current user.
|
||||||
|
* @param jwt
|
||||||
|
*/
|
||||||
|
getMe(jwt: string): Promise<Authentication>;
|
||||||
|
|
||||||
|
loginUser(identifier: string, password: string): Promise<Authentication>;
|
||||||
|
}
|
||||||
BIN
frontend/svelte/src/resources/icons/android-icon-144x144.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
frontend/svelte/src/resources/icons/android-icon-192x192.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
frontend/svelte/src/resources/icons/android-icon-36x36.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
frontend/svelte/src/resources/icons/android-icon-48x48.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
frontend/svelte/src/resources/icons/android-icon-72x72.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
frontend/svelte/src/resources/icons/android-icon-96x96.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -1,87 +1,48 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Note} from "../models/types";
|
import type {Note} from "../models/types";
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import {bearerFetch, jwt} from "../models/PomeloUtils";
|
import {StrapiNoteRepository} from "../models/repos/note/StrapiNoteRepository";
|
||||||
|
import {StrapiUserRepo} from "../models/repos/user/StrapiUserRepo";
|
||||||
const endpoint = "/notes";
|
import {Content, Modal, Trigger} from "sv-popup";
|
||||||
|
|
||||||
//
|
|
||||||
// //:TODO TEMP!!!
|
|
||||||
// const jsonStr = "[{\"id\":0,\"attributes\":{\"title\":\"mike\",\"content\":\"C Moasta\",\"lastViewed\":\"2022-09-27\"}},{\"id\":1,\"attributes\":{\"title\":\"samc\",\"content\":\"drupal gott\",\"lastViewed\":\"1999-09-09\"}},{\"id\":2,\"attributes\":{\"title\":\"DIO\",\"content\":\"in all CAPS\",\"lastViewed\":\"2022-09-27\"}},{\"id\":3,\"attributes\":{\"title\":\"Eren\",\"content\":\"Jäger\",\"lastViewed\":\"2022-09-27\"}},{\"id\":4,\"attributes\":{\"title\":\"stow\",\"content\":\"Beitn Chef\",\"lastViewed\":\"2022-09-27\"}},{\"id\":5,\"attributes\":{\"title\":\"Wonder of U\",\"content\":\"Umm... so, personally... this is the first time this has happened, so I'm a bit surprised. Only a centimeter away... I mean, I don't think there's ever been someone who's gotten that close to me... without a, you know... calamity occurring. I'm not really... not really sure what happens at one centimeter away... 'cause it's my first time. I don't really understand it either. Seriously. But in the flow of calamity... there's nobody who can attack me. Not a single person. That, I know for sure. Wonder of U.\",\"lastViewed\":\"2022-09-27\"}}]";
|
|
||||||
// //:TODO TEMP!!!
|
|
||||||
//
|
|
||||||
// let notes: Note[] = JSON.parse(jsonStr);
|
|
||||||
|
|
||||||
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||||
|
const noteRepo: StrapiNoteRepository = StrapiNoteRepository.getInstance();
|
||||||
let notes: Note[];
|
let notes: Note[];
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const response = await bearerFetch(endpoint, jwt);
|
StrapiUserRepo.getInstance();
|
||||||
let data = await response.json();
|
notes = await noteRepo.getNotes();
|
||||||
notes = data.data;
|
|
||||||
notes.forEach(note => {
|
notes.forEach(note => {
|
||||||
note.attributes.lastViewed = new Date(note.attributes.lastViewed);
|
note.lastViewed = new Date(note.lastViewed);
|
||||||
});
|
});
|
||||||
console.log(notes);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reloads the Notes Listing
|
* Adds a Note with the title "New Note" and redirects to editor
|
||||||
* (by doing something very intelligent)
|
|
||||||
*/
|
*/
|
||||||
function reloadNotesListing() {
|
async function addNoteHandler() {
|
||||||
notes = notes.filter(i => i === i);
|
const newTitle = "New Note";
|
||||||
|
const newNote = await addNote(newTitle);
|
||||||
|
noteRepo.currentNoteId = newNote.id;
|
||||||
|
window.location = "/editor";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives the user a prompt to input the new title of the note and creates it if the title is valid
|
* Adds a new note to the Database
|
||||||
*/
|
|
||||||
function addNotePrompt() {
|
|
||||||
let newTitle = prompt('Name of the new Note');
|
|
||||||
console.log(notes)
|
|
||||||
if (newTitle != null && newTitle != '') {
|
|
||||||
addNote(newTitle);
|
|
||||||
console.log(notes)
|
|
||||||
reloadNotesListing();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new note to the "notes" Array with:
|
|
||||||
* * the latest id + 1 (or 0 if no notes exist)
|
|
||||||
* * no content
|
|
||||||
* * the current date as the "lastViewed" property
|
|
||||||
* @param title The title of the new Note
|
* @param title The title of the new Note
|
||||||
|
* @return The created Note Object
|
||||||
*/
|
*/
|
||||||
function addNote(title: string) {
|
async function addNote(title: string): Promise<Note> {
|
||||||
const date = new Date();
|
return await noteRepo.createNote({title: title,});
|
||||||
const newNoteId: number = (notes.length == 0) ? 0 : notes[notes.length - 1].id + 1
|
|
||||||
const note: Note = {
|
|
||||||
id: newNoteId,
|
|
||||||
attributes: {
|
|
||||||
title: title,
|
|
||||||
content: "",
|
|
||||||
lastViewed: date
|
|
||||||
}
|
|
||||||
};
|
|
||||||
notes.push(note);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gives the user a prompt if they are sure to delete this note and deletes it if they confirm
|
* Deletes the note from the "notes" Array and the database
|
||||||
* @param note The note to be deleted
|
|
||||||
*/
|
|
||||||
function deleteNotePrompt(note) {
|
|
||||||
const reallyDelete = confirm("Do you really want to delete this Note?");
|
|
||||||
if (reallyDelete) {
|
|
||||||
deleteNote(note);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the note from the "notes" Array
|
|
||||||
* @param note The note to be deleted
|
* @param note The note to be deleted
|
||||||
*/
|
*/
|
||||||
function deleteNote(note) {
|
function deleteNote(note) {
|
||||||
notes = notes.filter(i => i !== note);
|
notes = notes.filter(i => i !== note);
|
||||||
|
noteRepo.deleteNote(note.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -101,32 +62,44 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a click on a note list element
|
* Sets the currentNoteId and redirects to the editor
|
||||||
* @param note The note the user clicked on
|
* @param note The note the user clicked on
|
||||||
*/
|
*/
|
||||||
function onNoteLiClick(note) {
|
function onNoteLiClick(note) {
|
||||||
|
noteRepo.currentNoteId = note.id;
|
||||||
window.location = "/editor";
|
window.location = "/editor";
|
||||||
note.attributes.lastViewed = new Date();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the modal (popup for deletion)
|
||||||
|
*/
|
||||||
|
async function closeModal() {
|
||||||
|
closeModalBool = true;
|
||||||
|
await sleep(1);
|
||||||
|
closeModalBool = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let closeModalBool = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
<title>PomeloNote | Home</title>
|
<title>PomeloNote | Home</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
|
||||||
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
|
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Add Note Button -->
|
<!-- Add Note Button -->
|
||||||
<div class="offset-md-7 col-md-1">
|
<div class="offset-md-7 col-md-1">
|
||||||
<button class="btn btn-primary" on:click={() => addNotePrompt()}>Add Note</button>
|
<button class="btn btn-primary" on:click={() => addNoteHandler()}>Add Note</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -141,18 +114,42 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-10" on:click={() => onNoteLiClick(note)}>
|
<div class="col-10" on:click={() => onNoteLiClick(note)}>
|
||||||
<div>
|
<div>
|
||||||
{note.attributes.title}
|
{note.title}
|
||||||
</div>
|
</div>
|
||||||
<div class="list-date-text">
|
<div class="list-date-text">
|
||||||
{note.attributes.lastViewed.toLocaleDateString()}
|
{note.lastViewed.toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-1">
|
<div class="col-1">
|
||||||
<button style="display: none" id={"noteButton" + note.id}
|
<!-- Delete Note Popup -->
|
||||||
on:click={() => deleteNotePrompt(note)}>
|
<Modal basic small={true} button={false} close={closeModalBool}>
|
||||||
<i class="bi bi-x"></i>
|
<Content>
|
||||||
</button>
|
<div class="row" style="margin-bottom: 10px;">
|
||||||
|
<h5>Do you really want to delete the "{note.title}" note?</h5>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4 offset-2">
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
on:click={() => deleteNote(note)}
|
||||||
|
on:click={() => closeModal()}>
|
||||||
|
<b>Yes</b>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-4 offset-2">
|
||||||
|
<button class="btn btn-primary" autofocus
|
||||||
|
on:click={() => closeModal()}>
|
||||||
|
<b>No</b>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
<Trigger>
|
||||||
|
<button style="display: none" id={"noteButton" + note.id}>
|
||||||
|
<i class="bi bi-x"></i>
|
||||||
|
</button>
|
||||||
|
</Trigger>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
@ -166,17 +163,11 @@
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@import "../customBootstrap.css";
|
||||||
|
|
||||||
html,
|
html,
|
||||||
:root {
|
:root {
|
||||||
--main-txt-color: black;
|
|
||||||
--sub-txt-color: gray;
|
--sub-txt-color: gray;
|
||||||
--cross-txt-color: red;
|
|
||||||
|
|
||||||
--color-primary: #fff494;
|
|
||||||
--color-primary-600: #fff17a;
|
|
||||||
--color-primary-700: #ffec47;
|
|
||||||
--color-primary-800: #ffe714;
|
|
||||||
--color-primary-900: #e0c900;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|
@ -230,18 +221,6 @@
|
||||||
color: var(--cross-txt-color);
|
color: var(--cross-txt-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--color-primary-800);
|
|
||||||
border: var(--color-primary-800);
|
|
||||||
color: var(--main-txt-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: var(--color-primary-900);
|
|
||||||
border: var(--color-primary-900);
|
|
||||||
color: var(--main-txt-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-date-text {
|
.list-date-text {
|
||||||
color: var(--sub-txt-color);
|
color: var(--sub-txt-color);
|
||||||
font-size: 0.8314159265358979323846264338rem;
|
font-size: 0.8314159265358979323846264338rem;
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,6 @@
|
||||||
import {bearerFetch, jwt} from "../models/PomeloUtils";
|
import {StrapiUserRepo} from "../models/repos/user/StrapiUserRepo";
|
||||||
|
|
||||||
/** @type {import('./$types').PageLoad} */
|
/** @type {import('./$types').PageLoad} */
|
||||||
export async function load() {
|
export async function load() {
|
||||||
let invalid = !jwt;
|
// StrapiUserRepo.getInstance();
|
||||||
|
|
||||||
if (!invalid) {
|
|
||||||
const request = await bearerFetch("/users/me", jwt);
|
|
||||||
const response = await request.json();
|
|
||||||
|
|
||||||
invalid = "error" in response;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalid) {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// @ts-ignore
|
|
||||||
window.location = "/login";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,24 +1,81 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Note} from "../../models/types";
|
import type {Note} from "../../models/types";
|
||||||
|
import {StrapiNoteRepository} from "../../models/repos/note/StrapiNoteRepository";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
|
||||||
let notes: Note[] = JSON.parse(window.localStorage.getItem("notes"));
|
let noteRepo: StrapiNoteRepository;
|
||||||
const clickedNoteId = window.localStorage.getItem("clickedNoteId");
|
let currentNote: Note;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
noteRepo = StrapiNoteRepository.getInstance();
|
||||||
|
try {
|
||||||
|
currentNote = await noteRepo.getNote(noteRepo.currentNoteId);
|
||||||
|
} catch {
|
||||||
|
returnToListing();
|
||||||
|
}
|
||||||
|
title = (<Note>currentNote).title;
|
||||||
|
content = (<Note>currentNote).content;
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* saves the currently opened Note and returns to listing
|
||||||
|
*/
|
||||||
|
function saveAndQuit() {
|
||||||
|
noteRepo.updateNote(currentNote.id, {
|
||||||
|
"title": title,
|
||||||
|
"content": content,
|
||||||
|
})
|
||||||
|
returnToListing();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* redirects to listing
|
||||||
|
*/
|
||||||
|
function returnToListing() {
|
||||||
|
window.location = "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
export let title: string, content: string;
|
||||||
|
|
||||||
const currNote = notes.find((note)=>{
|
|
||||||
return note.id === parseInt(clickedNoteId);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{"Pomelonote | Edit " + currNote.title}</title>
|
<title>Editor</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
|
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<div class="offset-3 col-6">
|
<div class="offset-3 col-6 wrapper">
|
||||||
{currNote.content}
|
<h1 class="">{title === "" ? "" : title}</h1>
|
||||||
|
<input bind:value={title} class="input"> <br/>
|
||||||
|
<textarea bind:value={content} class="input textarea"></textarea>
|
||||||
|
<div class="button-container">
|
||||||
|
<button on:click={() => saveAndQuit()} class="btn btn-primary">Save</button>
|
||||||
|
<button on:click={() => returnToListing()} class="btn btn-outline-danger">Cancel</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@import "../../customBootstrap.css";
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {setCookie} from "nookies";
|
import {setCookie} from "nookies";
|
||||||
import type {Authentication} from "./models/authentication";
|
import {SvelteToast} from '@zerodevx/svelte-toast'
|
||||||
import { SvelteToast } from '@zerodevx/svelte-toast'
|
|
||||||
import {createErrorToast} from "../../models/customToasts";
|
|
||||||
import logo from "../../resources/images/logo2.svg";
|
import logo from "../../resources/images/logo2.svg";
|
||||||
|
import {handleErrorsFromResponseWithToast} from "../../models/PomeloUtils";
|
||||||
|
import {StrapiUserRepo} from "../../models/repos/user/StrapiUserRepo";
|
||||||
|
|
||||||
let user: string;
|
let user: string;
|
||||||
let password: string;
|
let password: string;
|
||||||
|
|
@ -14,31 +14,12 @@
|
||||||
* Handles the button click.
|
* Handles the button click.
|
||||||
*/
|
*/
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const endpoint = "http://localhost:1337/api/auth/local";
|
const userRepo: StrapiUserRepo = StrapiUserRepo.getInstance(false);
|
||||||
const payload = {
|
|
||||||
identifier: user,
|
|
||||||
password: password
|
|
||||||
};
|
|
||||||
|
|
||||||
const login = await fetch(endpoint, {
|
const response = await userRepo.loginUser(user, password);
|
||||||
method: 'post',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
|
|
||||||
const response: Authentication = await login.json();
|
if (response.error != null) {
|
||||||
|
handleErrorsFromResponseWithToast(response);
|
||||||
if (response.error != null){
|
|
||||||
if (response.error.details.errors){
|
|
||||||
for (const error of response.error.details.errors) {
|
|
||||||
createErrorToast(error.message);
|
|
||||||
}
|
|
||||||
} else{
|
|
||||||
createErrorToast(response.error.message);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (rememberMe) {
|
if (rememberMe) {
|
||||||
setCookie(null, 'jwt', response.jwt, {
|
setCookie(null, 'jwt', response.jwt, {
|
||||||
|
|
@ -56,34 +37,34 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
<title>PomeloNote | Login</title>
|
<title>PomeloNote | Login</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
|
||||||
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
|
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main class="form-signin w-100 m-auto">
|
<main class="form-signin w-100 m-auto">
|
||||||
|
|
||||||
<img class="img-fluid" src="{logo}" alt="Logo">
|
<img alt="Logo" class="img-fluid" src="{logo}">
|
||||||
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
|
<h1 class="h3 mb-3 fw-normal">Please sign in</h1>
|
||||||
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="email" class="form-control" id="floatingInput" placeholder="name@example.com" bind:value={user}>
|
<input bind:value={user} class="form-control" id="floatingInput" placeholder="name@example.com" type="text">
|
||||||
<label for="floatingInput">Email address or username</label>
|
<label for="floatingInput">Email address or username</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" bind:value={password}>
|
<input bind:value={password} class="form-control" id="floatingPassword" placeholder="Password" type="password">
|
||||||
<label for="floatingPassword">Password</label>
|
<label for="floatingPassword">Password</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="checkbox mb-3">
|
<div class="checkbox mb-3">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" value="rememberMe" bind:checked={rememberMe}> Remember me
|
<input bind:checked={rememberMe} type="checkbox" value="rememberMe"> Remember me
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button class="w-100 btn btn-lg btn-primary" on:click={handleSubmit}>Sign in</button>
|
<button class="w-100 btn btn-lg btn-primary" on:click={handleSubmit}>Sign in</button>
|
||||||
<a href="/register" class="opacity-75 d-flex justify-content-center text-center fs-6">No user yet? Register.</a>
|
<a class="opacity-75 d-flex justify-content-center text-center fs-6" href="/register">No user yet? Register.</a>
|
||||||
<p class="mt-5 mb-3 text-muted">©2022</p>
|
<p class="mt-5 mb-3 text-muted">©2022</p>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -91,42 +72,7 @@
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html,
|
@import "../../userInput.css";
|
||||||
body {
|
@import "../../customBootstrap.css";
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 40px;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin {
|
|
||||||
max-width: 330px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin .form-floating:focus-within {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin input[type="email"] {
|
|
||||||
margin-bottom: -1px;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin input[type="password"] {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-fluid{
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import logo from "../../resources/images/logo2.svg";
|
import logo from "../../resources/images/logo2.svg";
|
||||||
import {SvelteToast} from "@zerodevx/svelte-toast";
|
import {SvelteToast} from "@zerodevx/svelte-toast";
|
||||||
import type {Authentication} from "../login/models/authentication";
|
import {StrapiUserRepo} from "../../models/repos/user/StrapiUserRepo";
|
||||||
import {createErrorToast} from "../../models/customToasts";
|
import {handleErrorsFromResponseWithToast} from "../../models/PomeloUtils";
|
||||||
|
|
||||||
let user: string;
|
let user: string;
|
||||||
let password: string;
|
let password: string;
|
||||||
|
|
@ -13,32 +13,12 @@
|
||||||
* Handles the button click.
|
* Handles the button click.
|
||||||
*/
|
*/
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const endpoint = "http://localhost:1337/api/auth/local/register";
|
const userRepo: StrapiUserRepo = StrapiUserRepo.getInstance(false);
|
||||||
const payload = {
|
|
||||||
email: email,
|
|
||||||
password: password,
|
|
||||||
username: user
|
|
||||||
};
|
|
||||||
|
|
||||||
const login = await fetch(endpoint, {
|
const response = await userRepo.registerUser(email, user, password);
|
||||||
method: 'post',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
|
|
||||||
const response: Authentication = await login.json();
|
|
||||||
|
|
||||||
if (response.error != null) {
|
if (response.error != null) {
|
||||||
if (response.error.details.errors) {
|
handleErrorsFromResponseWithToast(response);
|
||||||
for (const error of response.error.details.errors) {
|
|
||||||
createErrorToast(error.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
createErrorToast(response.error.message);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
window.location = "/login";
|
window.location = "/login";
|
||||||
}
|
}
|
||||||
|
|
@ -49,29 +29,29 @@
|
||||||
lang="en">
|
lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
<title>PomeloNote | Register</title>
|
<title>PomeloNote | Register</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css" rel="stylesheet"
|
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.1/dist/css/bootstrap.min.css"
|
||||||
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" crossorigin="anonymous">
|
integrity="sha384-iYQeCzEYFbKjA/T2uDLTpkwGzCiq6soy8tYaI1GyVh/UjpbCx/TYkiZhlZB6+fzT" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main class="form-signin w-100 m-auto">
|
<main class="form-signin w-100 m-auto">
|
||||||
|
|
||||||
<img class="img-fluid" src="{logo}" alt="Logo">
|
<img alt="Logo" class="img-fluid" src="{logo}">
|
||||||
<h1 class="h3 mb-3 fw-normal">Register a new user</h1>
|
<h1 class="h3 mb-3 fw-normal">Register a new user</h1>
|
||||||
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="text" class="form-control" id="floatingUsr" placeholder="exampleUsername" bind:value={user}>
|
<input bind:value={user} class="form-control" id="floatingUsr" placeholder="exampleUsername" type="text">
|
||||||
<label for="floatingUsr">Username</label>
|
<label for="floatingUsr">Username</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="email" class="form-control" id="floatingInput" placeholder="name@example.com" bind:value={email}>
|
<input bind:value={email} class="form-control" id="floatingInput" placeholder="name@example.com" type="email">
|
||||||
<label for="floatingInput">Email address</label>
|
<label for="floatingInput">Email address</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="password" class="form-control" id="floatingPassword" placeholder="Password" bind:value={password}>
|
<input bind:value={password} class="form-control" id="floatingPassword" placeholder="Password" type="password">
|
||||||
<label for="floatingPassword">Password</label>
|
<label for="floatingPassword">Password</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -79,7 +59,7 @@
|
||||||
Register user
|
Register user
|
||||||
{#if user}: {user} {/if}
|
{#if user}: {user} {/if}
|
||||||
</button>
|
</button>
|
||||||
<a href="/login" class="opacity-75 d-flex justify-content-center text-center fs-6">Already registered? Login.</a>
|
<a class="opacity-75 d-flex justify-content-center text-center fs-6" href="/login">Already registered? Login.</a>
|
||||||
<p class="mt-5 mb-3 text-muted">©2022</p>
|
<p class="mt-5 mb-3 text-muted">©2022</p>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -88,46 +68,6 @@
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
html,
|
@import "../../userInput.css";
|
||||||
body {
|
@import "../../customBootstrap.css";
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
align-items: center;
|
|
||||||
padding-top: 40px;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin {
|
|
||||||
max-width: 330px;
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin .form-floating:focus-within {
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin input[type="text"] {
|
|
||||||
margin-bottom: -1px;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-signin input[type="email"] {
|
|
||||||
margin-bottom: -1px;
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.form-signin input[type="password"] {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-fluid {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
78
frontend/svelte/src/service-worker.js
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
|
import { build, files, version } from '$service-worker';
|
||||||
|
|
||||||
|
const worker = ServiceWorkerGlobalScope;
|
||||||
|
// const FILES = cache + version;
|
||||||
|
|
||||||
|
const to_cache = build.concat(files);
|
||||||
|
const staticAssets = new Set(to_cache);
|
||||||
|
|
||||||
|
worker.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches
|
||||||
|
.open(FILES)
|
||||||
|
.then((cache) => cache.addAll(to_cache))
|
||||||
|
.then(() => {
|
||||||
|
worker.skipWaiting();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
worker.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then(async (keys) => {
|
||||||
|
// delete old caches
|
||||||
|
for (const key of keys) {
|
||||||
|
if (key !== FILES) await caches.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.clients.claim();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the asset from the network and store it in the cache.
|
||||||
|
* Fall back to the cache if the user is offline.
|
||||||
|
*/
|
||||||
|
async function fetchAndCache(request) {
|
||||||
|
const cache = await caches.open(offline + version);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(request);
|
||||||
|
cache.put(request, response.clone());
|
||||||
|
return response;
|
||||||
|
} catch (err) {
|
||||||
|
const response = await cache.match(request);
|
||||||
|
if (response) return response;
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.addEventListener('fetch', (event) => {
|
||||||
|
if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
|
||||||
|
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// don't try to handle e.g. data: URIs
|
||||||
|
const isHttp = url.protocol.startsWith('http');
|
||||||
|
const isDevServerRequest =
|
||||||
|
url.hostname === self.location.hostname && url.port !== self.location.port;
|
||||||
|
const isStaticAsset = url.host === self.location.host && staticAssets.has(url.pathname);
|
||||||
|
const skipBecauseUncached = event.request.cache === 'only-if-cached' && !isStaticAsset;
|
||||||
|
|
||||||
|
if (isHttp && !isDevServerRequest && !skipBecauseUncached) {
|
||||||
|
event.respondWith(
|
||||||
|
(async () => {
|
||||||
|
// always serve static files and bundler-generated assets from cache.
|
||||||
|
// if your application has other URLs with data that will never change,
|
||||||
|
// set this variable to true for them and they will only be fetched once.
|
||||||
|
const cachedAsset = isStaticAsset && (await caches.match(event.request));
|
||||||
|
|
||||||
|
return cachedAsset || fetchAndCache(event.request);
|
||||||
|
})()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
7
frontend/svelte/src/stores.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import {writable} from "svelte/store";
|
||||||
|
import {browser} from "$app/environment"
|
||||||
|
export const currentNoteId = writable<number>();
|
||||||
|
if (browser) {
|
||||||
|
currentNoteId.set(Number(localStorage.getItem("currentNoteId") || ""))
|
||||||
|
currentNoteId.subscribe(val => localStorage.setItem("currentNoteId", String(val)));
|
||||||
|
}
|
||||||
41
frontend/svelte/src/userInput.css
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin {
|
||||||
|
max-width: 330px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin .form-floating:focus-within {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin input[type="text"] {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin input[type="email"] {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-signin input[type="password"] {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-fluid {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
41
frontend/svelte/static/manifest.json
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"lang": "en",
|
||||||
|
"dir": "/",
|
||||||
|
"name": "Pomelo Note",
|
||||||
|
"short_name": "Pomelo",
|
||||||
|
"description": "Best Note App",
|
||||||
|
"theme_color": "#000",
|
||||||
|
"background_color": "#000",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"scope": "/",
|
||||||
|
"start_url": "/",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "../resources/icons/android-icon-36x36.png",
|
||||||
|
"sizes": "36x36",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "0.75"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "../resources/icons/android-icon-48x48.png",
|
||||||
|
"sizes": "48x48",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "1.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "../resources/icons/android-icon-72x72.png",
|
||||||
|
"sizes": "72x72",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "1.5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "../resources/icons/android-icon-96x96.png",
|
||||||
|
"sizes": "96x96",
|
||||||
|
"type": "image\/png",
|
||||||
|
"density": "2.0"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"splash_pages": null
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,14 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import type { UserConfig } from 'vite';
|
import type { UserConfig } from 'vite';
|
||||||
|
|
||||||
const config: UserConfig = {
|
const config: UserConfig = {
|
||||||
plugins: [sveltekit()]
|
plugins: [sveltekit()],
|
||||||
|
|
||||||
|
server: {
|
||||||
|
fs: {
|
||||||
|
// Allow serving files from one level up to the project root
|
||||||
|
allow: ['..'],
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
||||||