Erste kleine Anpassungen am Tibi-Svelte-Starter um später mehr Zeit in neuen Projekten zu sparen. Hier werden noch weitere Anpassungen folgen, die grundlegend in den meisten Projekten benötigt werden.

This commit is contained in:
Mario Linz 2022-03-17 11:12:06 +01:00
parent 75a8906d4a
commit 5caa62eb7e
9 changed files with 585 additions and 26 deletions

View File

@ -1,16 +1,18 @@
{ {
"printWidth": 80, "printWidth": 120,
"tabWidth": 4, "tabWidth": 4,
"singleQuote": false, "singleQuote": false,
"trailingComma": "es5", "trailingComma": "es5",
"semi": false, "semi": false,
"newline-before-return": true, "newline-before-return": true,
"no-duplicate-variable": [true, "check-parameters"], "no-duplicate-variable": [
true,
"check-parameters"
],
"no-var-keyword": true, "no-var-keyword": true,
"svelteSortOrder": "scripts-styles-markup", "svelteSortOrder": "scripts-styles-markup",
"svelteStrictMode": true, "svelteStrictMode": true,
"svelteBracketNewLine": true, "svelteBracketNewLine": true,
"svelteAllowShorthand": true, "svelteAllowShorthand": true,
"svelteIndentScriptAndStyle": true "svelteIndentScriptAndStyle": true
} }

View File

@ -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:

306
api/collections/general.yml Normal file
View File

@ -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: "<strong>Noindex</strong>: Weist eine Suchmaschine an, eine Seite nicht zu indizieren.<br/><strong>index</strong>: Weist eine Suchmaschine an, eine Seite zu indizieren. Beachten Sie, dass Sie dieses Meta-Tag nicht hinzufügen müssen; es ist die Voreinstellung.<br/><strong>follow</strong>: Auch wenn die Seite nicht indexiert ist, sollte der Crawler allen Links auf einer Seite folgen und Eigenkapital an die verlinkten Seiten weitergeben.<br/><strong>nofollow</strong>: Weist einen Crawler an, keinen Links auf einer Seite zu folgen oder Link-Equity weiterzugeben.<br/><strong>noimageindex</strong>: Weist einen Crawler an, keine Bilder auf einer Seite zu indizieren.<br/><strong>none</strong>: Entspricht der gleichzeitigen Verwendung der noindex- und nofollow-Tags.<br/><strong>noarchive</strong>: Suchmaschinen sollten keinen zwischengespeicherten Link zu dieser Seite auf einem SERP anzeigen.<br/><strong>nocache</strong>: Wie noarchive, aber nur von Internet Explorer und Firefox verwendet.<br/><strong>nosnippet</strong>: Weist eine Suchmaschine an, kein Snippet dieser Seite (d. h. Meta-Beschreibung) dieser Seite auf einem SERP anzuzeigen.<br/><strong>nnavailable_after</strong>: Suchmaschinen sollen diese Seite nach einem bestimmten Datum nicht mehr indexieren.<br/>"
en: "<strong>Noindex</strong>: Tells a search engine not to index a page.<br/><strong>index</strong>: Tells a search engine to index a page. Note that you dont need to add this meta tag; its the default.<br/><strong>follow</strong>: Even if the page isnt indexed, the crawler should follow all the links on a page and pass equity to the linked pages.<br/><strong>nofollow</strong>: Tells a crawler not to follow any links on a page or pass along any link equity.<br/><strong>noimageindex</strong>: Tells a crawler not to index any images on a page.<br/><strong>none</strong>: Equivalent to using both the noindex and nofollow tags simultaneously.<br/><strong>noarchive</strong>: Search engines should not show a cached link to this page on a SERP.<br/><strong>nocache</strong>: Same as noarchive, but only used by Internet Explorer and Firefox.<br/><strong>nosnippet</strong>: Tells a search engine not to show a snippet of this page (i.e. meta description) of this page on a SERP.<br/><strong>nnavailable_after</strong>: Search engines should no longer index this page after a particular date.<br/>"
- 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" }

View File

@ -7,6 +7,8 @@ meta:
# Liste aller möglichen Kollektionen (Listen, Seiten...) zum Projekt # Liste aller möglichen Kollektionen (Listen, Seiten...) zum Projekt
collections: collections:
- !include collections/general.yml
- !include collections/articles.yml
- !include collections/content.yml - !include collections/content.yml
- !include collections/contact_form.yml - !include collections/contact_form.yml
- !include collections/ssr.yml - !include collections/ssr.yml

View File

