mktcms 0.1.17 → 0.1.19

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 (44) hide show
  1. package/README.md +17 -14
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +11 -3
  4. package/dist/runtime/app/components/admin.vue +1 -1
  5. package/dist/runtime/app/components/content/breadcrumb.d.vue.ts +6 -0
  6. package/dist/runtime/app/components/content/breadcrumb.vue +29 -0
  7. package/dist/runtime/app/components/content/breadcrumb.vue.d.ts +6 -0
  8. package/dist/runtime/app/components/content/dirs.d.vue.ts +7 -0
  9. package/dist/runtime/app/components/content/dirs.vue +36 -0
  10. package/dist/runtime/app/components/content/dirs.vue.d.ts +7 -0
  11. package/dist/runtime/app/components/content/editor/blob/image.d.vue.ts +10 -0
  12. package/dist/runtime/app/components/content/editor/blob/image.vue +12 -0
  13. package/dist/runtime/app/components/content/editor/blob/image.vue.d.ts +10 -0
  14. package/dist/runtime/app/components/content/editor/blob/index.vue +41 -0
  15. package/dist/runtime/app/components/content/editor/{index.vue → text/index.vue} +23 -13
  16. package/dist/runtime/app/components/content/files.d.vue.ts +7 -0
  17. package/dist/runtime/app/components/content/files.vue +109 -0
  18. package/dist/runtime/app/components/content/files.vue.d.ts +7 -0
  19. package/dist/runtime/app/components/content/index.vue +12 -50
  20. package/dist/runtime/app/components/content/upload.vue +90 -9
  21. package/dist/runtime/app/components/header.vue +38 -11
  22. package/dist/runtime/app/composables/useAdminUpload.d.ts +2 -0
  23. package/dist/runtime/app/composables/useAdminUpload.js +4 -0
  24. package/dist/runtime/app/pages/admin/edit/blob/[path].d.vue.ts +3 -0
  25. package/dist/runtime/app/pages/admin/edit/blob/[path].vue +12 -0
  26. package/dist/runtime/app/pages/admin/edit/blob/[path].vue.d.ts +3 -0
  27. package/dist/runtime/app/pages/admin/edit/text/[path].d.vue.ts +3 -0
  28. package/dist/runtime/app/pages/admin/edit/text/[path].vue +12 -0
  29. package/dist/runtime/app/pages/admin/edit/text/[path].vue.d.ts +3 -0
  30. package/dist/runtime/server/api/admin/content/[path].d.ts +1 -1
  31. package/dist/runtime/server/api/admin/content/[path].js +8 -2
  32. package/dist/runtime/server/api/content/[path].js +6 -2
  33. package/package.json +1 -1
  34. package/dist/runtime/app/pages/admin/edit/[path].vue +0 -12
  35. /package/dist/runtime/app/components/content/editor/{index.d.vue.ts → blob/index.d.vue.ts} +0 -0
  36. /package/dist/runtime/app/components/content/editor/{index.vue.d.ts → blob/index.vue.d.ts} +0 -0
  37. /package/dist/runtime/app/components/content/editor/{csv.d.vue.ts → text/csv.d.vue.ts} +0 -0
  38. /package/dist/runtime/app/components/content/editor/{csv.vue → text/csv.vue} +0 -0
  39. /package/dist/runtime/app/components/content/editor/{csv.vue.d.ts → text/csv.vue.d.ts} +0 -0
  40. /package/dist/runtime/app/{pages/admin/edit/[path].d.vue.ts → components/content/editor/text/index.d.vue.ts} +0 -0
  41. /package/dist/runtime/app/{pages/admin/edit/[path].vue.d.ts → components/content/editor/text/index.vue.d.ts} +0 -0
  42. /package/dist/runtime/app/components/content/editor/{markdown.d.vue.ts → text/markdown.d.vue.ts} +0 -0
  43. /package/dist/runtime/app/components/content/editor/{markdown.vue → text/markdown.vue} +0 -0
  44. /package/dist/runtime/app/components/content/editor/{markdown.vue.d.ts → text/markdown.vue.d.ts} +0 -0
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Simple CMS module for Nuxt (pre-alpha)
2
2
 
