Added images

This commit is contained in:
Torsten Schulz
2024-06-20 07:35:55 +02:00
parent 8c54988023
commit d78bc26e30
12 changed files with 455 additions and 89 deletions

View File

@@ -71,6 +71,21 @@ exports.getImageById = async (req, res) => {
}
};
exports.getImageByHash = async (req, res) => {
try {
const { idHash } = req.params;
const image = await Image.findByPk(idHash);
if (image) {
res.status(200).json(image);
} else {
res.status(404).send('Bild nicht gefunden');
}
} catch (error) {
console.error('Fehler beim Abrufen des Bildes:', error);
res.status(500).send('Fehler beim Abrufen des Bildes');
}
};
exports.updateImage = async (req, res) => {
try {
const { id } = req.params;

View File

@@ -1,6 +1,6 @@
const express = require('express');
const router = express.Router();
const { uploadImage, saveImageDetails, getImages, getImagesByPage, getImageById, updateImage } = require('../controllers/imageController');
const { uploadImage, saveImageDetails, getImages, getImagesByPage, getImageById, getImageByHash, updateImage } = require('../controllers/imageController');
const authMiddleware = require('../middleware/authMiddleware')
router.post('/', authMiddleware, uploadImage, saveImageDetails);
@@ -8,5 +8,5 @@ router.get('/', authMiddleware, getImages);
router.get('/page/:pageId', getImagesByPage);
router.get('/:id', getImageById);
router.put('/:id', authMiddleware, updateImage);
router.put('/hash/:id', getImageByHash);
module.exports = router;

View File

@@ -5,6 +5,7 @@
border: 1px solid black;
margin: 7px;
padding: 5px;
overflow: auto;
}
.htmleditor table {

View File

@@ -0,0 +1,126 @@
<template>
<div>
<div v-if="isOpen" class="dialog-overlay">
<div class="dialog-content">
<h3>Bild auswählen</h3>
<div class="images-container">
<div v-for="image in images" :key="image.id" class="image-block" @click="selectImage(image)" :class="{ selected: image.id === selectedImage?.id }">
<img :src="'/images/uploads/' + image.filename" />
<span v-if="image.description" :title="image.description">{{ image.title }}</span>
<span v-else>{{ image.title }}</span>
</div>
</div>
<div>
<button @click="confirmAddImageConfiguration">Bestätigen</button>
<button @click="closeAddImageDialog">Schließen</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import axios from '@/axios';
export default {
name: 'AddImageDialog',
emits: ['confirm'],
setup(props, { emit }) {
const isOpen = ref(false);
const images = ref([]);
const selectedImage = ref(null);
const openAddImageDialog = () => {
isOpen.value = true;
fetchImages();
};
const closeAddImageDialog = () => {
isOpen.value = false;
};
const confirmAddImageConfiguration = () => {
if (selectedImage.value) {
console.log('->', selectImage.value);
emit('confirm', `${selectedImage.value.id}`);
}
closeAddImageDialog();
};
const fetchImages = async () => {
try {
const response = await axios.get('/image');
images.value = response.data;
} catch (error) {
console.error('Fehler beim Laden der Bilder:', error);
images.value = [];
}
};
const selectImage = (image) => {
console.log(image);
selectedImage.value = image;
};
return {
isOpen,
images,
selectedImage,
openAddImageDialog,
closeAddImageDialog,
confirmAddImageConfiguration,
selectImage,
};
},
};
</script>
<style scoped>
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-content {
background: white;
padding: 20px;
border-radius: 8px;
}
.images-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.images-container img {
cursor: pointer;
border: 2px solid transparent;
}
.images-container img:hover {
border-color: #007bff;
}
.image-block {
display: inline-block;
margin: 2.5px;
}
.image-block img {
max-width: 150px;
max-height: 150px;
}
.selected {
border: 2px solid black;
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<img v-if="image.filename" :src="`/images/uploads/${image.filename}`" :alt="image.title" :title="image.title"
class="image" />
</template>
<script>
import axios from '@/axios';
import { reactive } from 'vue';
export default {
name: 'ImageRender',
props: {
id: {
type: Number,
required: true,
},
},
setup(props) {
const image = reactive({
filename: '',
title: '',
description: '',
uploadDate: '',
pageId: null,
});
const fetchImage = async () => {
try {
const response = await axios.get('/image/' + props.id);
Object.assign(image, response.data);
} catch (error) {
console.log('Fehler beim Abrufen eines Bildes', error);
}
};
fetchImage();
return {
image,
};
},
};
</script>
<style scoped>
.image {
max-width: 400px;
max-height: 300px;
}
</style>

View File

@@ -1,74 +1,100 @@
<template>
<div>
<component :is="dynamicComponent" v-bind="componentProps"></component>
</div>
</template>
<div v-html="parsedContent"></div>
</template>
<script>
import { createApp, h } from 'vue';
import WorshipRender from './WorshipRender.vue';
<script>
import { createApp, h, ref, watch } from 'vue';
import WorshipRender from './WorshipRender.vue';
import ImageRender from './ImageRender.vue';
export default {
export default {
name: 'RenderContentComponent',
props: {
content: {
type: String,
required: true
}
required: true,
},
data() {
return {
dynamicComponent: null,
componentProps: {}
},
setup(props) {
const parsedContent = ref('');
const renderContent = (content) => {
let result = renderWorship(content);
result = renderImage(content);
return result;
};
},
watch: {
content: {
immediate: true,
handler(newContent) {
this.renderContent(newContent);
}
}
},
methods: {
renderContent(content) {
const renderWorship = (content) => {
const worshipsPattern = /{{ worshipslist:(.*?) }}/g;
content.replace(worshipsPattern, (match, config) => {
const props = this.parseConfig(config);
this.dynamicComponent = WorshipRender;
this.componentProps = props;
return '<div id="worship-render-placeholder"></div>';
});
this.$nextTick(() => {
if (this.dynamicComponent) {
const placeholder = document.getElementById('worship-render-placeholder');
let result = content;
result = result.replace(worshipsPattern, (match, config) => {
const props = parseConfig(config);
const placeholderId = `worship-render-placeholder-${Math.random().toString(36).substr(2, 9)}`;
setTimeout(() => {
const placeholder = document.getElementById(placeholderId);
if (placeholder) {
const app = createApp({
render() {
return h(this.dynamicComponent, this.componentProps);
}
return h(WorshipRender, props);
},
});
app.mount(placeholder);
}
}
}, 0);
return `<div id="${placeholderId}"></div>`;
});
return result;
}
const renderImage = (content) => {
const imagePattern = /{{ image:(.*?) }}/g;
let result = content;
result = result.replace(imagePattern, (match, config) => {
const placeholderId = `image-render-placeholder-${Math.random().toString(36).substr(2, 9)}`;
setTimeout(() => {
const placeholder = document.getElementById(placeholderId);
if (placeholder) {
const app = createApp({
render() {
console.log(config);
return h(ImageRender, { 'id': config });
},
parseConfig(configString) {
});
app.mount(placeholder);
}
}, 0);
return `<span id="${placeholderId}"></span>`;
});
return result;
}
const parseConfig = (configString) => {
const config = {};
const configArray = configString.split(',');
configArray.forEach(item => {
configArray.forEach((item) => {
const [key, value] = item.split('=');
if (key && value !== undefined) {
config[key.trim()] = isNaN(value) ? value.trim() : Number(value);
}
});
return config;
}
}
};
</script>
<style scoped>
/* Add styles if needed */
</style>
watch(
() => props.content,
(newContent) => {
parsedContent.value = renderContent(newContent);
},
{ immediate: true }
);
return {
parsedContent,
};
},
};
</script>
<style scoped>
/* Add styles if needed */
</style>

View File

@@ -20,22 +20,51 @@
<button @click="editor.chain().focus().toggleUnderline().run()">
<UnderlineIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().toggleStrike().run()">Durchgestrichen</button>
<button
@click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">Tabelle</button>
<button @click="editor.chain().focus().toggleBulletList().run()">Liste</button>
<button @click="editor.chain().focus().toggleOrderedList().run()">Nummerierte Liste</button>
<button @click="editor.chain().focus().toggleStrike().run()">
<StrikethroughIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">
<TableIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().toggleBulletList().run()">
<ListIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().toggleOrderedList().run()">
<NumberedListLeftIcon width="24" height="24" />
</button>
<button @click="openAddImageDialog">
<StatsReportIcon width="24" height="24" />
</button>
</div>
<div class="table-toolbar">
<button @click="editor.chain().focus().addColumnBefore().run()">Spalte davor einfügen</button>
<button @click="editor.chain().focus().addColumnAfter().run()">Spalte danach einfügen</button>
<button @click="editor.chain().focus().addRowBefore().run()">Zeile davor einfügen</button>
<button @click="editor.chain().focus().addRowAfter().run()">Zeile danach einfügen</button>
<button @click="editor.chain().focus().deleteColumn().run()">Spalte löschen</button>
<button @click="editor.chain().focus().deleteRow().run()">Zeile löschen</button>
<button @click="editor.chain().focus().toggleHeaderColumn().run()">Header-Spalte umschalten</button>
<button @click="editor.chain().focus().toggleHeaderRow().run()">Header-Zeile umschalten</button>
<button @click="editor.chain().focus().toggleHeaderCell().run()">Header-Zelle umschalten</button>
<button @click="editor.chain().focus().addColumnBefore().run()">
<ArrowDownIcon width="10" height="10" class="align-top" />
<Table2ColumnsIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().addColumnAfter().run()">
<Table2ColumnsIcon width="24" height="24" />
<ArrowDownIcon width="10" height="10" class="align-top" />
</button>
<button @click="editor.chain().focus().addRowBefore().run()">
<ArrowRightIcon width="10" height="10" class="align-top" />
<TableRowsIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().addRowAfter().run()">
<ArrowRightIcon width="10" height="10" />
<TableRowsIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().deleteColumn().run()">
<Table2ColumnsIcon width="24" height="24" class="delete-icon" />
</button>
<button @click="editor.chain().focus().deleteRow().run()">
<TableRowsIcon width="24" height="24" class="delete-icon" />
</button>
<button @click="editor.chain().focus().toggleHeaderColumn().run()">
<AlignTopBoxIcon width="24" height="24" />
</button>
<button @click="editor.chain().focus().toggleHeaderRow().run()">
<AlignLeftBoxIcon width="24" height="24" />
</button>
</div>
<div class="additional-toolbar">
<button>Events</button>
@@ -49,6 +78,7 @@
<button @click="savePageContent">Speichern</button>
<WorshipDialog ref="worshipDialog" @confirm="insertWorshipList" />
<AddImageDialog ref="addImageDialog" @confirm="insertImage" />
</div>
</template>
@@ -69,16 +99,32 @@ import OrderedList from '@tiptap/extension-ordered-list';
import Heading from '@tiptap/extension-heading';
import { CustomTableCell, CustomTableHeader } from '../../extensions/CustomTableCell';
import WorshipDialog from '@/components/WorshipDialog.vue';
import { BoldIcon, ItalicIcon, UnderlineIcon } from '@/icons';
import AddImageDialog from '@/components/AddImageDialog.vue';
import { BoldIcon, ItalicIcon, UnderlineIcon, StrikethroughIcon, ListIcon, NumberedListLeftIcon, TableIcon,
Table2ColumnsIcon, ArrowDownIcon, ArrowRightIcon, TableRowsIcon, AlignTopBoxIcon, AlignLeftBoxIcon, StatsReportIcon
} from '@/icons';
export default {
name: 'EditPagesComponent',
components: {
EditorContent,
WorshipDialog,
AddImageDialog,
BoldIcon,
ItalicIcon,
UnderlineIcon
UnderlineIcon,
StrikethroughIcon,
ListIcon,
NumberedListLeftIcon,
TableIcon,
Table2ColumnsIcon,
ArrowDownIcon,
ArrowRightIcon,
TableRowsIcon,
AlignTopBoxIcon,
AlignLeftBoxIcon,
StatsReportIcon,
},
setup() {
const store = useStore();
@@ -86,6 +132,7 @@ export default {
const selectedPage = ref('');
const pageHtmlContent = computed(() => store.state.pageContent);
const worshipDialog = ref(null);
const addImageDialog = ref(null);
const editor = useEditor({
extensions: [
@@ -190,6 +237,10 @@ export default {
worshipDialog.value.openWorshipDialog();
};
const openAddImageDialog = () => {
addImageDialog.value.openAddImageDialog();
};
const insertWorshipList = (selectedLocations) => {
if (editor.value) {
const configuration = `location=${selectedLocations},order:"date asc"`;
@@ -197,6 +248,12 @@ export default {
}
};
const insertImage = (selectedImage) => {
if (editor.value) {
editor.value.chain().focus().insertContent(`{{ image:${selectedImage} }}`).run();
}
};
return {
pages,
sortedPages,
@@ -208,6 +265,9 @@ export default {
openWorshipDialog,
insertWorshipList,
worshipDialog,
addImageDialog,
openAddImageDialog,
insertImage,
};
},
};
@@ -269,4 +329,10 @@ export default {
width: 24px;
height: 24px;
}
.delete-icon {
fill: #ff0000;
}
.align-top {
vertical-align: top;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="some-page">
<ContentComponent :link="currentLink" />
</div>
</template>
<script>
import ContentComponent from '@/components/ContentComponent.vue';
export default {
name: 'KitaAmBuegel',
components: {
ContentComponent,
},
computed: {
currentLink() {
return this.$route.path;
},
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="some-page">
<ContentComponent :link="currentLink" />
</div>
</template>
<script>
import ContentComponent from '@/components/ContentComponent.vue';
export default {
name: 'KitaAmBuegel',
components: {
ContentComponent,
},
computed: {
currentLink() {
return this.$route.path;
},
},
};
</script>
<style scoped></style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="some-page">
<ContentComponent :link="currentLink" />
</div>
</template>
<script>
import ContentComponent from '@/components/ContentComponent.vue';
export default {
name: 'KitaAmBuegel',
components: {
ContentComponent,
},
computed: {
currentLink() {
return this.$route.path;
},
},
};
</script>
<style scoped></style>

View File

@@ -1,8 +1,21 @@
// src/icons.js
import { Bold, Italic, Underline } from '@iconoir/vue';
import { Bold, Italic, Underline, Strikethrough, List, NumberedListLeft, Table, Table2Columns, ArrowRight, ArrowDown, TableRows,
AlignTopBox, AlignLeftBox, StatsReport
} from '@iconoir/vue';
export {
Bold as BoldIcon,
Italic as ItalicIcon,
Underline as UnderlineIcon,
Strikethrough as StrikethroughIcon,
List as ListIcon,
NumberedListLeft as NumberedListLeftIcon,
Table as TableIcon,
Table2Columns as Table2ColumnsIcon,
ArrowRight as ArrowRightIcon,
ArrowDown as ArrowDownIcon,
TableRows as TableRowsIcon,
AlignTopBox as AlignTopBoxIcon,
AlignLeftBox as AlignLeftBoxIcon,
StatsReport as StatsReportIcon
};

View File

@@ -4,7 +4,7 @@ const webpack = require('webpack');
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
host: '127.0.0.1',
host: 'localhost',
port: 8080
},
configureWebpack: {