diff --git a/.prettierrc b/.prettierrc index 9402e22..fc75096 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,16 +1,18 @@ { - "printWidth": 80, + "printWidth": 120, "tabWidth": 4, "singleQuote": false, "trailingComma": "es5", "semi": false, "newline-before-return": true, - "no-duplicate-variable": [true, "check-parameters"], + "no-duplicate-variable": [ + true, + "check-parameters" + ], "no-var-keyword": true, - "svelteSortOrder": "scripts-styles-markup", "svelteStrictMode": true, "svelteBracketNewLine": true, "svelteAllowShorthand": true, "svelteIndentScriptAndStyle": true -} +} \ No newline at end of file diff --git a/api/collections/articles.yml b/api/collections/articles.yml new file mode 100644 index 0000000..fbaefd9 --- /dev/null +++ b/api/collections/articles.yml @@ -0,0 +1,150 @@ +######################################################################## +# Articles +######################################################################## + +name: articles +uploadPath: ../media/articles +meta: + # Navigationseintrag in der Admin-UI + label: { de: "Artikel auf der Seite", en: "Page articles" } + # Icon (Material UI) für den Navigationseintrag + muiIcon: file-document-edit-outline + # Standardsortierung der Liste + defaultSort: { field: "name", order: "ASC" } + # Admin-Backend Ansichten + defaultImageFilter: s + views: + # Mobile Darstellung + - type: simpleList + mediaQuery: "(max-width:599px)" + primaryText: path + columns: + - public + - image + - title + - position + # Desktop + - type: table + mediaQuery: "(min-width:600px)" + columns: + - public + - image + - title + - position + +imageFilter: + xs: + - fit: true + height: 90 + width: 90 + resampling: lanczos + quality: 60 + s: + - fit: true + height: 300 + width: 300 + resampling: lanczos + quality: 60 + m: + - fit: true + height: 600 + width: 600 + resampling: lanczos + quality: 60 + l: + - fit: true + height: 1200 + width: 1200 + resampling: lanczos + quality: 60 + xl: + - fit: true + height: 2000 + width: 2000 + resampling: lanczos + quality: 60 + +permissions: + public: + methods: + get: true + post: false + put: false + delete: false + user: + methods: + get: true + post: true + put: true + delete: true + # token als Zusatzsicherung gegen Spam, mehr siehe Hook + "token:${PUBLIC_TOKEN}": + methods: + get: false + post: false + put: false + delete: false + +# hooks: +# post: +# create: +# type: javascript +# file: hooks/article/post_create.js +# put: +# update: +# type: javascript +# file: hooks/article/put_return.js +# delete: +# return: +# type: javascript +# file: hooks/article/delete_return.js + +fields: + - name: articleTabs + type: tabs + meta: + label: + de: Informationen zu einem Artikel + en: Article Information + activeTab: 0 + subFields: + - name: articleTab + type: object + meta: + label: + de: Artikel + en: Article + css: + subFields: + - name: articleLayoutTab + type: object + meta: + label: + de: Layout + en: Layout + css: + subFields: + - name: articleMediaTab + type: object + meta: + label: + de: Bilder + en: Images + css: + subFields: + - name: articleAttachmentsTab + type: object + meta: + label: + de: Anhänge / Downloads + en: Attachments / Downloads + css: + subFields: + - name: articleMetaTab + type: object + meta: + label: + de: Meta + en: Meta + css: + subFields: diff --git a/api/collections/general.yml b/api/collections/general.yml new file mode 100644 index 0000000..2c1d8e1 --- /dev/null +++ b/api/collections/general.yml @@ -0,0 +1,306 @@ +######################################################################## +# General Information +######################################################################## + +name: general +uploadPath: ../media/general +meta: + # Navigationseintrag in der Admin-UI + label: { de: "Allgemeine Informationen", en: "General Information" } + # Icon (Material UI) für den Navigationseintrag + muiIcon: information-outline + # Identifizierung eines Eintrags für z.B. Select-Boxen in der Admin-UI + rowIdentTpl: { twig: "{{ email }} - {{ subject }}" } + # Standardsortierung der Liste + defaultSort: { field: "path", order: "ASC" } + # Admin-Backend Ansichten + defaultImageFilter: s + views: + # Mobile Darstellung + - type: simpleList + mediaQuery: "(max-width:599px)" + primaryText: firstname + columns: + - public + - firstname + - lastname + - companyName + # Desktop + - type: table + mediaQuery: "(min-width:600px)" + columns: + - public + - firstname + - lastname + - companyName + +imageFilter: + xs: + - fit: true + height: 90 + width: 90 + resampling: lanczos + quality: 60 + s: + - fit: true + height: 300 + width: 300 + resampling: lanczos + quality: 60 + m: + - fit: true + height: 600 + width: 600 + resampling: lanczos + quality: 60 + l: + - fit: true + height: 1200 + width: 1200 + resampling: lanczos + quality: 60 + xl: + - fit: true + height: 2000 + width: 2000 + resampling: lanczos + quality: 60 + +permissions: + public: + methods: + get: true + post: false + put: false + delete: false + user: + methods: + get: true + post: true + put: true + delete: true + # token als Zusatzsicherung gegen Spam, mehr siehe Hook + "token:${PUBLIC_TOKEN}": + methods: + get: false + post: false + put: false + delete: false + +fields: + - name: generalInformationTabs + type: tabs + meta: + label: + de: Allgemeine Information + en: General Information + activeTab: 0 + subFields: + - name: generalInformationTab + type: object + meta: + label: + de: Allgemein + en: General + css: + subFields: + - name: public + type: boolean + meta: + label: + de: Veröffentlicht + en: Public + helperText: + de: "Alle allgemeinen Informationen werden auf der Seite angezeigt." + en: "All general information are displayed on the page." + - name: metaInformationTab + type: object + meta: + label: + de: Meta / SEO + en: Meta / SEO + css: + subFields: + - name: metaTitle + type: string + meta: + label: { de: "Titel der Webseite", en: "Page Title" } + - name: metaDescription + type: string + meta: + label: { de: "Beschreibung der Webseite", en: "Page Description" } + - name: metaTagRobots + type: string[] + meta: + widget: chipArray + label: + de: Robots + en: Robots + inputProps: + placeholder: "nofollow" + defaultValue: [] + autocomplete: true + choices: + - { id: "noindex", name: "noindex" } + - { id: "index", name: "index" } + - { id: "follow", name: "follow" } + - { id: "nofollow", name: "nofollow" } + - { id: "noimageindex", name: "noimageindex" } + - { id: "none", name: "none" } + - { id: "noarchive", name: "noarchive" } + - { id: "nocache", name: "nocache" } + - { id: "nosnippet", name: "nosnippet" } + - { id: "nnavailable_after", name: "nnavailable_after" } + helperText: + de: "Noindex: Weist eine Suchmaschine an, eine Seite nicht zu indizieren.
index: Weist eine Suchmaschine an, eine Seite zu indizieren. Beachten Sie, dass Sie dieses Meta-Tag nicht hinzufügen müssen; es ist die Voreinstellung.
follow: Auch wenn die Seite nicht indexiert ist, sollte der Crawler allen Links auf einer Seite folgen und Eigenkapital an die verlinkten Seiten weitergeben.
nofollow: Weist einen Crawler an, keinen Links auf einer Seite zu folgen oder Link-Equity weiterzugeben.
noimageindex: Weist einen Crawler an, keine Bilder auf einer Seite zu indizieren.
none: Entspricht der gleichzeitigen Verwendung der noindex- und nofollow-Tags.
noarchive: Suchmaschinen sollten keinen zwischengespeicherten Link zu dieser Seite auf einem SERP anzeigen.
nocache: Wie noarchive, aber nur von Internet Explorer und Firefox verwendet.
nosnippet: Weist eine Suchmaschine an, kein Snippet dieser Seite (d. h. Meta-Beschreibung) dieser Seite auf einem SERP anzuzeigen.
nnavailable_after: Suchmaschinen sollen diese Seite nach einem bestimmten Datum nicht mehr indexieren.
" + en: "Noindex: Tells a search engine not to index a page.
index: Tells a search engine to index a page. Note that you don’t need to add this meta tag; it’s the default.
follow: Even if the page isn’t indexed, the crawler should follow all the links on a page and pass equity to the linked pages.
nofollow: Tells a crawler not to follow any links on a page or pass along any link equity.
noimageindex: Tells a crawler not to index any images on a page.
none: Equivalent to using both the noindex and nofollow tags simultaneously.
noarchive: Search engines should not show a cached link to this page on a SERP.
nocache: Same as noarchive, but only used by Internet Explorer and Firefox.
nosnippet: Tells a search engine not to show a snippet of this page (i.e. meta description) of this page on a SERP.
nnavailable_after: Search engines should no longer index this page after a particular date.
" + - name: metaKeywords + type: string + meta: + label: { de: "SEO / Schlüsselwörter", en: "SEO / Keywords" } + helperText: + de: "Beispiel: Stichwort1, Stichwort2, Stichwort3" + en: "Example: keyword1, keyword2, keyword3" + - name: personalInformationTab + type: object + meta: + label: + de: Personendaten + en: Personal Data + css: + subFields: + - name: firstname + type: string + meta: + label: { de: "Vorname", en: "Firstname" } + - name: lastname + type: string + meta: + label: { de: "Nachname", en: "Lastname" } + - name: street + type: string + meta: + label: { de: "Straße", en: "Street" } + - name: postcode + type: string + meta: + label: { de: "Postleitzahl", en: "Postcode" } + - name: city + type: string + meta: + label: { de: "Ort", en: "City" } + - name: tel + type: string + meta: + label: { de: "Telefonnummer", en: "Phone number" } + - name: mobile + type: string + meta: + label: { de: "Handynummer", en: "Mobile number" } + - name: email + type: string + meta: + label: { de: "E-Mail", en: "E-Mail" } + - name: companyInformationTab + type: object + meta: + label: + de: Unternehmensdaten + en: Company Data + css: + subFields: + - name: companyName + type: string + meta: + label: { de: "Name des Unternehmens", en: "Company Name" } + - name: companyWebUrl + type: string + meta: + label: { de: "URL zur Webseite", en: "Website URL" } + - name: companyAddresses + type: object[] + meta: + label: + de: Adresse + en: Adresse + css: + subFields: + - name: street + type: string + meta: + label: { de: "Straße", en: "Street" } + - name: houseNr + type: string + meta: + label: { de: "Hausnummer", en: "House number" } + - name: postcode + type: string + meta: + label: { de: "PLZ", en: "ZIP" } + - name: city + type: string + meta: + label: { de: "Ort", en: "City" } + - name: tel + type: string + meta: + label: { de: "Telefon", en: "Phone number" } + - name: fax + type: string + meta: + label: { de: "Fax", en: "Fax" } + - name: email + type: string + meta: + label: { de: "E-Mail", en: "E-Mail" } + - name: mediaInformationTab + type: object + meta: + label: + de: Media + en: Media + css: + subFields: + - name: favicon + type: file + meta: + label: { de: "Favicon", en: "Favicon" } + helperText: + de: "Ein Favicon ist ein kleines Icon, Symbol oder Logo, das von Webbrowsern verwendet wird, um eine Website auf wiedererkennbare Weise zu kennzeichnen." + en: "A favicon is a small icon, symbol, or logo used by web browsers to identify a website in a recognizable way." + - name: favicon + type: file + meta: + label: { de: "Logo / Brand", en: "Logo / Brand" } + helperText: + de: "Logo der Seite" + en: "Page Logo" + - name: mediaFiles + type: object[] + meta: + label: { de: "Dateien", en: "Files" } + subFields: + - name: title + type: string + meta: + label: { de: "Datei-Titel", en: "File Title" } + - name: id + type: string + meta: + label: { de: "Technischer Name / ID", en: "Technical name / ID" } + - name: file + type: file + meta: + label: { de: "", en: "" } + - name: copyrightInformationTab + type: object + meta: + label: + de: Copyright + en: Copyright + css: + subFields: + - name: copyright + type: string + meta: + label: { de: "Copyright Text", en: "Copyright Text" } diff --git a/api/config.yml b/api/config.yml index b679030..ef55585 100644 --- a/api/config.yml +++ b/api/config.yml @@ -7,6 +7,8 @@ meta: # Liste aller möglichen Kollektionen (Listen, Seiten...) zum Projekt collections: + - !include collections/general.yml + - !include collections/articles.yml - !include collections/content.yml - !include collections/contact_form.yml - !include collections/ssr.yml diff --git a/esbuild.config.js b/esbuild.config.js index 6658b63..18bdac3 100644 --- a/esbuild.config.js +++ b/esbuild.config.js @@ -5,8 +5,7 @@ const resolvePlugin = { // url in css does not resolve via esbuild-svelte correctly build.onResolve({ filter: /.*/, namespace: "fakecss" }, (args) => { // console.log(args) - if (args.path.match(/^\./)) - return { path: path.dirname(args.importer) + "/" + args.path } + if (args.path.match(/^\./)) return { path: path.dirname(args.importer) + "/" + args.path } // return { path: path.join(args.resolveDir, "public", args.path) } }) }, @@ -54,6 +53,8 @@ const options = { ".eot": "file", ".svg": "file", ".ttf": "file", + ".png": "file", + ".jpg": "file", }, sourcemap: true, target: ["es2020", "chrome61", "firefox60", "safari11", "edge18"], @@ -63,9 +64,7 @@ const bsMiddleware = [] if (process.argv[2] == "start") { const { createProxyMiddleware } = require("http-proxy-middleware") - const apiBase = - process.env.API_BASE || - "http://localhost:8080/api/v1/_/" + process.env.NAMESPACE + const apiBase = process.env.API_BASE || "http://localhost:8080/api/v1/_/" + process.env.NAMESPACE bsMiddleware.push( createProxyMiddleware("/api", { target: apiBase, diff --git a/package.json b/package.json index ae9dd03..96e2a64 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "license": "MIT", "scripts": { "validate": "svelte-check && tsc --noEmit", - "start": "node scripts/esbuild-wrapper.js start", - "start:localapi": "API_BASE=http://localhost:8080 node scripts/esbuild-wrapper.js start", + "start": "NAMESPACE=__NAMESPACE__ node scripts/esbuild-wrapper.js start", + "start:remoteapi": "API_BASE=https://login.tibicms.de/api/v1/_/__NAMESPACE__ node scripts/esbuild-wrapper.js start", "dev": "node scripts/esbuild-wrapper.js watch", "build": "node scripts/esbuild-wrapper.js build", "build:legacy": "node scripts/esbuild-wrapper.js build esbuild.config.legacy.js && babel _temp/index.js -o _temp/index.babeled.js && esbuild _temp/index.babeled.js --outfile=dist/_dist_/index.es5.js --target=es5 --bundle --minify --sourcemap", @@ -62,4 +62,4 @@ "live-server": "^1.2.1", "mongodb": "^4.3.1" } -} +} \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 6b22d17..cde7833 100644 --- a/src/api.ts +++ b/src/api.ts @@ -43,9 +43,7 @@ const _f = function (url, options): Promise { .replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => { keys.push((key = key.toLowerCase())) all.push([key, value]) - headers[key] = headers[key] - ? `${headers[key]},${value}` - : value + headers[key] = headers[key] ? `${headers[key]},${value}` : value }) resolve(response()) } @@ -62,12 +60,7 @@ const _f = function (url, options): Promise { }) } -const _fetch = - typeof fetch === "undefined" - ? typeof window === "undefined" - ? _f - : window.fetch || _f - : fetch +const _fetch = typeof fetch === "undefined" ? (typeof window === "undefined" ? _f : window.fetch || _f) : fetch export const api = async ( endpoint: string, @@ -107,8 +100,7 @@ export const api = async ( let method = "GET" let query = "&count=1" - if (options?.filter) - query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter)) + if (options?.filter) query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter)) if (options?.sort) query += "&sort=" + options.sort + "&sort=_id" if (options?.limit) query += "&limit=" + options.limit if (options?.offset) query += "&offset=" + options.offset @@ -149,8 +141,7 @@ export const api = async ( // @ts-ignore let data = (await response?.json()) || null - if (response?.status < 200 || response?.status >= 400) - throw { response, data } + if (response?.status < 200 || response?.status >= 400) throw { response, data } // @ts-ignore return { data, count: response?.headers?.get("x-results-count") || 0 } @@ -163,3 +154,34 @@ export const getContent = async (path: string): Promise => { } return null } + +export const getGeneralInformation = async (): Promise => { + try { + let response = await api("general", { + method: "get", + offset: 0, + limit: 1, + filter: { + active: true, + }, + }) + return response.data + } catch (e) { + return null + } +} + +export const getArticles = async (): Promise => { + try { + let response = await api("articles", { + method: "get", + offset: 0, + filter: { + active: true, + }, + }) + return response.data + } catch (e) { + return null + } +} diff --git a/src/store.ts b/src/store.ts index a9abd06..c8342cc 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,4 +1,5 @@ import { writable, get } from "svelte/store" +import { getGeneralInformation, getArticles } from "./api" const initLoc = { path: (typeof window !== "undefined" && window.location?.pathname) || "/", @@ -10,3 +11,37 @@ const initLoc = { initLoc.categoryPath = initLoc.path.replace(/\/\d{4,99}[^\/]+$/, "") export const location = writable(initLoc) + +// General Information + +export const generalInformation = writable() +const getGeneralProjectInformation = async () => { + const infos = await getGeneralInformation() + if (infos && infos.length) { + generalInformation.set(infos[0]) + } +} +getGeneralProjectInformation() + +// Articles + +export const articles = writable() +const getAllArticles = async () => { + const list = await getArticles() + articles.set(list) +} +getAllArticles() + +// Cookies - Webmakers Cookie Bar + +export const ccTags = writable([]) + +const updateCcTtags = () => { + // @ts-ignore + ccTags.set(window.ccLoadedTags || []) +} + +if (typeof window !== "undefined") { + window.addEventListener("ccInit", updateCcTtags) + window.addEventListener("ccAccept", updateCcTtags) +} diff --git a/types/global.d.ts b/types/global.d.ts index 4416573..d1b8ece 100644 --- a/types/global.d.ts +++ b/types/global.d.ts @@ -8,9 +8,52 @@ interface ContentBlock { images?: ImageEntry[] } - interface Content { id: string path: string blocks: ContentBlock[] } + +interface GeneralInformation { + id: string + active: boolean + firstname: string + lastname: string + street: string + postcode: number + city: string + tel: string + mobile: string + email: string + images: GeneralImage[] + insertTime: string + updateTime: string + lastPageUpdate: string +} + +interface GeneralImage { + file: File[] + id: string + label: string +} + +interface TibiArticle { + id: string + active: boolean + content: string + details: string + image: File + insertTime: string + position: string + subtitle: string + title: string + updateTime: string +} + +interface File { + lastModified?: number + path: string + size?: string + src: string + type: string +}