@ -5,8 +5,7 @@ const resolvePlugin = {
// url in css does not resolve via esbuild-svelte correctly // url in css does not resolve via esbuild-svelte correctly
build.onResolve({ filter: /.*/, namespace: "fakecss" }, (args) => { build.onResolve({ filter: /.*/, namespace: "fakecss" }, (args) => {
// console.log(args) // console.log(args)
if (args.path.match(/^\./)) if (args.path.match(/^\./)) return { path: path.dirname(args.importer) + "/" + args.path }
return { path: path.dirname(args.importer) + "/" + args.path }
// return { path: path.join(args.resolveDir, "public", args.path) } // return { path: path.join(args.resolveDir, "public", args.path) }
}) })
}, },
@ -54,6 +53,8 @@ const options = {
".eot": "file", ".eot": "file",
".svg": "file", ".svg": "file",
".ttf": "file", ".ttf": "file",
".png": "file",
".jpg": "file",
}, },
sourcemap: true, sourcemap: true,
target: ["es2020", "chrome61", "firefox60", "safari11", "edge18"], target: ["es2020", "chrome61", "firefox60", "safari11", "edge18"],
@ -63,9 +64,7 @@ const bsMiddleware = []
if (process.argv[2] == "start") { if (process.argv[2] == "start") {
const { createProxyMiddleware } = require("http-proxy-middleware") const { createProxyMiddleware } = require("http-proxy-middleware")
const apiBase = const apiBase = process.env.API_BASE || "http://localhost:8080/api/v1/_/" + process.env.NAMESPACE
process.env.API_BASE ||
"http://localhost:8080/api/v1/_/" + process.env.NAMESPACE
bsMiddleware.push( bsMiddleware.push(
createProxyMiddleware("/api", { createProxyMiddleware("/api", {
target: apiBase, target: apiBase,

View File

@ -7,8 +7,8 @@
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"validate": "svelte-check && tsc --noEmit", "validate": "svelte-check && tsc --noEmit",
"start": "node scripts/esbuild-wrapper.js start", "start": "NAMESPACE=__NAMESPACE__ node scripts/esbuild-wrapper.js start",
"start:localapi": "API_BASE=http://localhost:8080 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", "dev": "node scripts/esbuild-wrapper.js watch",
"build": "node scripts/esbuild-wrapper.js build", "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", "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", "live-server": "^1.2.1",
"mongodb": "^4.3.1" "mongodb": "^4.3.1"
} }
} }

View File

@ -43,9 +43,7 @@ const _f = function (url, options): Promise<Response> {
.replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => { .replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, (m, key, value) => {
keys.push((key = key.toLowerCase())) keys.push((key = key.toLowerCase()))
all.push([key, value]) all.push([key, value])
headers[key] = headers[key] headers[key] = headers[key] ? `${headers[key]},${value}` : value
? `${headers[key]},${value}`
: value
}) })
resolve(response()) resolve(response())
} }
@ -62,12 +60,7 @@ const _f = function (url, options): Promise<Response> {
}) })
} }
const _fetch = const _fetch = typeof fetch === "undefined" ? (typeof window === "undefined" ? _f : window.fetch || _f) : fetch
typeof fetch === "undefined"
? typeof window === "undefined"
? _f
: window.fetch || _f
: fetch
export const api = async <T>( export const api = async <T>(
endpoint: string, endpoint: string,
@ -107,8 +100,7 @@ export const api = async <T>(
let method = "GET" let method = "GET"
let query = "&count=1" let query = "&count=1"
if (options?.filter) if (options?.filter) query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter))
query += "&filter=" + encodeURIComponent(JSON.stringify(options.filter))
if (options?.sort) query += "&sort=" + options.sort + "&sort=_id" if (options?.sort) query += "&sort=" + options.sort + "&sort=_id"
if (options?.limit) query += "&limit=" + options.limit if (options?.limit) query += "&limit=" + options.limit
if (options?.offset) query += "&offset=" + options.offset if (options?.offset) query += "&offset=" + options.offset
@ -149,8 +141,7 @@ export const api = async <T>(
// @ts-ignore // @ts-ignore
let data = (await response?.json()) || null let data = (await response?.json()) || null
if (response?.status < 200 || response?.status >= 400) if (response?.status < 200 || response?.status >= 400) throw { response, data }
throw { response, data }
// @ts-ignore // @ts-ignore
return { data, count: response?.headers?.get("x-results-count") || 0 } return { data, count: response?.headers?.get("x-results-count") || 0 }
@ -163,3 +154,34 @@ export const getContent = async (path: string): Promise<Content> => {
} }
return null return null
} }
export const getGeneralInformation = async (): Promise<GeneralInformation[]> => {
try {
let response = await api<GeneralInformation[]>("general", {
method: "get",
offset: 0,
limit: 1,
filter: {
active: true,
},
})
return response.data
} catch (e) {
return null
}
}
export const getArticles = async (): Promise<TibiArticle[]> => {
try {
let response = await api<TibiArticle[]>("articles", {
method: "get",
offset: 0,
filter: {
active: true,
},
})
return response.data
} catch (e) {
return null
}
}

View File

@ -1,4 +1,5 @@
import { writable, get } from "svelte/store" import { writable, get } from "svelte/store"
import { getGeneralInformation, getArticles } from "./api"
const initLoc = { const initLoc = {
path: (typeof window !== "undefined" && window.location?.pathname) || "/", path: (typeof window !== "undefined" && window.location?.pathname) || "/",
@ -10,3 +11,37 @@ const initLoc = {
initLoc.categoryPath = initLoc.path.replace(/\/\d{4,99}[^\/]+$/, "") initLoc.categoryPath = initLoc.path.replace(/\/\d{4,99}[^\/]+$/, "")
export const location = writable(initLoc) export const location = writable(initLoc)
// General Information
export const generalInformation = writable<GeneralInformation>()
const getGeneralProjectInformation = async () => {
const infos = await getGeneralInformation()
if (infos && infos.length) {
generalInformation.set(infos[0])
}
}
getGeneralProjectInformation()
// Articles
export const articles = writable<TibiArticle[]>()
const getAllArticles = async () => {
const list = await getArticles()
articles.set(list)
}
getAllArticles()
// Cookies - Webmakers Cookie Bar
export const ccTags = writable<string[]>([])
const updateCcTtags = () => {
// @ts-ignore
ccTags.set(window.ccLoadedTags || [])
}
if (typeof window !== "undefined") {
window.addEventListener("ccInit", updateCcTtags)
window.addEventListener("ccAccept", updateCcTtags)
}

45
types/global.d.ts vendored
View File

@ -8,9 +8,52 @@ interface ContentBlock {
images?: ImageEntry[] images?: ImageEntry[]
} }
interface Content { interface Content {
id: string id: string
path: string path: string
blocks: ContentBlock[] 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
}