mktcms 0.1.21 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/module.mjs +11 -6
  3. package/dist/runtime/app/components/admin.vue +1 -1
  4. package/dist/runtime/app/components/content/breadcrumb.d.vue.ts +1 -4
  5. package/dist/runtime/app/components/content/breadcrumb.vue +25 -9
  6. package/dist/runtime/app/components/content/breadcrumb.vue.d.ts +1 -4
  7. package/dist/runtime/app/components/content/delete.vue +69 -0
  8. package/dist/runtime/app/components/content/dirs.d.vue.ts +0 -1
  9. package/dist/runtime/app/components/content/dirs.vue +19 -3
  10. package/dist/runtime/app/components/content/dirs.vue.d.ts +0 -1
  11. package/dist/runtime/app/components/content/editor/csv.vue +284 -0
  12. package/dist/runtime/app/components/content/editor/image.vue +27 -0
  13. package/dist/runtime/app/components/content/editor/{text/markdown.vue → markdown.vue} +30 -8
  14. package/dist/runtime/app/components/content/editor/pdf.d.vue.ts +3 -0
  15. package/dist/runtime/app/components/content/editor/pdf.vue +26 -0
  16. package/dist/runtime/app/components/content/editor/pdf.vue.d.ts +3 -0
  17. package/dist/runtime/app/components/content/editor/txt.d.vue.ts +3 -0
  18. package/dist/runtime/app/components/content/editor/txt.vue +24 -0
  19. package/dist/runtime/app/components/content/editor/txt.vue.d.ts +3 -0
  20. package/dist/runtime/app/components/content/fileButtons.d.vue.ts +6 -0
  21. package/dist/runtime/app/components/content/fileButtons.vue +73 -0
  22. package/dist/runtime/app/components/content/fileButtons.vue.d.ts +6 -0
  23. package/dist/runtime/app/components/content/fileIcon.d.vue.ts +6 -0
  24. package/dist/runtime/app/components/content/fileIcon.vue +80 -0
  25. package/dist/runtime/app/components/content/fileIcon.vue.d.ts +6 -0
  26. package/dist/runtime/app/components/content/files.d.vue.ts +0 -1
  27. package/dist/runtime/app/components/content/files.vue +10 -91
  28. package/dist/runtime/app/components/content/files.vue.d.ts +0 -1
  29. package/dist/runtime/app/components/content/index.vue +3 -8
  30. package/dist/runtime/app/components/content/saved.d.vue.ts +3 -0
  31. package/dist/runtime/app/components/content/saved.vue +19 -0
  32. package/dist/runtime/app/components/content/saved.vue.d.ts +3 -0
  33. package/dist/runtime/app/components/content/upload.vue +18 -12
  34. package/dist/runtime/app/components/header.vue +55 -25
  35. package/dist/runtime/app/composables/useFileType.d.ts +7 -0
  36. package/dist/runtime/app/composables/useFileType.js +14 -0
  37. package/dist/runtime/app/composables/usePathParam.d.ts +9 -0
  38. package/dist/runtime/app/composables/usePathParam.js +16 -0
  39. package/dist/runtime/app/composables/useSaveContent.d.ts +7 -0
  40. package/dist/runtime/app/composables/useSaveContent.js +31 -0
  41. package/dist/runtime/app/pages/admin/delete/[path].d.vue.ts +3 -0
  42. package/dist/runtime/app/pages/admin/delete/[path].vue +14 -0
  43. package/dist/runtime/app/pages/admin/delete/[path].vue.d.ts +3 -0
  44. package/dist/runtime/app/pages/admin/edit/[path].d.vue.ts +3 -0
  45. package/dist/runtime/app/pages/admin/edit/[path].vue +24 -0
  46. package/dist/runtime/app/pages/admin/edit/[path].vue.d.ts +3 -0
  47. package/dist/runtime/app/pages/admin/index.vue +2 -0
  48. package/dist/runtime/app/pages/admin/login.vue +68 -21
  49. package/dist/runtime/app/styles/admin.css +1 -0
  50. package/dist/runtime/app/styles/admin.min.css +1 -0
  51. package/dist/runtime/app/util/csv.d.ts +13 -0
  52. package/dist/runtime/app/util/csv.js +38 -0
  53. package/dist/runtime/server/api/admin/content/[path].delete.d.ts +4 -0
  54. package/dist/runtime/server/api/admin/content/[path].delete.js +14 -0
  55. package/dist/runtime/server/api/admin/content/[path].js +0 -5
  56. package/dist/runtime/server/api/admin/content/list.js +0 -7
  57. package/dist/runtime/server/api/content/[path].js +0 -5
  58. package/dist/runtime/server/api/content/list.js +0 -7
  59. package/dist/runtime/server/plugins/storage.js +0 -3
  60. package/package.json +10 -2
  61. package/dist/runtime/app/components/content/editor/blob/image.d.vue.ts +0 -10
  62. package/dist/runtime/app/components/content/editor/blob/image.vue +0 -12
  63. package/dist/runtime/app/components/content/editor/blob/image.vue.d.ts +0 -10
  64. package/dist/runtime/app/components/content/editor/blob/index.vue +0 -41
  65. package/dist/runtime/app/components/content/editor/text/csv.d.vue.ts +0 -10
  66. package/dist/runtime/app/components/content/editor/text/csv.vue +0 -235
  67. package/dist/runtime/app/components/content/editor/text/csv.vue.d.ts +0 -10
  68. package/dist/runtime/app/components/content/editor/text/index.vue +0 -58
  69. package/dist/runtime/app/components/content/editor/text/markdown.d.vue.ts +0 -10
  70. package/dist/runtime/app/components/content/editor/text/markdown.vue.d.ts +0 -10
  71. package/dist/runtime/app/pages/admin/edit/blob/[path].vue +0 -12
  72. package/dist/runtime/app/pages/admin/edit/text/[path].vue +0 -12
  73. /package/dist/runtime/app/components/content/{editor/blob/index.d.vue.ts → delete.d.vue.ts} +0 -0
  74. /package/dist/runtime/app/components/content/{editor/blob/index.vue.d.ts → delete.vue.d.ts} +0 -0
  75. /package/dist/runtime/app/components/content/editor/{text/index.d.vue.ts → csv.d.vue.ts} +0 -0
  76. /package/dist/runtime/app/components/content/editor/{text/index.vue.d.ts → csv.vue.d.ts} +0 -0
  77. /package/dist/runtime/app/{pages/admin/edit/blob/[path].d.vue.ts → components/content/editor/image.d.vue.ts} +0 -0
  78. /package/dist/runtime/app/{pages/admin/edit/blob/[path].vue.d.ts → components/content/editor/image.vue.d.ts} +0 -0
  79. /package/dist/runtime/app/{pages/admin/edit/text/[path].d.vue.ts → components/content/editor/markdown.d.vue.ts} +0 -0
  80. /package/dist/runtime/app/{pages/admin/edit/text/[path].vue.d.ts → components/content/editor/markdown.vue.d.ts} +0 -0
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mktcms",
3
3
  "configKey": "mktcms",
