Ability to load archive, remote posts, save archives and code improvements.
This commit is contained in:
parent
eac0aa15eb
commit
14694f7082
4 changed files with 163 additions and 26 deletions
162
src/App.vue
162
src/App.vue
|
@ -7,10 +7,13 @@ import { generateTimestamp, parseTimestampToUTC, generatePostObj } from './post-
|
||||||
import { getArchive, getPost } from './http-helper.service';
|
import { getArchive, getPost } from './http-helper.service';
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// Modal requesting blog install base url.
|
// Ability to delete posts from the archive.
|
||||||
// Fetch archive from install and show posts in a list.
|
|
||||||
// Allow selection of posts form list and load them.
|
let timestampUpdateInterval: number;
|
||||||
// Save post and archive.
|
|
||||||
|
const blogInstallLocation = ref('');
|
||||||
|
|
||||||
|
const archive = ref<Archive[]>();
|
||||||
|
|
||||||
const postTitle = ref('');
|
const postTitle = ref('');
|
||||||
const postContent = ref('');
|
const postContent = ref('');
|
||||||
|
@ -20,21 +23,48 @@ const isDraft = ref(false);
|
||||||
|
|
||||||
const isEditingExisting = ref(false);
|
const isEditingExisting = ref(false);
|
||||||
|
|
||||||
let timestampUpdateInterval: number;
|
const onlyLoadArchive = ref(false);
|
||||||
|
|
||||||
const archive = ref<Archive[]>();
|
const openLoadModal = (archiveOnly = false): void => {
|
||||||
|
onlyLoadArchive.value = archiveOnly;
|
||||||
|
(document.getElementById('dialog') as HTMLDialogElement).showModal();
|
||||||
|
}
|
||||||
|
|
||||||
const openLoadModal = (): void => {
|
const closeLoadModal = (): void => {
|
||||||
alert('Not implemented, yet.');
|
(document.getElementById('dialog') as HTMLDialogElement).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
const reset = (): void => {
|
||||||
|
archive.value = undefined;
|
||||||
|
postTitle.value = '';
|
||||||
|
postContent.value = '';
|
||||||
|
postTimestamp.value = '';
|
||||||
|
editedTimestamp.value = '';
|
||||||
|
isDraft.value = false;
|
||||||
|
isEditingExisting.value = false;
|
||||||
|
editedTimestamp.value = '';
|
||||||
|
|
||||||
|
clearInterval(timestampUpdateInterval);
|
||||||
|
|
||||||
|
timestampUpdateInterval = setInterval(() => {
|
||||||
|
postTimestamp.value = generateTimestamp();
|
||||||
|
}, 33);
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadArchive = (baseUrl: string): void => {
|
const loadArchive = (baseUrl: string): void => {
|
||||||
getArchive(baseUrl).then(res => {
|
getArchive(baseUrl).then(res => {
|
||||||
archive.value = res;
|
archive.value = res;
|
||||||
|
|
||||||
|
if (onlyLoadArchive.value) {
|
||||||
|
onlyLoadArchive.value = false;
|
||||||
|
closeLoadModal();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadPostFromFile = (file: any): void => {
|
const loadPostFromFile = (file: any): void => {
|
||||||
|
archive.value = undefined;
|
||||||
|
|
||||||
loadFromFile(file.target.files[0]).then((res: Post) => {
|
loadFromFile(file.target.files[0]).then((res: Post) => {
|
||||||
postTitle.value = res.postTitle;
|
postTitle.value = res.postTitle;
|
||||||
postContent.value = res.postContent;
|
postContent.value = res.postContent;
|
||||||
|
@ -65,10 +95,12 @@ const loadPost = (baseUrl: string, filename: string): void => {
|
||||||
timestampUpdateInterval = setInterval(() => {
|
timestampUpdateInterval = setInterval(() => {
|
||||||
editedTimestamp.value = generateTimestamp();
|
editedTimestamp.value = generateTimestamp();
|
||||||
}, 33);
|
}, 33);
|
||||||
|
|
||||||
|
closeLoadModal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const savePost = (): void => {
|
const savePost = (saveArchive?: boolean): void => {
|
||||||
if (!postTitle.value) {
|
if (!postTitle.value) {
|
||||||
postTitle.value = 'No title.';
|
postTitle.value = 'No title.';
|
||||||
}
|
}
|
||||||
|
@ -90,6 +122,17 @@ const savePost = (): void => {
|
||||||
),
|
),
|
||||||
computedFilename.value
|
computedFilename.value
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (saveArchive) {
|
||||||
|
if (!archive.value?.filter(post => post.filename === computedFilename.value) && !isDraft) {
|
||||||
|
archive.value?.push({
|
||||||
|
postTitle: postTitle.value,
|
||||||
|
timestamp: postTimestamp.value,
|
||||||
|
filename: computedFilename.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
downloadFile(archive.value, 'archive');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -105,6 +148,37 @@ const computedEditedTimestamp = computed(() => parseTimestampToUTC(editedTimesta
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
<dialog id="dialog">
|
||||||
|
<div class="dialog-header">
|
||||||
|
<div v-if="onlyLoadArchive">
|
||||||
|
<h4>Load Archive...</h4>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h4>Load post from Archive...</h4>
|
||||||
|
</div>
|
||||||
|
<div class="close-btn">
|
||||||
|
<button class="btn-primary" @click="closeLoadModal()">
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!archive">
|
||||||
|
Blog Location:
|
||||||
|
https://
|
||||||
|
<input type="text" v-model="blogInstallLocation" placeholder="pedrocx486.club/blog" />
|
||||||
|
/assets/posts/archive.json
|
||||||
|
<br />
|
||||||
|
<button class="btn-primary btn-wide" :disabled="!blogInstallLocation" @click="loadArchive(blogInstallLocation)">
|
||||||
|
Load Archive
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div v-for="post in archive" v-if="!onlyLoadArchive">
|
||||||
|
<button class="btn-secondary btn-wide" @click="loadPost(blogInstallLocation, post.filename)">
|
||||||
|
{{ post.postTitle }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<div class="editor-area">
|
<div class="editor-area">
|
||||||
<div class="title-area">
|
<div class="title-area">
|
||||||
<input class="title" type="text" placeholder="Post title..." v-model="postTitle" />
|
<input class="title" type="text" placeholder="Post title..." v-model="postTitle" />
|
||||||
|
@ -115,17 +189,22 @@ const computedEditedTimestamp = computed(() => parseTimestampToUTC(editedTimesta
|
||||||
</div>
|
</div>
|
||||||
<div class="preview">
|
<div class="preview">
|
||||||
<p class="preview-title"> Preview: </p>
|
<p class="preview-title"> Preview: </p>
|
||||||
<VueShowdown v-bind:markdown="postContent" flavor="github" :options="{ emoji: true }" tag="span" />
|
<VueShowdown :markdown="postContent" flavor="github" :options="{ emoji: true }" tag="span" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Filename: {{ computedFilename }} <br />
|
Filename: {{ computedFilename }}<span v-if="postTitle">.json</span> <br />
|
||||||
Created on: {{ computedTimestamp }} <br />
|
Created on: {{ computedTimestamp }} <br />
|
||||||
<span v-if="isEditingExisting">Edited on: {{ computedEditedTimestamp }} <br /></span>
|
<span v-if="isEditingExisting">Edited on: {{ computedEditedTimestamp }} <br /></span>
|
||||||
Is it a draft? {{ isDraft? 'Yes': 'No' }}. <br />
|
Is it a draft? {{ isDraft? 'Yes': 'No' }}. <br />
|
||||||
<div class="footer-buttons">
|
<div class="footer-buttons">
|
||||||
<button class="btn-primary" @click="savePost">Save (ngx-retroblog format)</button>
|
<button class="btn-tertiary" @click="reset()">Reset Editor</button>
|
||||||
<button class="btn-primary" @click="openLoadModal">Load Post (from Archive)</button> <br />
|
<button class="btn-primary" @click="openLoadModal(true)">Load Archive</button>
|
||||||
|
<button class="btn-primary" @click="openLoadModal()">Load Post (from Archive)</button>
|
||||||
|
<button class="btn-primary" @click="savePost()">Save Post</button>
|
||||||
|
<button class="btn-primary" @click="savePost(true)" :disabled="!archive || isDraft">Save Post & Archive</button>
|
||||||
|
|
||||||
|
<br />
|
||||||
<input type="file" accept=".json" id="file-input" @change="loadPostFromFile($event)">
|
<input type="file" accept=".json" id="file-input" @change="loadPostFromFile($event)">
|
||||||
<label for="file-input">Load Post (from File)</label>
|
<label for="file-input">Load Post (from File)</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -153,12 +232,65 @@ button {
|
||||||
background-color: rgb(97, 0, 162);
|
background-color: rgb(97, 0, 162);
|
||||||
color: white;
|
color: white;
|
||||||
font-size: .8rem;
|
font-size: .8rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary[disabled] {
|
||||||
|
background-color: rgb(144, 144, 144);
|
||||||
|
color: rgb(75, 75, 75);
|
||||||
|
font-size: .8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
box-shadow: 0px 0px 7px rgb(97, 0, 162);
|
box-shadow: 0px 0px 7px rgb(97, 0, 162);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: rgb(0, 241, 221);
|
||||||
|
color: rgb(78, 78, 78);
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
box-shadow: 0px 0px 7px rgb(0, 241, 221);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-tertiary {
|
||||||
|
background-color: rgb(241, 8, 0);
|
||||||
|
color: rgb(233, 233, 233);
|
||||||
|
font-size: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-tertiary:hover {
|
||||||
|
box-shadow: 0px 0px 7px rgb(241, 8, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
text-align: end;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-wide {
|
||||||
|
min-width: -webkit-fill-available;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dialog {
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header>* h4 {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.wrapper {
|
.wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
grid-gap: 1rem;
|
grid-gap: 1rem;
|
||||||
|
@ -213,6 +345,7 @@ button {
|
||||||
|
|
||||||
.footer-buttons {
|
.footer-buttons {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
padding-bottom: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-input {
|
#file-input {
|
||||||
|
@ -236,10 +369,11 @@ button {
|
||||||
background-color: rgb(97, 0, 162);
|
background-color: rgb(97, 0, 162);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
margin-top: 0.3rem;
|
margin-top: 0.5rem;
|
||||||
width: 20.7rem;
|
width: 20.7rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-left: -11.2rem;
|
margin-left: -11.2rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-input:focus+label,
|
#file-input:focus+label,
|
||||||
|
|
|
@ -21,13 +21,13 @@ export const parseFilename = (titleToFilename: string): string => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return titleToFilename + '.json';
|
return titleToFilename;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const downloadFile = (postObj: any, fileName: string, archiveFile?: boolean): void => {
|
export const downloadFile = (dataPbj: any, fileName: string): void => {
|
||||||
const blob = new Blob([JSON.stringify(postObj, null, 2)], { type: 'text/plain;charset=utf-8;' });
|
const blob = new Blob([JSON.stringify(dataPbj, null, 2)], { type: 'text/plain;charset=utf-8;' });
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.setAttribute('download', archiveFile ? 'archive.json' : fileName);
|
a.setAttribute('download', `${fileName}.json`);
|
||||||
a.setAttribute('href', window.URL.createObjectURL(blob));
|
a.setAttribute('href', window.URL.createObjectURL(blob));
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,15 +17,18 @@ httpService.interceptors.response.use(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export async function getArchive(baseUrl: string): Promise<Archive[]> {
|
||||||
export async function getArchive(baseUrl: string) {
|
return await httpService.get<Archive[]>(`https://${baseUrl}/assets/posts/archive.json`).then((res: any) => {
|
||||||
return await httpService.get<Archive[]>(`https://${baseUrl}/assets/posts/archive.json`).then((res) => {
|
|
||||||
return res.data;
|
return res.data;
|
||||||
|
}).catch(err => {
|
||||||
|
alert(`Failed to load Archive! We've got a ${err.message}!\nAre you connected to the internet or is the location correct?`)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPost(baseUrl: string, post: string) {
|
export async function getPost(baseUrl: string, post: string): Promise<Post> {
|
||||||
return await httpService.get<Post>(`http://${baseUrl}/assets/posts/${post}.json`).then((res) => {
|
return await httpService.get<Post>(`https://${baseUrl}/assets/posts/${post}.json`).then((res: any) => {
|
||||||
return res.data;
|
return res.data;
|
||||||
|
}).catch(err => {
|
||||||
|
alert(`Failed to load Post! We've got a ${err.message}!\nAre you connected to the internet?`)
|
||||||
});
|
});
|
||||||
}
|
}
|
Loading…
Reference in a new issue