3
- This module is my personal, minimalist, opinionated, independent alternative to @nuxt/content and to a large portion of the WordPress projects I’ve worked on.
3
+ This module is my personal, minimalist, opinionated, independent alternative to @nuxt/content and to a large portion of the WordPress projects I’ve worked on. I want to build projects with Nuxt and customers need the simplest possible admin interface to manage content without getting overwhelmed by buttons and features they don’t need.
4
+
5
+ I don't want to decide on a database schema or limit myself to json. All editable content is stored as a file of supported format (json, markdown, csv, jpg, webp, pdf, etc.) in an S3 bucket. The admin interface is a simple file explorer/editor. The module provides a `useContent` composable to read content files and a `sendMail` utility to send emails via SMTP.
4
6
 
5
7
  [![npm version][npm-version-src]][npm-version-href]
6
8
  [![npm downloads][npm-downloads-src]][npm-downloads-href]
@@ -26,19 +28,20 @@ npx nuxi module add mktcms
26
28
  ```
27
29
 
28
30
  ```bash
29
- MKTCMS_ADMIN_AUTH_KEY="your-admin-auth-key"
30
- MKTCMS_S3_ACCESS_KEY_ID=your-s3-access-key-id
31
- MKTCMS_S3_SECRET_ACCESS_KEY=your-s3-secret-access-key
32
- MKTCMS_S3_BUCKET=your-s3-bucket-name
33
- MKTCMS_S3_REGION=your-s3-bucket-region
34
- MKTCMS_S3_PREFIX="your-project"
35
- MKTCMS_SMTP_HOST="your-smtp-host"
36
- MKTCMS_SMTP_PORT=465
37
- MKTCMS_SMTP_SECURE=true
38
- MKTCMS_SMTP_USER="your-smtp-user"
39
- MKTCMS_SMTP_PASS="your-smtp-pass"
40
- MKTCMS_MAILER_FROM="your-mailer-from-address"
41
- MKTCMS_MAILER_TO="your-mailer-to-address"
31
+ NUXT_PUBLIC_MKTCMS_SITE_URL="http://localhost:3000"
32
+ NUXT_MKTCMS_ADMIN_AUTH_KEY="your-admin-auth-key"
33
+ NUXT_MKTCMS_S3_ACCESS_KEY_ID=your-s3-access-key-id
34
+ NUXT_MKTCMS_S3_SECRET_ACCESS_KEY=your-s3-secret-access-key
35
+ NUXT_MKTCMS_S3_BUCKET=your-s3-bucket-name
36
+ NUXT_MKTCMS_S3_REGION=your-s3-bucket-region
37
+ NUXT_MKTCMS_S3_PREFIX="your-project"
38
+ NUXT_MKTCMS_SMTP_HOST="your-smtp-host"
39
+ NUXT_MKTCMS_SMTP_PORT=465
40
+ NUXT_MKTCMS_SMTP_SECURE=true
41
+ NUXT_MKTCMS_SMTP_USER="your-smtp-user"
42
+ NUXT_MKTCMS_SMTP_PASS="your-smtp-pass"
43
+ NUXT_MKTCMS_MAILER_FROM="your-mailer-from-address"
44
+ NUXT_MKTCMS_MAILER_TO="your-mailer-to-address"
42
45
  ```
43
46
 
44
47
  Add local development storage folder in `.gitignore`:
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mktcms",
3
3
  "configKey": "mktcms",
4
- "version": "0.1.17",
4
+ "version": "0.1.19",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -24,6 +24,9 @@ const module$1 = defineNuxtModule({
24
24
  mailerFrom: "",
25
25
  mailerTo: ""
26
26
  }));
27
+ _nuxt.options.runtimeConfig.public.mktcms = defu((_nuxt.options.runtimeConfig.public.mktcms, {
28
+ siteUrl: ""
29
+ }));
27
30
  addServerImports({
28
31
  name: "sendMail",
29
32
  from: resolver.resolve("runtime/server/utils/sendMail")
@@ -74,9 +77,14 @@ const module$1 = defineNuxtModule({
74
77
  file: resolver.resolve("./runtime/app/pages/admin/index.vue")
75
78
  });
76
79
  pages.push({
77
- name: "Admin Editor",
78
- path: "/admin/edit/:path",
79
- file: resolver.resolve("./runtime/app/pages/admin/edit/[path].vue")
80
+ name: "Admin Text Editor",
81
+ path: "/admin/edit/text/:path",
82
+ file: resolver.resolve("./runtime/app/pages/admin/edit/text/[path].vue")
83
+ });
84
+ 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")
80
88
  });
81
89
  pages.push({
82
90
  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{background-color:#f9f9f9;margin:0 auto;max-width:800px;padding:20px}#mktcms-admin h1{color:#333}#mktcms-admin button{background-color:#3cb371;border:none;border-radius:5px;color:#fff;cursor:pointer;font-size:16px;padding:10px 20px;transition:background-color .3s}#mktcms-admin button:hover{background-color:#45a049}#mktcms-admin input[type=email],#mktcms-admin input[type=password],#mktcms-admin input[type=text],#mktcms-admin textarea{border:1px solid #ccc;border-radius:5px;font-size:16px;padding:10px}#mktcms-admin .breadcrumbs{color:#888;font-size:1.5rem;margin:20px 0}#mktcms-admin .breadcrumbs a{color:#888;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
+ *{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}
9
9
  </style>
@@ -0,0 +1,6 @@
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>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -0,0 +1,29 @@
1
+ <script setup>
2
+ defineProps({
3
+ parts: { type: Array, required: true }
4
+ });
5
+ </script>
6
+
7
+ <template>
8
+ <div class="breadcrumbs">
9
+ <NuxtLink to="/admin">Hauptordner</NuxtLink>
10
+ <span
11
+ v-for="(part, index) in parts"
12
+ :key="index"
13
+ >
14
+ /
15
+ <NuxtLink
16
+ v-if="index < parts.length - 1"
17
+ :to="`/admin/${parts.slice(0, index + 1).join(':')}`"
18
+ >
19
+ {{ part }}
20
+ </NuxtLink>
21
+ <span
22
+ v-else
23
+ style="font-weight: bold;"
24
+ >
25
+ {{ part }}
26
+ </span>
27
+ </span>
28
+ </div>
29
+ </template>
@@ -0,0 +1,6 @@
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>;
5
+ declare const _default: typeof __VLS_export;
6
+ export default _default;
@@ -0,0 +1,7 @@
1
+ type __VLS_Props = {
2
+ path: string;
3
+ dirs: string[];
4
+ };
5
+ 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>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,36 @@
1
+ <script setup>
2
+ defineProps({
3
+ path: { type: String, required: true },
4
+ dirs: { type: Array, required: true }
5
+ });
6
+ </script>
7
+
8
+ <template>
9
+ <div class="dirs">
10
+ <NuxtLink
11
+ v-for="dir in dirs"
12
+ :key="dir"
13
+ :to="`/admin/${path ? path + ':' : ''}${dir}`"
14
+ >
15
+ <span>
16
+ {{ dir }}
17
+ </span>
18
+ <span>
19
+ <svg
20
+ xmlns="http://www.w3.org/2000/svg"
21
+ fill="none"
22
+ viewBox="0 0 24 24"
23
+ stroke-width="1.5"
24
+ stroke="currentColor"
25
+ style="width: 16px; height: 16px; vertical-align: middle;"
26
+ >
27
+ <path
28
+ stroke-linecap="round"
29
+ stroke-linejoin="round"
30
+ d="m8.25 4.5 7.5 7.5-7.5 7.5"
31
+ />
32
+ </svg>
33
+ </span>
34
+ </NuxtLink>
35
+ </div>
36
+ </template>
@@ -0,0 +1,7 @@
1
+ type __VLS_Props = {
2
+ path: string;
3
+ dirs: string[];
4
+ };
5
+ 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>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,10 @@
1
+ type __VLS_ModelProps = {
2
+ 'path'?: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ "update:path": (value: string | undefined) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
7
+ "onUpdate:path"?: ((value: string | undefined) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,12 @@
1
+ <script setup>
2
+ const path = defineModel("path", { type: String });
3
+ </script>
4
+
5
+ <template>
6
+ <div class="image-editor">
7
+ <img
8
+ :src="`/api/admin/content/${path}`"
9
+ alt="Image Preview"
10
+ >
11
+ </div>
12
+ </template>
@@ -0,0 +1,10 @@
1
+ type __VLS_ModelProps = {
2
+ 'path'?: string;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ "update:path": (value: string | undefined) => any;
6
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
7
+ "onUpdate:path"?: ((value: string | undefined) => any) | undefined;
8
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
9
+ declare const _default: typeof __VLS_export;
10
+ export default _default;
@@ -0,0 +1,41 @@
1
+ <script setup>
2
+ import { useRoute } from "#app";
3
+ import { ref } from "vue";
4
+ import Image from "./image.vue";
5
+ import Breadcrumb from "../../breadcrumb.vue";
6
+ const path = useRoute().params.path || "";
7
+ const pathParts = path.split(":");
8
+ const isSaving = ref(false);
9
+ const savingSuccessful = ref(false);
10
+ async function saveContent() {
11
+ isSaving.value = true;
12
+ savingSuccessful.value = false;
13
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
14
+ isSaving.value = false;
15
+ savingSuccessful.value = true;
16
+ }
17
+ </script>
18
+
19
+ <template>
20
+ <div>
21
+ <Breadcrumb :parts="pathParts" />
22
+
23
+ <div>
24
+ <Image
25
+ v-if="path.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i)"
26
+ :path="path"
27
+ />
28
+ <button
29
+ style="margin-top: 10px;"
30
+ @click="saveContent"
31
+ >
32
+ <span v-if="isSaving">Speichern...</span>
33
+ <span v-else>Speichern</span>
34
+ </button>
35
+ <span
36
+ v-if="savingSuccessful"
37
+ style="color: green; margin-left: 10px;"
38
+ >✔️ Gespeichert</span>
39
+ </div>
40
+ </div>
41
+ </template>
@@ -1,32 +1,32 @@
1
1
  <script setup>
2
2
  import { useFetch, useRoute } from "#app";
3
+ import { ref, watch } from "vue";
3
4
  import Csv from "./csv.vue";
4
5
  import Markdown from "./markdown.vue";
6
+ import Breadcrumb from "../../breadcrumb.vue";
5
7
  const path = useRoute().params.path || "";
6
8
  const pathParts = path.split(":");
7
9
  const { data: content } = await useFetch(`/api/admin/content/${path}`);
10
+ const isSaving = ref(false);
11
+ const savingSuccessful = ref(false);
12
+ watch(content, () => {
13
+ savingSuccessful.value = false;
14
+ });
8
15
  async function saveContent() {
16
+ isSaving.value = true;
17
+ savingSuccessful.value = false;
9
18
  await $fetch(`/api/admin/content/${path}`, {
10
19
  method: "POST",
11
20
  body: { content: content.value }
12
21
  });
22
+ isSaving.value = false;
23
+ savingSuccessful.value = true;
13
24
  }
14
25
  </script>
15
26
 
16
27
  <template>
17
28
  <div>
18
- <div class="breadcrumbs">
19
- <a href="/admin">Hauptverzeichnis</a>
20
- <span
21
- v-for="(part, index) in pathParts"
22
- :key="index"
23
- >
24
- /
25
- <a :href="`/admin/${pathParts.slice(0, index + 1).join(':')}`">
26
- {{ part }}
27
- </a>
28
- </span>
29
- </div>
29
+ <Breadcrumb :parts="pathParts" />
30
30
 
31
31
  <div v-if="content !== void 0">
32
32
  <Markdown
@@ -37,12 +37,22 @@ async function saveContent() {
37
37
  v-else-if="path.endsWith('.csv')"
38
38
  v-model:content="content"
39
39
  />
40
+ <textarea
41
+ v-if="path.match(/\.(txt|json)$/i)"
42
+ v-model="content"
43
+ style="width: 100%; height: 400px; font-family: monospace; resize: vertical;"
44
+ />
40
45
  <button
41
46
  style="margin-top: 10px;"
42
47
  @click="saveContent"
43
48
  >
44
- Speichern
49
+ <span v-if="isSaving">Speichern...</span>
50
+ <span v-else>Speichern</span>
45
51
  </button>
52
+ <span
53
+ v-if="savingSuccessful"
54
+ style="color: green; margin-left: 10px;"
55
+ >✔️ Gespeichert</span>
46
56
  </div>
47
57
  </div>
48
58
  </template>
@@ -0,0 +1,7 @@
1
+ type __VLS_Props = {
2
+ path: string;
3
+ files: string[];
4
+ };
5
+ 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>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -0,0 +1,109 @@
1
+ <script setup>
2
+ defineProps({
3
+ path: { type: String, required: true },
4
+ files: { type: Array, required: true }
5
+ });
6
+ </script>
7
+
8
+ <template>
9
+ <div class="files">
10
+ <div
11
+ v-for="file in files"
12
+ :key="file"
13
+ style="display: flex; align-items: center;"
14
+ >
15
+ <NuxtLink
16
+ :to="`/admin/edit/${file.match(/\.md$|\.csv$|\.txt$|\.json$/i) ? 'text' : 'blob'}/${path ? path + ':' : ''}${file}`"
17
+ style="flex-grow: 1;"
18
+ >
19
+ <img
20
+ v-if="file.match(/\.png$|\.jpg$|\.jpeg$|\.gif$|\.svg$|\.webp$/i)"
21
+ :src="`/api/content/${path ? path + ':' : ''}${file}`"
22
+ alt="Vorschaubild"
23
+ style="width: 64px; height: 64px; vertical-align: middle; margin-right: 4px; object-fit: cover;"
24
+ >
25
+ {{ file }}
26
+ </NuxtLink>
27
+ <NuxtLink
28
+ v-if="file.match(/\.png$|\.jpg$|\.jpeg$|\.gif$|\.svg$|\.webp$/i)"
29
+ style="margin-left: 8px;"
30
+ title="Link kopieren"
31
+ >
32
+ <svg
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ fill="none"
35
+ viewBox="0 0 24 24"
36
+ stroke-width="1.5"
37
+ stroke="currentColor"
38
+ class="button-icon"
39
+ >
40
+ <path
41
+ stroke-linecap="round"
42
+ stroke-linejoin="round"
43
+ d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"
44
+ />
45
+ </svg>
46
+ </NuxtLink>
47
+ <NuxtLink
48
+ :to="`/admin/move/${path ? path + ':' : ''}${file}`"
49
+ style="margin-left: 8px;"
50
+ title="verschieben / umbenennen"
51
+ >
52
+ <svg
53
+ xmlns="http://www.w3.org/2000/svg"
54
+ fill="none"
55
+ viewBox="0 0 24 24"
56
+ stroke-width="1.5"
57
+ stroke="currentColor"
58
+ class="button-icon"
59
+ >
60
+ <path
61
+ stroke-linecap="round"
62
+ stroke-linejoin="round"
63
+ 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"
64
+ />
65
+ </svg>
66
+ </NuxtLink>
67
+ <NuxtLink
68
+ :to="`/admin/copy/${path ? path + ':' : ''}${file}`"
69
+ style="margin-left: 8px;"
70
+ title="kopieren"
71
+ >
72
+ <svg
73
+ xmlns="http://www.w3.org/2000/svg"
74
+ fill="none"
75
+ viewBox="0 0 24 24"
76
+ stroke-width="1.5"
77
+ stroke="currentColor"
78
+ class="button-icon"
79
+ >
80
+ <path
81
+ stroke-linecap="round"
82
+ stroke-linejoin="round"
83
+ d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
84
+ />
85
+ </svg>
86
+ </NuxtLink>
87
+ <NuxtLink
88
+ :to="`/admin/delete/${path ? path + ':' : ''}${file}`"
89
+ style="margin-left: 8px;"
90
+ title="löschen"
91
+ >
92
+ <svg
93
+ xmlns="http://www.w3.org/2000/svg"
94
+ fill="none"
95
+ viewBox="0 0 24 24"
96
+ stroke-width="1.5"
97
+ stroke="currentColor"
98
+ class="button-icon"
99
+ >
100
+ <path
101
+ stroke-linecap="round"
102
+ stroke-linejoin="round"
103
+ 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"
104
+ />
105
+ </svg>
106
+ </NuxtLink>
107
+ </div>
108
+ </div>
109
+ </template>
@@ -0,0 +1,7 @@
1
+ type __VLS_Props = {
2
+ path: string;
3
+ files: string[];
4
+ };
5
+ 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>;
6
+ declare const _default: typeof __VLS_export;
7
+ export default _default;
@@ -1,6 +1,9 @@
1
1
  <script setup>
2
2
  import { useFetch, useRoute } from "#app";
3
3
  import { computed } from "vue";
4
+ import Breadcrumb from "./breadcrumb.vue";
5
+ import Files from "./files.vue";
6
+ import Dirs from "./dirs.vue";
4
7
  const path = useRoute().params.path || "";
5
8
  const pathParts = path.split(":");
6
9
  const { data: keys } = await useFetch("/api/admin/content/list", {
@@ -30,60 +33,19 @@ const dirs = computed(() => {
30
33
 
31
34
  <template>
32
35
  <div>
33
- <div class="breadcrumbs">
34
- <a href="/admin">Hauptverzeichnis</a>
35
- <span
36
- v-for="(part, index) in pathParts"
37
- :key="index"
38
- >
39
- /
40
- <a :href="`/admin/${pathParts.slice(0, index + 1).join(':')}`">
41
- {{ part }}
42
- </a>
43
- </span>
44
- </div>
36
+ <Breadcrumb :parts="pathParts" />
45
37
 
46
- <div
38
+ <Files
47
39
  v-if="files.length"
48
- class="files"
49
- >
50
- <a
51
- v-for="file in files"
52
- :key="file"
53
- :href="`/admin/edit/${path ? path + ':' : ''}${file}`"
54
- >{{ file }}</a>
55
- </div>
40
+ :path="path"
41
+ :files="files"
42
+ />
56
43
 
57
- <div
44
+ <Dirs
58
45
  v-if="dirs.length"
59
- class="dirs"
46
+ :path="path"
47
+ :dirs="dirs"
60
48
  style="margin-top: 8px;"
61
- >
62
- <a
63
- v-for="dir in dirs"
64
- :key="dir"
65
- :href="`/admin/${path ? path + ':' : ''}${dir}`"
66
- >
67
- <span>
68
- {{ dir }}
69
- </span>
70
- <span>
71
- <svg
72
- xmlns="http://www.w3.org/2000/svg"
73
- fill="none"
74
- viewBox="0 0 24 24"
75
- stroke-width="1.5"
76
- stroke="currentColor"
77
- style="width: 16px; height: 16px; vertical-align: middle;"
78
- >
79
- <path
80
- stroke-linecap="round"
81
- stroke-linejoin="round"
82
- d="m8.25 4.5 7.5 7.5-7.5 7.5"
83
- />
84
- </svg>
85
- </span>
86
- </a>
87
- </div>
49
+ />
88
50
  </div>
89
51
  </template>
@@ -1,23 +1,86 @@
1
1
  <script setup>
2
+ import { computed, onMounted, ref, useFetch, useRoute } from "#imports";
2
3
  import useAdminUpload from "../../composables/useAdminUpload";
3
- const { isUploading, fileInput, path, uploadFiles } = useAdminUpload();
4
+ const { data: keys } = await useFetch("/api/admin/content/list");
5
+ const dirs = computed(() => {
6
+ const dirSet = /* @__PURE__ */ new Set();
7
+ keys.value?.forEach((key) => {
8
+ const parts = key.split(":");
9
+ if (parts.length > 1) {
10
+ for (let i = 0; i < parts.length - 1; i++) {
11
+ const dir2 = parts.slice(0, i + 1).join("/");
12
+ dirSet.add(dir2);
13
+ }
14
+ }
15
+ });
16
+ return Array.from(dirSet).sort();
17
+ });
18
+ const route = useRoute();
19
+ const dir = ref(route.query.dir || "");
20
+ const newSubdir = ref("");
21
+ const { isUploading, fileInput, fileInputImg, fileInputPdf, path, uploadFiles } = useAdminUpload();
22
+ onMounted(() => {
23
+ path.value = dir.value;
24
+ });
4
25
  </script>
5
26
 
6
27
  <template>
7
- <div style="display: flex; gap: 8px; margin: 16px 0;">
8
- <input
9
- v-model="path"
10
- type="text"
11
- placeholder="Unterordner (z.B. 'Produkte')"
28
+ <div style="display: flex; flex-direction: column; gap: 8px; margin: 16px 0;">
29
+ <h1 style="margin-bottom: 0;">
30
+ Datei hochladen
31
+ </h1>
32
+
33
+ <div style="display: flex; gap: 16px; align-items: center;">
34
+ <div style="flex-grow: 1;">
35
+ <h3 style="margin-bottom: 4px;">
36
+ Ordner
37
+ </h3>
38
+ <select
39
+ v-model="dir"
40
+ @change="path = dir"
41
+ >
42
+ <option value="">
43
+ Hauptordner
44
+ </option>
45
+ <option
46
+ v-for="d in dirs"
47
+ :key="d"
48
+ :value="d"
49
+ >
50
+ {{ d }}
51
+ </option>
52
+ </select>
53
+ </div>
54
+ <div style="flex-grow: 1;">
55
+ <h3 style="margin-bottom: 4px;">
56
+ Neuen Unterordner erstellen
57
+ </h3>
58
+ <input
59
+ v-model="newSubdir"
60
+ type="text"
61
+ placeholder="Unterordner (z.B. 'Produkte')"
62
+ @change="path = dir ? dir.replace(/\//g, ':') + ':' + newSubdir : newSubdir"
63
+ >
64
+ </div>
65
+ </div>
66
+
67
+ <button
68
+ :disabled="isUploading"
69
+ @click="fileInputImg?.click()"
70
+ >
71
+ Bild hochladen
72
+ </button>
73
+ <button
74
+ :disabled="isUploading"
75
+ @click="fileInputPdf?.click()"
12
76
  >
13
- <button :disabled="isUploading">
14
- Neuer Inhalt
77
+ PDF hochladen
15
78
  </button>
16
79
  <button
17
80
  :disabled="isUploading"
18
81
  @click="fileInput?.click()"
19
82
  >
20
- Bild/Dokument hochladen
83
+ Andere Datei hochladen
21
84
  </button>
22
85
  <input
23
86
  ref="fileInput"
@@ -26,6 +89,24 @@ const { isUploading, fileInput, path, uploadFiles } = useAdminUpload();
26
89
  accept=".pdf,.jpg,.jpeg,.png,.gif,.svg,.webp,.md,.docx,.txt"
27
90
  @change="async (e) => {
28
91
  await uploadFiles(e);
92
+ }"
93
+ >
94
+ <input
95
+ ref="fileInputImg"
96
+ style="display: none"
97
+ type="file"
98
+ accept=".jpg,.jpeg,.png,.gif,.svg,.webp"
99
+ @change="async (e) => {
100
+ await uploadFiles(e);
101
+ }"
102
+ >
103
+ <input
104
+ ref="fileInputPdf"
105
+ style="display: none"
106
+ type="file"
107
+ accept=".pdf"
108
+ @change="async (e) => {
109
+ await uploadFiles(e);
29
110
  }"
30
111
  >
31
112
  </div>
@@ -1,23 +1,50 @@
1
+ <script setup>
2
+ import { useRoute, useRuntimeConfig } from "#imports";
3
+ const { public: { mktcms: { siteUrl } } } = useRuntimeConfig();
4
+ const route = useRoute();
5
+ </script>
6
+
1
7
  <template>
2
- <div style="display: flex; justify-content: space-between; align-items: center;">
3
- <h1>
4
- <NuxtLink
5
- to="/admin"
6
- style="text-decoration: none; color: inherit;"
8
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 8px;">
9
+ <NuxtLink
10
+ v-if="route.fullPath != '/admin'"
11
+ to="/admin"
12
+ class="button soft"
13
+ >
14
+ <svg
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ fill="none"
17
+ viewBox="0 0 24 24"
18
+ stroke-width="1.5"
19
+ stroke="currentColor"
20
+ class="button-icon"
7
21
  >
8
- Website Verwaltung
9
- </NuxtLink>
10
- </h1>
22
+ <path
23
+ stroke-linecap="round"
24
+ stroke-linejoin="round"
25
+ d="M15.75 19.5 8.25 12l7.5-7.5"
26
+ />
27
+ </svg>
28
+ </NuxtLink>
11
29
  <NuxtLink
12
30
  to="/admin/new"
13
- style="background-color: #4CAF50; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; margin-left: auto; margin-right: 10px;"
31
+ class="button"
32
+ style="margin-left: auto;"
33
+ >
34
+ Datei hochladen
35
+ </NuxtLink>
36
+ <NuxtLink
37
+ :to="siteUrl"
38
+ external
39
+ target="_blank"
40
+ class="button soft"
14
41
  >
15
- Neuer Inhalt
42
+ zur Website
16
43
  </NuxtLink>
17
44
  <NuxtLink
18
45
  external
19
46
  to="/api/admin/logout"
20
- style="background-color: #ccc; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;"
47
+ class="button soft"
21
48
  >
22
49
  Abmelden
23
50
  </NuxtLink>
@@ -4,6 +4,8 @@ export default function useAdminUpload(): {
4
4
  path: import("vue").Ref<string | null, string | null>;
5
5
  files: import("vue").Ref<string[], string[]>;
6
6
  fileInput: import("vue").Ref<HTMLInputElement | null, HTMLInputElement | null>;
7
+ fileInputImg: import("vue").Ref<HTMLInputElement | null, HTMLInputElement | null>;
8
+ fileInputPdf: import("vue").Ref<HTMLInputElement | null, HTMLInputElement | null>;
7
9
  uploadFiles: (event: Event) => Promise<void>;
8
10
  deleteFile: (path: string) => Promise<void>;
9
11
  };
@@ -3,6 +3,8 @@ export default function useAdminUpload() {
3
3
  const uploadError = ref(null);
4
4
  const files = ref([]);
5
5
  const fileInput = ref(null);
6
+ const fileInputImg = ref(null);
7
+ const fileInputPdf = ref(null);
6
8
  const isUploading = ref(false);
7
9
  const path = ref(null);
8
10
  const sanePath = computed(() => {
@@ -55,6 +57,8 @@ export default function useAdminUpload() {
55
57
  path,
56
58
  files,
57
59
  fileInput,
60
+ fileInputImg,
61
+ fileInputPdf,
58
62
  uploadFiles,
59
63
  deleteFile
60
64
  };
@@ -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,12 @@
1
+ <script setup>
2
+ import Admin from "../../../../components/admin.vue";
3
+ import Header from "../../../../components/header.vue";
4
+ import Editor from "../../../../components/content/editor/blob/index.vue";
5
+ </script>
6
+
7
+ <template>
8
+ <Admin>
9
+ <Header />
10
+ <Editor />
11
+ </Admin>
12
+ </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,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,12 @@
1
+ <script setup>
2
+ import Admin from "../../../../components/admin.vue";
3
+ import Header from "../../../../components/header.vue";
4
+ import Editor from "../../../../components/content/editor/text/index.vue";
5
+ </script>
6
+
7
+ <template>
8
+ <Admin>
9
+ <Header />
10
+ <Editor />
11
+ </Admin>
12
+ </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;
@@ -1,2 +1,2 @@
1
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | number | true | object>>;
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
2
  export default _default;
@@ -8,11 +8,17 @@ export default defineEventHandler(async (event) => {
8
8
  const { path } = await getValidatedRouterParams(event, (params) => paramsSchema.parse(params));
9
9
  const { mktcms: { s3Prefix } } = useRuntimeConfig();
10
10
  const fullPath = s3Prefix + ":" + path;
11
+ const isImage = path.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i);
12
+ if (isImage) {
13
+ event.node.res.setHeader("Content-Type", "image/" + path.split(".").pop()?.toLowerCase());
14
+ } else {
15
+ event.node.res.setHeader("Content-Type", "text/plain; charset=utf-8");
16
+ }
11
17
  const storage = useStorage("content");
12
- const file = await storage.getItem(fullPath);
18
+ const file = isImage ? await storage.getItemRaw(fullPath) : await storage.getItem(fullPath);
13
19
  if (!file) {
14
20
  const fallbackStorage = useStorage("fallback");
15
- const fallbackFile = await fallbackStorage.getItem(fullPath);
21
+ const fallbackFile = isImage ? await fallbackStorage.getItemRaw(fullPath) : await fallbackStorage.getItem(fullPath);
16
22
  if (fallbackFile) {
17
23
  return fallbackFile;
18
24
  }
@@ -48,11 +48,15 @@ export default defineEventHandler(async (event) => {
48
48
  const { path } = await getValidatedRouterParams(event, (params) => paramsSchema.parse(params));
49
49
  const { mktcms: { s3Prefix } } = useRuntimeConfig();
50
50
  const fullPath = s3Prefix + ":" + path;
51
+ const isImage = path.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i);
52
+ if (isImage) {
53
+ event.node.res.setHeader("Content-Type", "image/" + path.split(".").pop()?.toLowerCase());
54
+ }
51
55
  const storage = useStorage("content");
52
- const file = await storage.getItem(fullPath);
56
+ const file = isImage ? await storage.getItemRaw(fullPath) : await storage.getItem(fullPath);
53
57
  if (!file) {
54
58
  const fallbackStorage = useStorage("fallback");
55
- const fallbackFile = await fallbackStorage.getItem(fullPath);
59
+ const fallbackFile = isImage ? await fallbackStorage.getItemRaw(fullPath) : await fallbackStorage.getItem(fullPath);
56
60
  if (fallbackFile) {
57
61
  return parsedFile(fullPath, fallbackFile);
58
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mktcms",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Simple CMS module for Nuxt",
5
5
  "repository": "mktcode/mktcms",
6
6
  "license": "MIT",
@@ -1,12 +0,0 @@
1
- <script setup>
2
- import Admin from "../../../components/admin.vue";
3
- import Header from "../../../components/header.vue";
4
- import Editor from "../../../components/content/editor/index.vue";
5
- </script>
6
-
7
- <template>
8
- <Admin>
9
- <Header />
10
- <Editor />
11
- </Admin>
12
- </template>