diff --git a/.gitignore b/.gitignore
index 8e0cfe12..c5cdb601 100644
--- a/.gitignore
+++ b/.gitignore
@@ -170,3 +170,6 @@ dmypy.json
# static images
mondey_backend/static/*.jpg
+
+# ssl keys
+*.pem
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
index 3ec346c9..6724905f 100644
--- a/DEPLOYMENT.md
+++ b/DEPLOYMENT.md
@@ -25,6 +25,12 @@ sudo docker compose ps
sudo docker compose logs
```
+To update the running website to the latest version:
+
+```
+sudo docker compose pull && sudo docker compose up -d && sudo docker system prune -af
+```
+
### Give users admin rights
To make an existing user with email `user@domain.com` into an admin, modify the users database, e.g.
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 00000000..016015a0
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,82 @@
+# Development
+
+Some information on how to locally build and serve the website if you would like to make changes to the code.
+There are two ways to do this:
+
+- docker
+ - closer to production environment
+ - but less convenient for development - you need to rebuild the image every time you make a change
+- python/pnpm
+ - further from production environment setup
+ - but convenient for development - see changes immediately without having to rebuild or restart anything
+
+## Run locally with docker
+
+Requires docker and docker compose.
+
+1. clone the repo:
+
+```sh
+git clone https://github.com/ssciwr/mondey.git
+cd mondey
+```
+
+2. generate a local SSL cert/key pair:
+
+```
+openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -sha256 -days 365 -nodes -subj '/CN=localhost'
+```
+
+3. build and run the website locally in docker containers on your computer:
+
+```sh
+docker compose up --build -d
+```
+
+The website is then served at https://localhost/
+(note that the SSL keys are self-signed keys and your browser will still warn about the site being insecure.)
+
+Whenever you make a change to the code you need to re-run the above command to see the effect of your changes.
+
+### Database
+
+The databases will by default be stored in a `db` folder
+in the folder where you run the docker compose command.
+
+### Make yourself an admin user
+
+```
+sudo sqlite3 docker_volume/predicTCR.db
+sqlite> UPDATE user SET is_admin=true WHERE email='you@address.com';
+sqlite> .quit
+```
+
+## Run locally with Python and pnpm
+
+Requires Python and [pnpm](https://pnpm.io/installation#using-a-standalone-script)
+
+1. clone the repo:
+
+```sh
+git clone https://github.com/ssciwr/mondey.git
+cd mondey
+```
+
+2. install and run the backend development server:
+
+```sh
+cd mondey_backend
+pip install .
+cd ..
+mondey-backend
+```
+
+3. install and run the frontend development server:
+
+```sh
+cd frontend
+pnpm install
+pnpm run dev
+```
+
+The website is then served at http://localhost:5173/.
diff --git a/frontend/src/lib/admin.ts b/frontend/src/lib/admin.ts
index dd32d488..a0c2da82 100644
--- a/frontend/src/lib/admin.ts
+++ b/frontend/src/lib/admin.ts
@@ -126,6 +126,105 @@ export function milestoneGroupImageUrl(id: number) {
return `${import.meta.env.VITE_MONDEY_API_URL}/static/mg${id}.jpg`;
}
+export async function newMilestone(milestoneGroupId: number) {
+ console.log('newMilestone...');
+ try {
+ const res = await fetch(
+ `${import.meta.env.VITE_MONDEY_API_URL}/admin/milestones/${milestoneGroupId}`,
+ {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json'
+ }
+ }
+ );
+ if (res.status === 200) {
+ const newMilestone = await res.json();
+ console.log(newMilestone);
+ await refreshMilestoneGroups();
+ return newMilestone;
+ } else {
+ console.log('Failed to create new Milestone');
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ return null;
+}
+
+export async function updateMilestone(milestone) {
+ console.log('updateMilestone...');
+ console.log(milestone);
+ try {
+ const res = await fetch(`${import.meta.env.VITE_MONDEY_API_URL}/admin/milestones/`, {
+ method: 'PUT',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json'
+ },
+ body: JSON.stringify(milestone)
+ });
+ if (res.status === 200) {
+ const updatedMilestone = await res.json();
+ console.log(updatedMilestone);
+ return updatedMilestone;
+ } else {
+ console.log('Failed to update Milestone');
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ return null;
+}
+
+export async function uploadMilestoneImage(milestoneId: number, file) {
+ console.log('uploadMilestoneImage...');
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+ const res = await fetch(
+ `${import.meta.env.VITE_MONDEY_API_URL}/admin/milestone-images/${milestoneId}`,
+ {
+ method: 'POST',
+ credentials: 'include',
+ body: formData
+ }
+ );
+ console.log(await res.json());
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+export async function deleteMilestone(milestoneId: number | null) {
+ try {
+ const res = await fetch(
+ `${import.meta.env.VITE_MONDEY_API_URL}/admin/milestones/${milestoneId}`,
+ {
+ method: 'DELETE',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json'
+ }
+ }
+ );
+ const json = await res.json();
+ console.log(json);
+ if (res.status === 200) {
+ console.log(`Deleted Milestone with id ${milestoneId}.`);
+ await refreshMilestoneGroups();
+ } else {
+ console.log(`Error deleting Milestone with id ${milestoneId}.`);
+ }
+ } catch (e) {
+ console.error(e);
+ }
+}
+
export async function refreshUserQuestions() {
console.log('refreshQuestions...');
try {
diff --git a/frontend/src/lib/components/Admin/EditMilestoneModal.svelte b/frontend/src/lib/components/Admin/EditMilestoneModal.svelte
new file mode 100644
index 00000000..09df804d
--- /dev/null
+++ b/frontend/src/lib/components/Admin/EditMilestoneModal.svelte
@@ -0,0 +1,135 @@
+
+
+
+ {#if milestone}
+
+
+ {#each Object.entries(milestone.text) as [lang_id, text]}
+
+
+ {$languages[text.lang_id]}
+
+
+
+ {/each}
+
+
+
+ {#each Object.entries(milestone.text) as [lang_id, text]}
+
+
+ {$languages[text.lang_id]}
+
+
+
+ {/each}
+
+
+
+ {#each Object.entries(milestone.text) as [lang_id, text]}
+
+
+ {$languages[text.lang_id]}
+
+
+
+ {/each}
+
+
+
+ {#each Object.entries(milestone.text) as [lang_id, text]}
+
+
+ {$languages[text.lang_id]}
+
+
+
+ {/each}
+
+
+
+
+ {#each milestone.images as milestoneImage, milestoneImageId (milestoneImage.id)}
+
+ {/each}
+ {#each images as image}
+
+ {/each}
+
+
+
+ {/if}
+
+
+
+
+
diff --git a/frontend/src/lib/components/Admin/MilestoneGroups.svelte b/frontend/src/lib/components/Admin/MilestoneGroups.svelte
index 934fa251..ac8ea6d7 100644
--- a/frontend/src/lib/components/Admin/MilestoneGroups.svelte
+++ b/frontend/src/lib/components/Admin/MilestoneGroups.svelte
@@ -11,20 +11,32 @@
import { _ } from 'svelte-i18n';
import { ChevronUpOutline, ChevronDownOutline } from 'flowbite-svelte-icons';
import EditMilestoneGroupModal from '$lib/components/Admin/EditMilestoneGroupModal.svelte';
+ import EditMilestoneModal from '$lib/components/Admin/EditMilestoneModal.svelte';
import DeleteModal from '$lib/components/Admin/DeleteModal.svelte';
import AddButton from '$lib/components/Admin/AddButton.svelte';
import EditButton from '$lib/components/Admin/EditButton.svelte';
import DeleteButton from '$lib/components/Admin/DeleteButton.svelte';
import { lang_id, milestoneGroups } from '$lib/stores/adminStore';
- import { refreshMilestoneGroups, newMilestoneGroup, milestoneGroupImageUrl } from '$lib/admin';
+ import {
+ refreshMilestoneGroups,
+ newMilestoneGroup,
+ newMilestone,
+ deleteMilestoneGroup,
+ deleteMilestone,
+ milestoneGroupImageUrl
+ } from '$lib/admin';
import { onMount } from 'svelte';
- import { deleteMilestoneGroup } from '$lib/admin.js';
let currentGroup: object | null = null;
let openGroupIndex: number | null = null;
let currentGroupId: number | null = null;
let showEditGroupModal: boolean = false;
- let showDeleteModal: boolean = false;
+ let showDeleteGroupModal: boolean = false;
+
+ let currentMilestone: object | null = null;
+ let currentMilestoneId: number | null = null;
+ let showEditMilestoneModal: boolean = false;
+ let showDeleteMilestoneModal: boolean = false;
function toggleOpenGroupIndex(index: number) {
if (openGroupIndex == index) {
@@ -43,33 +55,35 @@
showEditGroupModal = true;
}
+ async function addMilestone(milestoneGroupId: number) {
+ currentMilestone = await newMilestone(milestoneGroupId);
+ if (currentMilestone === null) {
+ return;
+ }
+ showEditMilestoneModal = true;
+ }
+
onMount(async () => {
await refreshMilestoneGroups();
});
+
+ $: console.log($milestoneGroups);
-