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) => { exports.updateImage = async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;

View File

@@ -1,6 +1,6 @@
const express = require('express'); const express = require('express');
const router = express.Router(); 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') const authMiddleware = require('../middleware/authMiddleware')
router.post('/', authMiddleware, uploadImage, saveImageDetails); router.post('/', authMiddleware, uploadImage, saveImageDetails);
@@ -8,5 +8,5 @@ router.get('/', authMiddleware, getImages);
router.get('/page/:pageId', getImagesByPage); router.get('/page/:pageId', getImagesByPage);
router.get('/:id', getImageById); router.get('/:id', getImageById);
router.put('/:id', authMiddleware, updateImage); router.put('/:id', authMiddleware, updateImage);
router.put('/hash/:id', getImageByHash);
module.exports = router; module.exports = router;

View File

@@ -5,6 +5,7 @@
border: 1px solid black; border: 1px solid black;
margin: 7px; margin: 7px;
padding: 5px; padding: 5px;
overflow: auto;
} }
.htmleditor table { .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> <template>
<div> <div v-html="parsedContent"></div>
<component :is="dynamicComponent" v-bind="componentProps"></component> </template>
</div>
</template>
<script> <script>
import { createApp, h } from 'vue'; import { createApp, h, ref, watch } from 'vue';
import WorshipRender from './WorshipRender.vue'; import WorshipRender from './WorshipRender.vue';
import ImageRender from './ImageRender.vue';
export default { export default {
name: 'RenderContentComponent', name: 'RenderContentComponent',
props: { props: {
content: { content: {
type: String, type: String,
required: true required: true,
}
}, },
data() { },
return { setup(props) {
dynamicComponent: null, const parsedContent = ref('');
componentProps: {}
const renderContent = (content) => {
let result = renderWorship(content);
result = renderImage(content);
return result;
}; };
},
watch: { const renderWorship = (content) => {
content: {
immediate: true,
handler(newContent) {
this.renderContent(newContent);
}
}
},
methods: {
renderContent(content) {
const worshipsPattern = /{{ worshipslist:(.*?) }}/g; const worshipsPattern = /{{ worshipslist:(.*?) }}/g;
content.replace(worshipsPattern, (match, config) => { let result = content;
const props = this.parseConfig(config); result = result.replace(worshipsPattern, (match, config) => {
this.dynamicComponent = WorshipRender; const props = parseConfig(config);
this.componentProps = props; const placeholderId = `worship-render-placeholder-${Math.random().toString(36).substr(2, 9)}`;
return '<div id="worship-render-placeholder"></div>'; setTimeout(() => {
}); const placeholder = document.getElementById(placeholderId);
this.$nextTick(() => {
if (this.dynamicComponent) {
const placeholder = document.getElementById('worship-render-placeholder');
if (placeholder) { if (placeholder) {
const app = createApp({ const app = createApp({
render() { render() {
return h(this.dynamicComponent, this.componentProps); return h(WorshipRender, props);
} },
}); });
app.mount(placeholder); 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 config = {};
const configArray = configString.split(','); const configArray = configString.split(',');
configArray.forEach(item => { configArray.forEach((item) => {
const [key, value] = item.split('='); const [key, value] = item.split('=');
if (key && value !== undefined) { if (key && value !== undefined) {
config[key.trim()] = isNaN(value) ? value.trim() : Number(value); config[key.trim()] = isNaN(value) ? value.trim() : Number(value);
} }
}); });
return config; return config;
}
}
}; };
</script>
<style scoped> watch(
/* Add styles if needed */ () => props.content,
</style> (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()"> <button @click="editor.chain().focus().toggleUnderline().run()">
<UnderlineIcon width="24" height="24" /> <UnderlineIcon width="24" height="24" />
</button> </button>
<button @click="editor.chain().focus().toggleStrike().run()">Durchgestrichen</button> <button @click="editor.chain().focus().toggleStrike().run()">
<button <StrikethroughIcon width="24" height="24" />
@click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">Tabelle</button> </button>
<button @click="editor.chain().focus().toggleBulletList().run()">Liste</button> <button @click="editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run()">
<button @click="editor.chain().focus().toggleOrderedList().run()">Nummerierte Liste</button> <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>
<div class="table-toolbar"> <div class="table-toolbar">
<button @click="editor.chain().focus().addColumnBefore().run()">Spalte davor einfügen</button> <button @click="editor.chain().focus().addColumnBefore().run()">
<button @click="editor.chain().focus().addColumnAfter().run()">Spalte danach einfügen</button> <ArrowDownIcon width="10" height="10" class="align-top" />
<button @click="editor.chain().focus().addRowBefore().run()">Zeile davor einfügen</button> <Table2ColumnsIcon width="24" height="24" />
<button @click="editor.chain().focus().addRowAfter().run()">Zeile danach einfügen</button> </button>
<button @click="editor.chain().focus().deleteColumn().run()">Spalte löschen</button> <button @click="editor.chain().focus().addColumnAfter().run()">
<button @click="editor.chain().focus().deleteRow().run()">Zeile löschen</button> <Table2ColumnsIcon width="24" height="24" />
<button @click="editor.chain().focus().toggleHeaderColumn().run()">Header-Spalte umschalten</button> <ArrowDownIcon width="10" height="10" class="align-top" />
<button @click="editor.chain().focus().toggleHeaderRow().run()">Header-Zeile umschalten</button> </button>
<button @click="editor.chain().focus().toggleHeaderCell().run()">Header-Zelle umschalten</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>
<div class="additional-toolbar"> <div class="additional-toolbar">
<button>Events</button> <button>Events</button>
@@ -49,6 +78,7 @@
<button @click="savePageContent">Speichern</button> <button @click="savePageContent">Speichern</button>
<WorshipDialog ref="worshipDialog" @confirm="insertWorshipList" /> <WorshipDialog ref="worshipDialog" @confirm="insertWorshipList" />
<AddImageDialog ref="addImageDialog" @confirm="insertImage" />
</div> </div>
</template> </template>
@@ -69,16 +99,32 @@ import OrderedList from '@tiptap/extension-ordered-list';
import Heading from '@tiptap/extension-heading'; import Heading from '@tiptap/extension-heading';
import { CustomTableCell, CustomTableHeader } from '../../extensions/CustomTableCell'; import { CustomTableCell, CustomTableHeader } from '../../extensions/CustomTableCell';
import WorshipDialog from '@/components/WorshipDialog.vue'; 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 { export default {
name: 'EditPagesComponent', name: 'EditPagesComponent',
components: { components: {
EditorContent, EditorContent,
WorshipDialog, WorshipDialog,
AddImageDialog,
BoldIcon, BoldIcon,
ItalicIcon, ItalicIcon,
UnderlineIcon UnderlineIcon,
StrikethroughIcon,
ListIcon,
NumberedListLeftIcon,
TableIcon,
Table2ColumnsIcon,
ArrowDownIcon,
ArrowRightIcon,
TableRowsIcon,
AlignTopBoxIcon,
AlignLeftBoxIcon,
StatsReportIcon,
}, },
setup() { setup() {
const store = useStore(); const store = useStore();
@@ -86,6 +132,7 @@ export default {
const selectedPage = ref(''); const selectedPage = ref('');
const pageHtmlContent = computed(() => store.state.pageContent); const pageHtmlContent = computed(() => store.state.pageContent);
const worshipDialog = ref(null); const worshipDialog = ref(null);
const addImageDialog = ref(null);
const editor = useEditor({ const editor = useEditor({
extensions: [ extensions: [
@@ -190,6 +237,10 @@ export default {
worshipDialog.value.openWorshipDialog(); worshipDialog.value.openWorshipDialog();
}; };
const openAddImageDialog = () => {
addImageDialog.value.openAddImageDialog();
};
const insertWorshipList = (selectedLocations) => { const insertWorshipList = (selectedLocations) => {
if (editor.value) { if (editor.value) {
const configuration = `location=${selectedLocations},order:"date asc"`; 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 { return {
pages, pages,
sortedPages, sortedPages,
@@ -208,6 +265,9 @@ export default {
openWorshipDialog, openWorshipDialog,
insertWorshipList, insertWorshipList,
worshipDialog, worshipDialog,
addImageDialog,
openAddImageDialog,
insertImage,
}; };
}, },
}; };
@@ -269,4 +329,10 @@ export default {
width: 24px; width: 24px;
height: 24px; height: 24px;
} }
.delete-icon {
fill: #ff0000;
}
.align-top {
vertical-align: top;
}
</style> </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 // 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 { export {
Bold as BoldIcon, Bold as BoldIcon,
Italic as ItalicIcon, Italic as ItalicIcon,
Underline as UnderlineIcon, 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({ module.exports = defineConfig({
transpileDependencies: true, transpileDependencies: true,
devServer: { devServer: {
host: '127.0.0.1', host: 'localhost',
port: 8080 port: 8080
}, },
configureWebpack: { configureWebpack: {