Cleanup logic, rewrite some of it and more.
This commit is contained in:
parent
274a4e09ff
commit
ccce6a3e20
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"postTitle": "Hello World!",
|
||||||
|
"timestamp": "1654062776",
|
||||||
|
"editedTimestamp": "",
|
||||||
|
"postContent": "Welcome to my humble blog! This blog is hosted using a blog platform loosely based on my [ngx-dumblog](http:\/\/github.com\/pedroCX486\/ngx-dumblog) project. My intent is to make this look more and more retro with time.\r\n \r\nAnd before you ask, it's availabe [here](http:\/\/git.pedrocx486.club\/pedrocx486\/ngx-retroblog).\r\n \r\nFor more info (maybe), check [The Club](https:\/\/pedrocx486.club)!",
|
||||||
|
"filename": "hello-world",
|
||||||
|
"draft": false
|
||||||
|
}
|
166
src/App.vue
166
src/App.vue
|
@ -1,133 +1,104 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { VueShowdown } from 'vue-showdown';
|
||||||
|
import { downloadFile, parseFilename } from './file-manager.service';
|
||||||
|
import { Archive } from './types';
|
||||||
|
import { generateTimestamp, parseTimestampToUTC, generatePostObj } from './post-utilities.service';
|
||||||
|
import { getArchive, getPost } from './http-helper.service';
|
||||||
|
|
||||||
const title = ref('');
|
const postTitle = ref('');
|
||||||
const content = ref('');
|
const postContent = ref('');
|
||||||
const timestamp = ref('');
|
const postTimestamp = ref('');
|
||||||
const editedTimestamp = ref('');
|
const editedTimestamp = ref('');
|
||||||
const draft = ref(false);
|
const isDraft = ref(false);
|
||||||
|
|
||||||
const archive = ref([{ postTitle: '', timestamp: '', filename: '' }])
|
const isEditingExisting = ref(false);
|
||||||
const post = ref({
|
|
||||||
postTitle: "",
|
|
||||||
timestamp: "",
|
|
||||||
editedTimestamp: "",
|
|
||||||
postContent: "",
|
|
||||||
filename: "",
|
|
||||||
draft: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
const generateTimestamp = (): string => {
|
let timestampUpdateInterval: number;
|
||||||
return Math.round((new Date()).getTime() / 1000).toString();
|
|
||||||
|
const archive = ref<Archive[]>();
|
||||||
|
|
||||||
|
const loadArchive = (baseUrl: string) => {
|
||||||
|
getArchive(baseUrl).then(res => {
|
||||||
|
archive.value = res;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseTimestamp = (unixTimestamp: string): string => {
|
const loadPost = (baseUrl: string, filename: string): void => {
|
||||||
if (!unixTimestamp) {
|
getPost(baseUrl, filename).then(res => {
|
||||||
return ''
|
postTitle.value = res.postTitle;
|
||||||
}
|
postContent.value = res.postContent;
|
||||||
|
postTimestamp.value = res.timestamp;
|
||||||
|
isDraft.value = res.draft;
|
||||||
|
|
||||||
return new Date(Number(unixTimestamp) * 1000).toUTCString();
|
isEditingExisting.value = true;
|
||||||
}
|
|
||||||
|
|
||||||
const parseFilename = (titleToFilename: string): string => {
|
clearInterval(timestampUpdateInterval);
|
||||||
if (!titleToFilename) {
|
|
||||||
return 'None yet.'
|
|
||||||
}
|
|
||||||
|
|
||||||
titleToFilename = titleToFilename.replace(/[^a-zA-Z0-9_]+/gi, '-').toLowerCase();
|
timestampUpdateInterval = setInterval(() => {
|
||||||
|
editedTimestamp.value = generateTimestamp();
|
||||||
while (titleToFilename.endsWith('-')) {
|
}, 33);
|
||||||
titleToFilename = titleToFilename.slice(0, -1);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (titleToFilename.length > 50) { // Limit filename size
|
|
||||||
titleToFilename = titleToFilename.substring(0, 50);
|
|
||||||
|
|
||||||
if (titleToFilename.includes('-')) {
|
|
||||||
titleToFilename = titleToFilename.substring(0, Math.min(titleToFilename.length, titleToFilename.lastIndexOf('-'))); // Re-trim to avoid cutting a word in half.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return titleToFilename + '.json';
|
|
||||||
}
|
|
||||||
|
|
||||||
const generatePostObj = (isDraft?: boolean): Object => {
|
|
||||||
return {
|
|
||||||
postTitle: title.value,
|
|
||||||
timestamp: isDraft ? '' : timestamp.value,
|
|
||||||
//editedTimestamp: isDraft ? '' : editedTimestamp.value,
|
|
||||||
postContent: content.value,
|
|
||||||
filename: computedFilename.value,
|
|
||||||
draft: draft.value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadFile = (isDraft?: boolean, archiveFile?: boolean): void => {
|
|
||||||
const blob = new Blob([JSON.stringify(generatePostObj(isDraft), null, 2)], { type: 'text/plain' });
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.setAttribute('download', archiveFile ? 'archive.json' : computedFilename.value);
|
|
||||||
a.setAttribute('href', window.URL.createObjectURL(blob));
|
|
||||||
a.click();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const savePost = (): void => {
|
const savePost = (): void => {
|
||||||
if (!title.value) {
|
if (!postTitle.value) {
|
||||||
title.value = 'No title.';
|
postTitle.value = 'No title.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!content.value) {
|
if (!postContent.value) {
|
||||||
content.value = 'No content.';
|
postContent.value = 'No content.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draft.value) { // Save post as draft (no timestamps)
|
// If draft is true, it'll have no timestamps.
|
||||||
downloadFile(true);
|
downloadFile(
|
||||||
} else { // Save post for publishing
|
generatePostObj(
|
||||||
|
postTitle.value,
|
||||||
// Reminder for when we have an editedTimestamp. Requires way more than this and I have no patience right now.
|
postTimestamp.value,
|
||||||
// if (timestamp.value) {
|
postContent.value,
|
||||||
// editedTimestamp.value = generateTimestamp();
|
computedFilename.value,
|
||||||
// }
|
isDraft.value,
|
||||||
|
isEditingExisting.value,
|
||||||
downloadFile();
|
editedTimestamp.value
|
||||||
|
),
|
||||||
// Reminder when we get to the point of saving archives.
|
computedFilename.value
|
||||||
//saveAs(new Blob([JSON.stringify(this.archives, null, 2)], { type: 'text/plain;charset=utf-8;' }), 'archive.json');
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Refactor this later when we have an editedTimestamp.
|
timestampUpdateInterval = setInterval(() => {
|
||||||
setInterval(() => {
|
postTimestamp.value = generateTimestamp();
|
||||||
timestamp.value = generateTimestamp();
|
}, 33);
|
||||||
}, 1000)
|
|
||||||
|
// For debugging.
|
||||||
|
// loadPost('localhost:3000', 'hello-world');
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedFilename = computed(() => parseFilename(title.value))
|
const computedFilename = computed(() => parseFilename(postTitle.value))
|
||||||
const computedTimestamp = computed(() => parseTimestamp(timestamp.value));
|
const computedTimestamp = computed(() => parseTimestampToUTC(postTimestamp.value));
|
||||||
const computedEditedTimestamp = computed(() => parseTimestamp(timestamp.value));
|
const computedEditedTimestamp = computed(() => parseTimestampToUTC(editedTimestamp.value));
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<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="title" />
|
<input class="title" type="text" placeholder="Post title..." v-model="postTitle" />
|
||||||
<input type="checkbox" id="draft" v-model="draft" /><label for="draft">Draft</label>
|
<input type="checkbox" id="draft" v-model="isDraft" /><label for="draft">Draft</label>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<textarea class="editor" v-model="content" placeholder="Post content..." />
|
<textarea class="editor" v-model="postContent" placeholder="Post content..." />
|
||||||
</div>
|
</div>
|
||||||
<div class="preview">
|
<div class="preview">
|
||||||
<p class="preview-title"> Preview: </p>
|
<p class="preview-title"> Preview: </p>
|
||||||
<VueShowdown v-bind:markdown="content" flavor="github" :options="{ emoji: true }" tag="span" />
|
<VueShowdown v-bind:markdown="postContent" flavor="github" :options="{ emoji: true }" tag="span" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
Filename: {{ computedFilename }} <br />
|
Filename: {{ computedFilename }} <br />
|
||||||
Created on Timestamp: {{ computedTimestamp }} <br />
|
Created on: {{ computedTimestamp }} <br />
|
||||||
<!-- Edited on Timestamp: {{ computedEditedTimestamp }} <br /> -->
|
<span v-if="isEditingExisting">Edited on: {{ computedEditedTimestamp }} <br /></span>
|
||||||
Is it a draft? {{ draft? 'Yes': 'No' }}. <br />
|
Is it a draft? {{ isDraft? 'Yes': 'No' }}. <br />
|
||||||
<button class="btn-primary" @click="savePost">Save (ngx-retroblog format)</button>
|
<button class="btn-primary" @click="savePost">Save (ngx-retroblog format)</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -167,6 +138,7 @@ button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 85vh;
|
height: 85vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-bottom: -1.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-area {
|
.editor-area {
|
||||||
|
@ -198,16 +170,22 @@ button {
|
||||||
|
|
||||||
.preview-title {
|
.preview-title {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
border-bottom: solid 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview>* {
|
.preview > * {
|
||||||
max-width: 50vw;
|
max-width: 50vw;
|
||||||
line-break: normal;
|
line-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview > * p {
|
||||||
|
padding: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (hover: none) {
|
@media only screen and (hover: none) {
|
||||||
.wrapper {
|
.wrapper {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin-bottom: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview {
|
.preview {
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
export const parseFilename = (titleToFilename: string): string => {
|
||||||
|
if (!titleToFilename) {
|
||||||
|
return 'None yet.'
|
||||||
|
}
|
||||||
|
|
||||||
|
titleToFilename = titleToFilename.replace(/[^a-zA-Z0-9_]+/gi, '-').toLowerCase();
|
||||||
|
|
||||||
|
while (titleToFilename.endsWith('-')) {
|
||||||
|
titleToFilename = titleToFilename.slice(0, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titleToFilename.length > 50) { // Limit filename size
|
||||||
|
titleToFilename = titleToFilename.substring(0, 50);
|
||||||
|
|
||||||
|
if (titleToFilename.includes('-')) {
|
||||||
|
titleToFilename = titleToFilename.substring(0, Math.min(titleToFilename.length, titleToFilename.lastIndexOf('-'))); // Re-trim to avoid cutting a word in half.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return titleToFilename + '.json';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const downloadFile = (postObj: any, fileName: string, archiveFile?: boolean): void => {
|
||||||
|
const blob = new Blob([JSON.stringify(postObj, null, 2)], { type: 'text/plain;charset=utf-8;' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('download', archiveFile ? 'archive.json' : fileName);
|
||||||
|
a.setAttribute('href', window.URL.createObjectURL(blob));
|
||||||
|
a.click();
|
||||||
|
}
|
|
@ -1,9 +1,8 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { Archive, Post } from './types';
|
||||||
|
|
||||||
const httpService = axios.create({
|
const httpService = axios.create({
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Accept': 'application/json',
|
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'Pragma': 'no-cache',
|
'Pragma': 'no-cache',
|
||||||
'Expires': '0',
|
'Expires': '0',
|
||||||
|
@ -20,13 +19,13 @@ httpService.interceptors.response.use(
|
||||||
|
|
||||||
|
|
||||||
export async function getArchive(baseUrl: string) {
|
export async function getArchive(baseUrl: string) {
|
||||||
return await httpService.get(`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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPost(baseUrl: string, post: string) {
|
export async function getPost(baseUrl: string, post: string) {
|
||||||
return await httpService.get(`https://${baseUrl}/assets/posts/${post}.json`).then((res: any) => {
|
return await httpService.get<Post>(`http://${baseUrl}/assets/posts/${post}.json`).then((res) => {
|
||||||
return res.data;
|
return res.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
export const generateTimestamp = (): string => {
|
||||||
|
return Math.round((new Date()).getTime() / 1000).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseTimestampToUTC = (unixTimestamp: string): string => {
|
||||||
|
if (!unixTimestamp) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(Number(unixTimestamp) * 1000).toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generatePostObj = (
|
||||||
|
postTitle: string,
|
||||||
|
timestamp: string,
|
||||||
|
postContent: string,
|
||||||
|
filename: string,
|
||||||
|
draft: boolean,
|
||||||
|
isEdit: boolean,
|
||||||
|
editedTimestamp: string
|
||||||
|
): Object => {
|
||||||
|
return {
|
||||||
|
postTitle,
|
||||||
|
timestamp: draft ? '' : timestamp,
|
||||||
|
editedTimestamp: draft ? '' : (isEdit ? editedTimestamp : ''),
|
||||||
|
postContent,
|
||||||
|
filename,
|
||||||
|
draft
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export type Post = {
|
||||||
|
postTitle: string,
|
||||||
|
timestamp: string,
|
||||||
|
editedTimestamp: string,
|
||||||
|
postContent: string,
|
||||||
|
filename: string,
|
||||||
|
draft: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Archive = {
|
||||||
|
postTitle: string,
|
||||||
|
timestamp: string,
|
||||||
|
filename: string
|
||||||
|
}
|
|
@ -11,7 +11,8 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"lib": ["esnext", "dom"],
|
"lib": ["esnext", "dom"],
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
|
Loading…
Reference in New Issue