Compare commits

...

15 commits
v1.2.2 ... main

22 changed files with 9636 additions and 6234 deletions

View file

@ -1,4 +1,4 @@
FROM node:18-alpine AS build FROM node:22-alpine AS build
WORKDIR /app WORKDIR /app
@ -10,7 +10,7 @@ COPY . /app
RUN npm run build RUN npm run build
FROM node:18-alpine AS prod FROM node:22-alpine AS prod
WORKDIR /app WORKDIR /app

14
app.vue
View file

@ -3,19 +3,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import calculateTitle from "./composables/calculateTitle"; const { seo, title } = useGlobal();
import provideGlobal from "./composables/provideGlobal";
const { SEO } = await provideGlobal();
const title = await calculateTitle();
const { metaDescription, keywords } = SEO ?? {};
useHead({ useHead({
title: title, title: title.value,
meta: [ meta: [
{ name: "description", content: metaDescription }, { name: "description", content: seo.value?.metaDescription },
{ name: "keywords", content: keywords }, { name: "keywords", content: seo.value?.keywords ?? "" },
], ],
}); });
</script> </script>

View file

@ -1,6 +1,10 @@
@tailwind base; @import "tailwindcss";
@tailwind components;
@tailwind utilities; @theme {
--color-primary: #b22222;
--color-darkgray: #2b292a;
--color-lightgray: #e3dfdf;
}
* { * {
font-family: "ConthraxSemiBold"; font-family: "ConthraxSemiBold";

View file

@ -29,7 +29,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const title = await calculateTitle(); const { footer, title } = useGlobal();
const { footer } = await provideGlobal(); const { links, maintained_by, copyright } = footer.value ?? {};
const { links, maintained_by, copyright } = footer ?? {};
</script> </script>

View file

@ -54,7 +54,7 @@
v-for="sublink in navbar_sub_items" v-for="sublink in navbar_sub_items"
:key="sublink.id" :key="sublink.id"
:to="`/${params?.[0]}/${sublink.URL}`" :to="`/${params?.[0]}/${sublink.URL}`"
:class="sublink.URL == params?.[1] && !params[2] ? 'active' : ''" :class="sublink.URL == params?.[1] ? 'active' : ''"
class="w-fit" class="w-fit"
> >
{{ sublink.name }} {{ sublink.name }}
@ -64,8 +64,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import provideGlobal from "../composables/provideGlobal";
const { const {
params: { slug: params }, params: { slug: params },
} = useRoute(); } = useRoute();
@ -73,12 +71,12 @@ const {
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const baseUrl = runtimeConfig.public.strapi.url; const baseUrl = runtimeConfig.public.strapi.url;
const { logo, navbar } = await provideGlobal(); const { logo, navbar } = useGlobal();
const open = ref(false); const open = ref(false);
const navbar_sub_items = computed(() => { const navbar_sub_items = computed(() => {
if (!navbar) return []; if (!navbar.value) return [];
return navbar.navbar_items.find((ni) => ni.URL == params?.[0])?.navbar_sub_items; return navbar.value.navbar_items.find((ni) => ni.URL == params?.[0])?.navbar_sub_items;
}); });
</script> </script>

View file

@ -4,27 +4,27 @@
:class="allowNavigation ? '' : 'pointer-events-none'" :class="allowNavigation ? '' : 'pointer-events-none'"
:to="`${urlOverwrite ?? $route.path}/${data?.slug}`" :to="`${urlOverwrite ?? $route.path}/${data?.slug}`"
> >
<div class="w-full h-56 relative"> <div class="w-full h-56 min-h-56 relative">
<NuxtPicture <NuxtPicture
loading="lazy" loading="lazy"
class="w-full h-full object-cover object-center" class="w-full h-full min-h-full object-cover object-center"
:src="data?.image?.url || logo?.url ? baseUrl + (data?.image?.url ?? logo?.url) : '/favicon.png'" :src="data?.image?.url || logo?.url ? baseUrl + (data?.image?.url ?? logo?.url) : '/favicon.png'"
:imgAttrs="{ class: 'w-full h-full object-cover object-center' }" :imgAttrs="{ class: 'w-full h-full object-cover object-center' }"
/> />
<h1 <h1
v-if="itemIndex" v-if="itemIndex"
class="text-center text-black text-4xl my-auto absolute bottom-2 left-2" class="text-center text-black text-4xl! my-auto absolute bottom-2 left-2"
style="text-shadow: 2px 2px 4px white" style="text-shadow: 2px 2px 4px white"
> >
{{ itemIndex }}. {{ itemIndex }}.
</h1> </h1>
</div> </div>
<div class="w-full h-44 relative bg-white px-2 py-5 flex flex-col justify-start items-start gap-2 overflow-y-auto"> <div class="w-full grow relative bg-white p-2 pb-4 flex flex-col justify-start items-start gap-2 overflow-y-auto">
<h1> <h1>
{{ data?.title }} {{ data?.title }}
</h1> </h1>
<p v-if="itemDate && lookup?.show_date" class="w-full text-[#5c5c5c]"> <p class="w-full text-[#5c5c5c] line-clamp-2 overflow-hidden">
{{ itemDate }} {{ itemDate }}
</p> </p>
<p class="w-full text-[#5c5c5c] line-clamp-2 overflow-hidden"> <p class="w-full text-[#5c5c5c] line-clamp-2 overflow-hidden">
@ -42,7 +42,7 @@ import type Lookup from "../../types/collection/lookup";
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const baseUrl = runtimeConfig.public.strapi.url; const baseUrl = runtimeConfig.public.strapi.url;
const { logo } = await provideGlobal(); const { logo } = useGlobal();
const props = defineProps({ const props = defineProps({
data: Object as PropType<BaseCollection>, data: Object as PropType<BaseCollection>,

View file

@ -4,7 +4,7 @@
:class="allowNavigation ? '' : 'pointer-events-none'" :class="allowNavigation ? '' : 'pointer-events-none'"
:to="`${urlOverwrite ?? $route.path}/${data?.slug}`" :to="`${urlOverwrite ?? $route.path}/${data?.slug}`"
> >
<h1 v-if="itemIndex" class="min-w-20 w-20 sm:min-w-24 sm:w-24 text-center text-black text-4xl my-auto"> <h1 v-if="itemIndex" class="min-w-20 w-20 sm:min-w-24 sm:w-24 text-center text-black text-4xl! my-auto">
{{ itemIndex }}. {{ itemIndex }}.
</h1> </h1>

View file

@ -25,7 +25,7 @@ import type SharedEmphasiseArticle from "../../types/component/shared/emphasiseA
import type Lookup from "../../types/collection/lookup"; import type Lookup from "../../types/collection/lookup";
const { find } = useStrapi(); const { find } = useStrapi();
const { data: lookup } = await useAsyncData("lookup", () => const { data: lookup } = await useAsyncData(() =>
find<Lookup>("collection-lookups", { find<Lookup>("collection-lookups", {
filters: { filters: {
collection: "articles", collection: "articles",

View file

@ -113,13 +113,13 @@ const pagination = ref<Meta>({ page: 0, pageSize: 0, pageCount: 0, total: 0 });
const activeYear = ref<number>(0); const activeYear = ref<number>(0);
if (props.data?.lookup.list_with_date != "none") { if (props.data?.lookup.list_with_date != "none") {
const { data: year } = await useAsyncData<Array<number>>("distinct-years", () => const { data: year } = await useAsyncData<Array<number>>(() =>
$fetch(`${baseUrl}/api/custom/${props.data?.lookup.collection}/distinct-years`) $fetch(`${baseUrl}/api/custom/${props.data?.lookup.collection}/distinct-years`)
); );
years.value = year.value ?? []; years.value = year.value ?? [];
activeYear.value = years.value[0] ?? 0; activeYear.value = years.value[0] ?? 0;
} }
const { data: collections } = await useAsyncData("collection", () => const { data: collections } = await useAsyncData(() =>
find<BaseCollection>(props.data?.lookup.collection ?? "", { find<BaseCollection>(props.data?.lookup.collection ?? "", {
...(props.data?.lookup?.list_with_date != "none" ...(props.data?.lookup?.list_with_date != "none"
? { ? {
@ -235,7 +235,7 @@ async function changeTimedData(year: number) {
withCount: true, withCount: true,
}, },
}); });
console.log(data);
collection.value = data?.data; collection.value = data?.data;
pagination.value = (data?.meta.pagination as unknown as { pagination.value = (data?.meta.pagination as unknown as {
page: number; page: number;

View file

@ -1,11 +0,0 @@
import provideGlobal from "./provideGlobal";
export default async function () {
const runtimeConfig = useRuntimeConfig();
const appTitle = runtimeConfig.public.app.title;
const { SEO } = await provideGlobal();
const { metaTitle } = SEO ?? {};
return metaTitle ?? appTitle;
}

View file

@ -1,9 +0,0 @@
import type Global from "../types/single/global";
export default async function () {
const { findOne } = useStrapi();
const { data: global } = await useAsyncData("global", () => findOne<Global>("global"));
const { logo, navbar, footer, SEO } = global.value?.data ?? {};
return { logo, navbar, footer, SEO };
}

26
composables/useGlobal.ts Normal file
View file

@ -0,0 +1,26 @@
import type BaseFile from "../types/component/baseFile";
import type Footer from "../types/component/global/footer";
import type Navbar from "../types/component/global/navbar";
import type SEO from "../types/component/global/seo";
import type Global from "../types/single/global";
export const useGlobal = () => {
const global = useState<Global | null>("global");
const runtimeConfig = useRuntimeConfig();
const appTitle = runtimeConfig.public.app.title;
const logo = computed<BaseFile | null>(() => global.value?.logo ?? null);
const navbar = computed<Navbar | null>(() => global.value?.navbar ?? null);
const footer = computed<Footer | null>(() => global.value?.footer ?? null);
const seo = computed<SEO | null>(() => global.value?.SEO ?? null);
const title = computed<string>(() => seo.value?.metaTitle ?? appTitle);
return {
logo,
global,
navbar,
footer,
seo,
title,
};
};

63
composables/useSitemap.ts Normal file
View file

@ -0,0 +1,63 @@
import type Page from "../types/collection/page";
import type { ComponentTypes } from "../types/component/baseComponent";
export const useSitemap = () => {
const { navbar, footer } = useGlobal();
const pages = useState<Page[]>("sitemap_pages");
const sitemap = ref<sitemap>([]);
for (const element of navbar.value?.navbar_items ?? []) {
if (!element.default_active_child) {
sitemap.value.push({
path: element.URL.startsWith("/") ? element.URL : `/${element.URL}`,
origin: "navbar",
document: element?.page?.documentId,
hasCollection: element.page.content.filter((c: ComponentTypes) => c.__component == "shared.list").length != 0,
});
}
for (const subelement of element.navbar_sub_items) {
let url = `${element.URL}/${subelement.URL}`;
sitemap.value.push({
path: url.startsWith("/") ? url : `/${url}`,
origin: "navbar",
document: subelement?.page?.documentId,
hasCollection:
subelement?.page?.content.filter((c: ComponentTypes) => c.__component == "shared.list").length != 0,
});
}
}
for (const element of pages.value) {
let url = element.slug.replaceAll("~", "/");
if (!sitemap.value.find((a) => a.path == url)) {
sitemap.value.push({
path: url.startsWith("/") ? url : `/${url}`,
origin: "page",
document: element.documentId,
hasCollection: element.content.filter((c: ComponentTypes) => c.__component == "shared.list").length != 0,
});
}
}
for (const element of footer.value?.links ?? []) {
let url = element.URL.startsWith("/") ? element.URL : `/${element.URL}`;
if (!sitemap.value.find((a) => a.path == url) && !element.URL.startsWith("http")) {
sitemap.value.push({
path: url,
origin: "footer",
document: undefined,
hasCollection: undefined,
});
}
}
return sitemap;
};
type sitemap = Array<{
path: string;
origin: string;
document?: string;
hasCollection?: boolean;
}>;

View file

@ -22,14 +22,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import provideGlobal from "../composables/provideGlobal";
import type Homepage from "../types/single/homepage"; import type Homepage from "../types/single/homepage";
const runtimeConfig = useRuntimeConfig(); const runtimeConfig = useRuntimeConfig();
const baseUrl = runtimeConfig.public.strapi.url; const baseUrl = runtimeConfig.public.strapi.url;
const { findOne } = useStrapi(); const { findOne } = useStrapi();
const { logo } = await provideGlobal(); const { logo } = useGlobal();
const { data: homepage } = await useAsyncData("homepage", () => findOne<Homepage>("homepage")); const { data: homepage } = await useAsyncData("homepage", () => findOne<Homepage>("homepage"));
const { backdrop } = homepage.value?.data ?? {}; const { backdrop } = homepage.value?.data ?? {};

View file

@ -1,3 +1,5 @@
import tailwindcss from "@tailwindcss/vite";
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
app: { app: {
@ -18,17 +20,14 @@ export default defineNuxtConfig({
css: ["~/assets/app.css", "~/assets/ConthraxSemiBold.css"], css: ["~/assets/app.css", "~/assets/ConthraxSemiBold.css"],
postcss: {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
},
compatibilityDate: "2024-04-03", compatibilityDate: "2024-04-03",
devtools: { enabled: false }, devtools: { enabled: false },
modules: ["@nuxtjs/strapi", "@nuxt/image"], modules: ["@nuxtjs/strapi", "@nuxt/image"],
vite: {
plugins: [tailwindcss()],
},
strapi: { strapi: {
url: process.env.PUBLIC_STRAPI_URL, url: process.env.PUBLIC_STRAPI_URL,
prefix: "/api", prefix: "/api",

15483
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,8 @@
{ {
"name": "nuxt-app", "name": "ff-webpage-landing",
"version": "1.3.2",
"private": true, "private": true,
"description": "Feuerwehr/Verein Webseite",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "nuxt build", "build": "nuxt build",
@ -12,14 +14,12 @@
"dependencies": { "dependencies": {
"@nuxt/image": "^1.8.1", "@nuxt/image": "^1.8.1",
"@nuxtjs/strapi": "^2.0.0", "@nuxtjs/strapi": "^2.0.0",
"nuxt": "^3.13.2", "@tailwindcss/vite": "^4.1.4",
"nuxt": "^3.16.2",
"vue": "latest", "vue": "latest",
"vue-router": "latest" "vue-router": "latest"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.20", "tailwindcss": "^4.1.4"
"postcss": "^8.4.47", }
"tailwindcss": "^3.4.14"
},
"version": "1.2.2"
} }

View file

@ -12,56 +12,87 @@ import type Event from "../types/collection/event";
import type Operation from "../types/collection/operation"; import type Operation from "../types/collection/operation";
import type Vehicle from "../types/collection/vehicle"; import type Vehicle from "../types/collection/vehicle";
import type Page from "../types/collection/page"; import type Page from "../types/collection/page";
import provideGlobal from "../composables/provideGlobal";
const { const {
params: { slug: params }, params: { slug: params },
} = useRoute(); } = useRoute();
const { findOne, find } = useStrapi(); const { findOne, find } = useStrapi();
const { navbar } = await provideGlobal(); const sitemap = useSitemap();
const navbar_items = computed(() => {
return navbar?.navbar_items ?? []; const detail = ref<Article | Operation | Event | Vehicle | undefined>(undefined);
const activePath = computed(() => {
return "/" + (Array.isArray(params) ? params.join("/") : params);
}); });
const navbar_sub_items = computed(() => { const activePageBySitemap = computed(() => {
return navbar_items.value.find((ni) => ni.URL == params[0])?.navbar_sub_items ?? []; return sitemap.value.find((s) => s.path == activePath.value);
}); });
const active_item = computed(() => { const similarestPage = computed(() => {
return navbar_items.value.find((ni) => ni.URL == params[0])?.page; return sitemap.value.reduce(
}); (bestMatch, current) => {
const active_sub_item = computed(() => { const currentMatchLength = current.path
return navbar_sub_items.value.find((si) => si.URL == params[1])?.page; .split("/")
}); .filter((segment, index) => segment != "" && segment == activePath.value.split("/")[index]).length;
const active_page_id = computed(() => { const bestMatchLength = bestMatch.path
return active_sub_item.value?.documentId ?? active_item.value?.documentId ?? ""; .split("/")
.filter((segment, index) => segment != "" && segment == activePath.value.split("/")[index]).length;
if (currentMatchLength > bestMatchLength) {
return current;
} else {
return bestMatch;
}
},
{ path: "", origin: "", hasCollection: false }
);
}); });
const { data: pages } = await useAsyncData("pages", () => const { data: pages } = await useAsyncData(
findOne<Page | Array<Page>>("pages", active_page_id.value, { () =>
populate: { findOne<Page | Array<Page>>("pages", similarestPage.value?.document, {
populate: "*", populate: {
content: {
populate: "*", populate: "*",
content: {
populate: "*",
},
hero: {
populate: "*",
},
}, },
hero: { filters: {
populate: "*", ...(!similarestPage.value?.document
? { slug: Array.isArray(params) ? params.join("~") : params, ref_only_access: false }
: {}),
}, },
}, }),
filters: { {
...(active_page_id.value == "" ? { slug: params[0], ref_only_access: true } : {}), default: () => null,
}, }
})
); );
const page = computed(() => { const page = computed(() => {
return Array.isArray(pages.value?.data) ? pages.value.data[0] : pages.value?.data; return Array.isArray(pages.value?.data) ? pages.value.data[0] : pages.value?.data;
}); });
let detail = ref<Article | Operation | Event | Vehicle | undefined>(undefined); const isCollectionDetail = computed(() => {
const searchDetail = computed(() => { return activePath.value != similarestPage.value.path && similarestPage.value.hasCollection;
if (!active_sub_item.value) return params[1];
return params[2];
}); });
if (searchDetail.value) {
const notFound = computed(() => {
if (isCollectionDetail.value && detail.value) return !detail.value;
else
return (
!page.value ||
(page.value && !(page.value.content.length != 0 || (page.value.hero.title && page.value.hero.banner)))
);
});
const showContentBuilder = computed(() => {
if (isCollectionDetail.value && detail.value) return !detail.value;
else return page.value && (page.value.content.length != 0 || (page.value.hero.title && page.value.hero.banner));
});
if (isCollectionDetail) {
let collectionOfDetail = [ let collectionOfDetail = [
...new Set( ...new Set(
page.value?.content page.value?.content
@ -72,11 +103,11 @@ if (searchDetail.value) {
]; ];
for (const element of collectionOfDetail) { for (const element of collectionOfDetail) {
const { data: details } = await useAsyncData("detail", () => const { data: details } = await useAsyncData(() =>
find<Article | Operation | Event | Vehicle>(element ?? "", { find<Article | Operation | Event | Vehicle>(element ?? "", {
populate: "*", populate: "*",
filters: { filters: {
slug: params[2] ?? params[1], slug: activePath.value.substring(activePath.value.lastIndexOf("/") + 1),
}, },
}) })
); );
@ -86,14 +117,4 @@ if (searchDetail.value) {
} }
} }
} }
const notFound = computed(() => {
if (searchDetail.value) return !detail.value;
else return active_page_id.value == "" && !page.value?.content && !page.value?.hero;
});
const showContentBuilder = computed(() => {
if (searchDetail.value) return !detail.value;
else return !!page.value?.content || !!page.value?.hero;
});
</script> </script>

15
pages/sitemap.vue Normal file
View file

@ -0,0 +1,15 @@
<template>
<NuxtLayout name="default">
<div class="min-h-[calc(100vh-9rem)] w-full">
<div class="container mx-auto py-12 px-2 min-h-[50vh] flex flex-col gap-2">
<NuxtLink v-for="item in sitemap" :key="item.path" :to="`${item.path}`">
{{ item.path }} {{ item.hasCollection ? "(...)" : "" }}
</NuxtLink>
</div>
</div>
</NuxtLayout>
</template>
<script setup lang="ts">
const sitemap = useSitemap();
</script>

15
plugins/global.ts Normal file
View file

@ -0,0 +1,15 @@
import type Global from "../types/single/global";
export default defineNuxtPlugin(async (nuxtApp) => {
const globalState = useState<Global | null>("global", () => null);
if (!globalState.value) {
const { findOne } = useStrapi();
const { data: global } = await useAsyncData("global", () => findOne<Global>("global"), {
server: true,
lazy: false,
default: () => {},
});
globalState.value = global.value?.data ?? null;
}
});

25
plugins/sitemap.ts Normal file
View file

@ -0,0 +1,25 @@
import type Page from "../types/collection/page";
import type Global from "../types/single/global";
export default defineNuxtPlugin(async (nuxtApp) => {
const pageState = useState<Page[]>("sitemap_pages", () => []);
if (!pageState.value) {
const { find } = useStrapi();
const { data: page_res } = await useAsyncData(
"sitemap_pages",
() =>
find<Page>("pages", {
filters: {
ref_only_access: false,
},
}),
{
server: true,
lazy: false,
default: () => {},
}
);
pageState.value = page_res.value?.data ?? [];
}
});

View file

@ -1,21 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{js,vue,ts}",
"./layouts/**/*.vue",
"./pages/**/*.vue",
"./plugins/**/*.{js,ts}",
"./app.vue",
"./error.vue",
],
theme: {
extend: {
colors: {
primary: "#B22222",
darkgray: "#2B292A",
lightgray: "#E3DFDF",
},
},
},
plugins: [],
};