4
- "version": "0.1.21",
4
+ "version": "0.1.22",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -58,6 +58,11 @@ const module$1 = defineNuxtModule({
58
58
  method: "post",
59
59
  handler: resolver.resolve("./runtime/server/api/admin/content/[path].post")
60
60
  });
61
+ addServerHandler({
62
+ route: "/api/admin/content/:path",
63
+ method: "delete",
64
+ handler: resolver.resolve("./runtime/server/api/admin/content/[path].delete")
65
+ });
61
66
  addServerHandler({
62
67
  route: "/api/admin/content/upload",
63
68
  handler: resolver.resolve("./runtime/server/api/admin/content/upload")
@@ -77,14 +82,14 @@ const module$1 = defineNuxtModule({
77
82
  file: resolver.resolve("./runtime/app/pages/admin/index.vue")
78
83
  });
79
84
  pages.push({
80
- name: "Admin Text Editor",
81
- path: "/admin/edit/text/:path",
82
- file: resolver.resolve("./runtime/app/pages/admin/edit/text/[path].vue")
85
+ name: "Admin Editor",
86
+ path: "/admin/edit/:path",
87
+ file: resolver.resolve("./runtime/app/pages/admin/edit/[path].vue")
83
88
  });
84
89
  pages.push({
85
- name: "Admin Blob Editor",
86
- path: "/admin/edit/blob/:path",
87
- file: resolver.resolve("./runtime/app/pages/admin/edit/blob/[path].vue")
90
+ name: "Admin Delete",
91
+ path: "/admin/delete/:path",
92
+ file: resolver.resolve("./runtime/app/pages/admin/delete/[path].vue")
88
93
  });
89
94
  pages.push({
90
95
  name: "Admin New Content",
@@ -5,5 +5,5 @@
5
5
  </template>
6
6
 
7
7
  <style>
8
- *{box-sizing:border-box}body{font-family:Arial,sans-serif;margin:0}#mktcms-admin{--mktcms-primary-color:#3cb371;--mktcms-primary-color-hover:#45a049;background-color:#f9f9f9;margin:0 auto;max-width:800px;padding:20px}#mktcms-admin h1{color:#333}#mktcms-admin .button,#mktcms-admin button{background-color:var(--mktcms-primary-color);border:none;border-radius:5px;color:#fff;cursor:pointer;font-size:16px;padding:10px 20px;text-align:center;text-decoration:none;transition:background-color .3s}#mktcms-admin .button:hover,#mktcms-admin button:hover{background-color:var(--mktcms-primary-color-hover)}#mktcms-admin .button.soft,#mktcms-admin button.soft{background-color:#eee;color:#666}#mktcms-admin .button.soft:hover,#mktcms-admin button.soft:hover{background-color:#ddd}#mktcms-admin .button-icon{height:20px;vertical-align:middle;width:20px}#mktcms-admin input[type=email],#mktcms-admin input[type=password],#mktcms-admin input[type=text],#mktcms-admin select,#mktcms-admin textarea{background-color:#fff;border:1px solid #ccc;border-radius:5px;font-size:16px;padding:10px;width:100%}#mktcms-admin .breadcrumbs{color:#888;font-size:1rem;margin:20px 0}#mktcms-admin .breadcrumbs a{background-color:#eee;border-radius:4px;color:#888;padding:2px 6px;text-decoration:none}#mktcms-admin .breadcrumbs a:hover{text-decoration:underline}#mktcms-admin .dirs,#mktcms-admin .files{display:flex;flex-direction:column;gap:8px}#mktcms-admin .dirs a,#mktcms-admin .files a{border-radius:4px;display:block;padding:8px 12px;text-decoration:none}#mktcms-admin .dirs a:hover,#mktcms-admin .files a:hover{text-decoration:underline}#mktcms-admin .files a{background-color:#fff;color:#555}#mktcms-admin .dirs a{background-color:#555;color:#fff;display:flex;justify-content:space-between}
8
+ @import "../styles/admin.min.css";
9
9
  </style>
@@ -1,6 +1,3 @@
1
- type __VLS_Props = {
2
- parts: string[];
3
- };
4
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
5
2
  declare const _default: typeof __VLS_export;
6
3
  export default _default;
@@ -1,20 +1,36 @@
1
1
  <script setup>
2
- defineProps({
3
- parts: { type: Array, required: true }
4
- });
2
+ import { useRoute } from "#app";
3
+ const path = useRoute().params.path || "";
4
+ const parts = path.split(":").filter((part) => part.length > 0);
5
+ parts.unshift("Hauptordner");
5
6
  </script>
6
7
 
7
8
  <template>
8
- <div class="breadcrumbs">
9
- <NuxtLink to="/admin">Hauptordner</NuxtLink>
10
- <span
9
+ <div class="text-gray-500 text-base mt-12 mb-6 flex items-center gap-1">
10
+ <div
11
11
  v-for="(part, index) in parts"
12
12
  :key="index"
13
+ class="flex items-center gap-1"
13
14
  >
14
- /
15
+ <svg
16
+ v-if="index > 0"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ fill="none"
19
+ viewBox="0 0 24 24"
20
+ stroke-width="1.5"
21
+ stroke="currentColor"
22
+ class="size-6"
23
+ >
24
+ <path
25
+ stroke-linecap="round"
26
+ stroke-linejoin="round"
27
+ d="m8.25 4.5 7.5 7.5-7.5 7.5"
28
+ />
29
+ </svg>
15
30
  <NuxtLink
16
31
  v-if="index < parts.length - 1"
17
- :to="`/admin/${parts.slice(0, index + 1).join(':')}`"
32
+ :to="`/admin/${index > 0 ? parts.slice(1, index + 1).join(':') : ''}`"
33
+ class="button secondary small"
18
34
  >
19
35
  {{ part }}
20
36
  </NuxtLink>
@@ -24,6 +40,6 @@ defineProps({
24
40
  >
25
41
  {{ part }}
26
42
  </span>
27
- </span>
43
+ </div>
28
44
  </div>
29
45
  </template>
@@ -1,6 +1,3 @@
1
- type __VLS_Props = {
2
- parts: string[];
3
- };
4
- declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
5
2
  declare const _default: typeof __VLS_export;
6
3
  export default _default;
@@ -0,0 +1,69 @@
1
+ <script setup>
2
+ import { navigateTo } from "#app";
3
+ import usePathParam from "../../composables/usePathParam";
4
+ const { path, pathParts } = usePathParam();
5
+ const parentPath = pathParts.slice(0, -1).join(":");
6
+ async function deleteContent() {
7
+ await fetch(`/api/admin/content/${path}`, {
8
+ method: "DELETE"
9
+ });
10
+ await navigateTo(`/admin/${parentPath}`);
11
+ }
12
+ </script>
13
+
14
+ <template>
15
+ <div>
16
+ <h1 class="text-2xl font-bold mb-4">
17
+ Datei löschen
18
+ </h1>
19
+ <p class="mb-4">
20
+ Sind Sie sicher, dass Sie
21
+ <strong>{{ pathParts[pathParts.length - 1] }}</strong>
22
+ aus
23
+ <strong>{{ parentPath || "Hauptordner" }}</strong>
24
+ löschen möchten? Die Datei kann nicht wiederhergestellt werden!
25
+ </p>
26
+ <div class="flex gap-2">
27
+ <NuxtLink
28
+ :to="`/admin/${parentPath}`"
29
+ class="button secondary flex-1"
30
+ >
31
+ <svg
32
+ xmlns="http://www.w3.org/2000/svg"
33
+ fill="none"
34
+ viewBox="0 0 24 24"
35
+ stroke-width="1.5"
36
+ stroke="currentColor"
37
+ class="size-5"
38
+ >
39
+ <path
40
+ stroke-linecap="round"
41
+ stroke-linejoin="round"
42
+ d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"
43
+ />
44
+ </svg>
45
+ Abbrechen
46
+ </NuxtLink>
47
+ <button
48
+ class="button danger"
49
+ @click="deleteContent"
50
+ >
51
+ <svg
52
+ xmlns="http://www.w3.org/2000/svg"
53
+ fill="none"
54
+ viewBox="0 0 24 24"
55
+ stroke-width="1.5"
56
+ stroke="currentColor"
57
+ class="size-5"
58
+ >
59
+ <path
60
+ stroke-linecap="round"
61
+ stroke-linejoin="round"
62
+ d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
63
+ />
64
+ </svg>
65
+ Datei löschen
66
+ </button>
67
+ </div>
68
+ </div>
69
+ </template>
@@ -1,5 +1,4 @@
1
1
  type __VLS_Props = {
2
- path: string;
3
2
  dirs: string[];
4
3
  };
5
4
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,21 +1,37 @@
1
1
  <script setup>
2
+ import usePathParam from "../../composables/usePathParam";
2
3
  defineProps({
3
- path: { type: String, required: true },
4
4
  dirs: { type: Array, required: true }
5
5
  });
6
+ const { path } = usePathParam();
6
7
  </script>
7
8
 
8
9
  <template>
9
- <div class="dirs">
10
+ <div class="flex flex-col gap-2">
10
11
  <NuxtLink
11
12
  v-for="dir in dirs"
12
13
  :key="dir"
13
14
  :to="`/admin/${path ? path + ':' : ''}${dir}`"
15
+ class="button secondary"
14
16
  >
17
+ <svg
18
+ xmlns="http://www.w3.org/2000/svg"
19
+ fill="none"
20
+ viewBox="0 0 24 24"
21
+ stroke-width="1.5"
22
+ stroke="currentColor"
23
+ class="size-6 opacity-20"
24
+ >
25
+ <path
26
+ stroke-linecap="round"
27
+ stroke-linejoin="round"
28
+ d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 0 0-1.883 2.542l.857 6a2.25 2.25 0 0 0 2.227 1.932H19.05a2.25 2.25 0 0 0 2.227-1.932l.857-6a2.25 2.25 0 0 0-1.883-2.542m-16.5 0V6A2.25 2.25 0 0 1 6 3.75h3.879a1.5 1.5 0 0 1 1.06.44l2.122 2.12a1.5 1.5 0 0 0 1.06.44H18A2.25 2.25 0 0 1 20.25 9v.776"
29
+ />
30
+ </svg>
15
31
  <span>
16
32
  {{ dir }}
17
33
  </span>
18
- <span>
34
+ <span class="ml-auto">
19
35
  <svg
20
36
  xmlns="http://www.w3.org/2000/svg"
21
37
  fill="none"
@@ -1,5 +1,4 @@
1
1
  type __VLS_Props = {
2
- path: string;
3
2
  dirs: string[];
4
3
  };
5
4
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,284 @@
1
+ <script setup>
2
+ import { computed, ref } from "vue";
3
+ import useSaveContent from "../../../composables/useSaveContent";
4
+ import { parseSemicolonCsv, serializeSemicolonCsv } from "../../../util/csv";
5
+ import Saved from "../saved.vue";
6
+ const { content, saveContent, isSaving, savingSuccessful } = await useSaveContent();
7
+ const parsedCsv = parseSemicolonCsv(content.value);
8
+ const headers = ref(parsedCsv.headers);
9
+ const rows = ref(parsedCsv.rows);
10
+ const hasUnsavedChanges = ref(false);
11
+ const columnCount = computed(() => headers.value.length);
12
+ async function saveCsv() {
13
+ const serializedCsv = serializeSemicolonCsv({ headers: headers.value, rows: rows.value });
14
+ content.value = serializedCsv;
15
+ await saveContent();
16
+ hasUnsavedChanges.value = false;
17
+ }
18
+ function getHeaderLabel(colIndex) {
19
+ return headers.value[colIndex] || `Spalte ${colIndex + 1}`;
20
+ }
21
+ function insertRow(atIndex) {
22
+ const newRow = Array(columnCount.value).fill("");
23
+ rows.value.splice(atIndex, 0, newRow);
24
+ hasUnsavedChanges.value = true;
25
+ }
26
+ function removeRow(rowIndex) {
27
+ rows.value.splice(rowIndex, 1);
28
+ hasUnsavedChanges.value = true;
29
+ }
30
+ function moveRowUp(rowIndex) {
31
+ if (rowIndex <= 0) return;
32
+ const row = rows.value.splice(rowIndex, 1)[0];
33
+ if (!row) return;
34
+ rows.value.splice(rowIndex - 1, 0, row);
35
+ hasUnsavedChanges.value = true;
36
+ }
37
+ function moveRowDown(rowIndex) {
38
+ if (rowIndex >= rows.value.length - 1) return;
39
+ const row = rows.value.splice(rowIndex, 1)[0];
40
+ if (!row) return;
41
+ rows.value.splice(rowIndex + 1, 0, row);
42
+ hasUnsavedChanges.value = true;
43
+ }
44
+ const editingCell = ref(null);
45
+ const editBuffer = ref("");
46
+ function startEdit(rowIndex, colIndex) {
47
+ editingCell.value = { rowIndex, colIndex };
48
+ editBuffer.value = rows.value[rowIndex][colIndex] || "";
49
+ }
50
+ function saveEdit() {
51
+ if (!editingCell.value) return;
52
+ const { rowIndex, colIndex } = editingCell.value;
53
+ rows.value[rowIndex][colIndex] = editBuffer.value;
54
+ hasUnsavedChanges.value = true;
55
+ cancelEdit();
56
+ }
57
+ function cancelEdit() {
58
+ editingCell.value = null;
59
+ editBuffer.value = "";
60
+ }
61
+ </script>
62
+
63
+ <template>
64
+ <div class="w-full">
65
+ <div class="flex items-center gap-2 mb-2.5">
66
+ <span
67
+ v-if="headers.length === 0"
68
+ class="opacity-70 text-sm"
69
+ >
70
+ Keine Kopfzeile gefunden. Bitte eine CSV mit Kopfzeile bereitstellen, um Zeilen zu bearbeiten.
71
+ </span>
72
+ </div>
73
+
74
+ <div class="bg-white">
75
+ <div class="flex items-center h-0 justify-center border border-gray-200 rounded-sm mb-6">
76
+ <button
77
+ type="button"
78
+ class="button small soft"
79
+ :disabled="headers.length === 0"
80
+ @click="insertRow(0)"
81
+ >
82
+ <svg
83
+ xmlns="http://www.w3.org/2000/svg"
84
+ fill="none"
85
+ viewBox="0 0 24 24"
86
+ stroke-width="1.5"
87
+ stroke="currentColor"
88
+ class="size-4"
89
+ >
90
+ <path
91
+ stroke-linecap="round"
92
+ stroke-linejoin="round"
93
+ d="M12 4.5v15m7.5-7.5h-15"
94
+ />
95
+ </svg>
96
+ </button>
97
+ </div>
98
+
99
+ <div>
100
+ <template
101
+ v-for="(r, rowIndex) in rows"
102
+ :key="rowIndex"
103
+ >
104
+ <div
105
+ class="flex items-stretch rounded-sm border border-gray-200"
106
+ >
107
+ <div class="flex-1 min-w-0 overflow-x-auto overflow-y-hidden">
108
+ <div class="grid grid-flow-col auto-cols-[minmax(160px,1fr)] max-[640px]:auto-cols-[minmax(220px,1fr)]">
109
+ <div
110
+ v-for="(cell, colIndex) in r"
111
+ :key="colIndex"
112
+ class="p-1.5 border-r border-gray-200 box-border last:border-r-0"
113
+ >
114
+ <div class="text-xs mb-1.5 leading-tight wrap-break-word flex items-center justify-between gap-1">
115
+ {{ getHeaderLabel(colIndex) }}
116
+ </div>
117
+ <div
118
+ class="text-xs whitespace-pre-line wrap-break-word border border-gray-300/70 rounded bg-gray-50 p-1.5 min-h-9.5 leading-[1.35] box-border overflow-hidden line-clamp-4 h-[calc(4*1.35*1em+12px)] max-[640px]:line-clamp-3 max-[640px]:h-[calc(3*1.35*1em+12px)] cursor-pointer"
119
+ :title="cell"
120
+ @click="startEdit(rowIndex, colIndex)"
121
+ >
122
+ {{ cell || "\u2014" }}
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <div class="flex-none flex flex-col items-start gap-1 p-1.5 border-l border-gray-200 bg-white max-[640px]:px-1 max-[640px]:gap-0.5">
129
+ <button
130
+ type="button"
131
+ class="button small"
132
+ :disabled="rowIndex === 0"
133
+ aria-label="Zeile nach oben"
134
+ title="Nach oben"
135
+ @click="moveRowUp(rowIndex)"
136
+ >
137
+ <svg
138
+ xmlns="http://www.w3.org/2000/svg"
139
+ fill="none"
140
+ viewBox="0 0 24 24"
141
+ stroke-width="1.5"
142
+ stroke="currentColor"
143
+ class="size-4"
144
+ >
145
+ <path
146
+ stroke-linecap="round"
147
+ stroke-linejoin="round"
148
+ d="M4.5 10.5 12 3m0 0 7.5 7.5M12 3v18"
149
+ />
150
+ </svg>
151
+ </button>
152
+ <button
153
+ type="button"
154
+ class="button small"
155
+ :disabled="rowIndex === rows.length - 1"
156
+ aria-label="Zeile nach unten"
157
+ title="Nach unten"
158
+ @click="moveRowDown(rowIndex)"
159
+ >
160
+ <svg
161
+ xmlns="http://www.w3.org/2000/svg"
162
+ fill="none"
163
+ viewBox="0 0 24 24"
164
+ stroke-width="1.5"
165
+ stroke="currentColor"
166
+ class="size-4"
167
+ >
168
+ <path
169
+ stroke-linecap="round"
170
+ stroke-linejoin="round"
171
+ d="M19.5 13.5 12 21m0 0-7.5-7.5M12 21V3"
172
+ />
173
+ </svg>
174
+ </button>
175
+ <button
176
+ type="button"
177
+ class="button small"
178
+ aria-label="Zeile löschen"
179
+ title="Löschen"
180
+ @click="removeRow(rowIndex)"
181
+ >
182
+ <svg
183
+ xmlns="http://www.w3.org/2000/svg"
184
+ fill="none"
185
+ viewBox="0 0 24 24"
186
+ stroke-width="1.5"
187
+ stroke="currentColor"
188
+ class="size-4"
189
+ >
190
+ <path
191
+ stroke-linecap="round"
192
+ stroke-linejoin="round"
193
+ d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
194
+ />
195
+ </svg>
196
+ </button>
197
+ </div>
198
+ </div>
199
+
200
+ <div class="h-0 flex items-center justify-center border border-gray-200 rounded-sm my-6">
201
+ <button
202
+ type="button"
203
+ class="button small"
204
+ :disabled="headers.length === 0"
205
+ @click="insertRow(rowIndex + 1)"
206
+ >
207
+ <svg
208
+ xmlns="http://www.w3.org/2000/svg"
209
+ fill="none"
210
+ viewBox="0 0 24 24"
211
+ stroke-width="1.5"
212
+ stroke="currentColor"
213
+ class="size-4"
214
+ >
215
+ <path
216
+ stroke-linecap="round"
217
+ stroke-linejoin="round"
218
+ d="M12 4.5v15m7.5-7.5h-15"
219
+ />
220
+ </svg>
221
+ </button>
222
+ </div>
223
+ </template>
224
+ </div>
225
+ </div>
226
+
227
+ <button
228
+ type="button"
229
+ class="button w-full mt-3 justify-center"
230
+ @click="saveCsv"
231
+ >
232
+ <span v-if="isSaving">Speichern...</span>
233
+ <span v-else>Speichern</span>
234
+ </button>
235
+ <Saved v-if="savingSuccessful && !hasUnsavedChanges" />
236
+
237
+ <div
238
+ v-if="editingCell"
239
+ class="fixed inset-0 bg-black/45 flex items-center justify-center p-4 z-9999"
240
+ role="presentation"
241
+ @click.self="cancelEdit()"
242
+ >
243
+ <div
244
+ class="w-full max-w-180 bg-white rounded-[10px] border border-black/10 shadow-[0_10px_40px_rgba(0,0,0,0.28)] p-3.5 flex flex-col gap-2.5"
245
+ role="dialog"
246
+ aria-modal="true"
247
+ :aria-label="`CSV-Zelle bearbeiten: ${getHeaderLabel(editingCell.colIndex)}`"
248
+ >
249
+ <div class="flex flex-col gap-0.5">
250
+ <div class="font-bold">
251
+ Zelle bearbeiten
252
+ </div>
253
+ <div class="opacity-75 text-[13px]">
254
+ {{ getHeaderLabel(editingCell.colIndex) }} · Zeile {{ editingCell.rowIndex + 1 }}
255
+ </div>
256
+ </div>
257
+
258
+ <textarea
259
+ id="csv-edit-textarea"
260
+ v-model="editBuffer"
261
+ class="w-full box-border p-2.5 border border-gray-300 rounded-lg resize-y min-h-40 font-[inherit] focus:outline-none focus:ring-2 focus:ring-emerald-500/30"
262
+ rows="8"
263
+ />
264
+
265
+ <div class="flex justify-end gap-2">
266
+ <button
267
+ type="button"
268
+ class="button"
269
+ @click="saveEdit()"
270
+ >
271
+ schließen
272
+ </button>
273
+ <button
274
+ type="button"
275
+ class="button secondary"
276
+ @click="cancelEdit()"
277
+ >
278
+ zurücksetzen
279
+ </button>
280
+ </div>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup>
2
+ import usePathParam from "../../../composables/usePathParam";
3
+ const { path } = usePathParam();
4
+ </script>
5
+
6
+ <template>
7
+ <div>
8
+ <button
9
+ class="button secondary w-full justify-center mb-2"
10
+ type="button"
11
+ >
12
+ Link kopieren
13
+ </button>
14
+ <button
15
+ class="button w-full justify-center mb-4"
16
+ type="button"
17
+ >
18
+ Bild austauschen
19
+ </button>
20
+
21
+ <img
22
+ :src="`/api/admin/content/${path}`"
23
+ alt="Image Preview"
24
+ class="w-full h-auto border border-gray-200 rounded-sm p-1"
25
+ >
26
+ </div>
27
+ </template>
@@ -1,7 +1,9 @@
1
1
  <script setup>
2
2
  import { computed, ref } from "vue";
3
3
  import { marked } from "marked";
4
- const content = defineModel("content", { type: String, ...{ default: "" } });
4
+ import useSaveContent from "../../../composables/useSaveContent";
5
+ import Saved from "../saved.vue";
6
+ const { content, saveContent, isSaving, savingSuccessful } = await useSaveContent();
5
7
  const mode = ref("edit");
6
8
  function escapeHtml(value) {
7
9
  return (value ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -25,7 +27,7 @@ const renderedHtml = computed(() => {
25
27
  const text = this.parser.parseInline(token.tokens);
26
28
  if (!safe) return text;
27
29
  const t = token.title ? ` title="${escapeHtml(token.title)}"` : "";
28
- const target = safe.startsWith("http") ? ' target="_blank" rel="noopener noreferrer"' : "";
30
+ const target = ' target="_blank" rel="noopener noreferrer"';
29
31
  return `<a href="${escapeHtml(safe)}"${t}${target}>${text}</a>`;
30
32
  };
31
33
  renderer.image = function(token) {
@@ -44,26 +46,46 @@ const renderedHtml = computed(() => {
44
46
  </script>
45
47
 
46
48
  <template>
47
- <div style="width: 100%;">
48
- <div style="display: flex; gap: 8px; margin-bottom: 10px; align-items: center;">
49
+ <div>
50
+ <div class="flex gap-2 mb-2">
49
51
  <button
50
52
  type="button"
51
- @click="mode = mode === 'edit' ? 'preview' : 'edit'"
53
+ class="button secondary flex-1"
54
+ :disabled="mode === 'edit'"
55
+ @click="mode = 'edit'"
52
56
  >
53
- {{ mode === "edit" ? "Vorschau" : "Bearbeiten" }}
57
+ Bearbeiten
58
+ </button>
59
+ <button
60
+ type="button"
61
+ class="button secondary flex-1"
62
+ :disabled="mode === 'preview'"
63
+ @click="mode = 'preview'"
64
+ >
65
+ Vorschau
54
66
  </button>
55
67
  </div>
56
68
 
57
69
  <textarea
58
70
  v-if="mode === 'edit'"
59
71
  v-model="content"
60
- style="width: 100%; height: 400px; resize: vertical; border: 1px solid #d0d0d0; padding: 10px; box-sizing: border-box;"
72
+ class="w-full min-h-72"
61
73
  />
62
74
 
63
75
  <div
64
76
  v-else
65
- style="width: 100%; min-height: 400px; border: 1px solid #d0d0d0; padding: 10px; box-sizing: border-box; overflow: auto;"
77
+ class="prose max-w-full min-h-72 border border-gray-200 rounded-sm p-4"
66
78
  v-html="renderedHtml"
67
79
  />
80
+
81
+ <button
82
+ type="button"
83
+ class="button w-full justify-center mt-3"
84
+ @click="saveContent"
85
+ >
86
+ <span v-if="isSaving">Speichern...</span>
87
+ <span v-else>Speichern</span>
88
+ </button>
89
+ <Saved v-if="savingSuccessful" />
68
90
  </div>
69
91
  </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,26 @@
1
+ <script setup>
2
+ import usePathParam from "../../../composables/usePathParam";
3
+ const { path } = usePathParam();
4
+ </script>
5
+
6
+ <template>
7
+ <div>
8
+ <button
9
+ class="button secondary w-full justify-center mb-2"
10
+ type="button"
11
+ >
12
+ Link kopieren
13
+ </button>
14
+ <button
15
+ class="button w-full justify-center mb-4"
16
+ type="button"
17
+ >
18
+ PDF austauschen
19
+ </button>
20
+ <embed
21
+ :src="`/api/admin/content/${path}`"
22
+ type="application/pdf"
23
+ class="w-full h-auto aspect-[5.5/8] border border-gray-200 rounded-sm"
24
+ >
25
+ </div>
26
+ </template>