145 Commits

Author SHA1 Message Date
itsfinniii
27b8dc4118 Fix thumbnail for blog posts 2026-04-27 16:08:14 +02:00
itsfinniii
45a2627ec6 Fix thumbnail for web pages 2026-04-27 15:42:38 +02:00
itsfinniii
eba518ccc2 Fix problem with build 2026-04-27 15:34:12 +02:00
itsfinniii
3cfe6697a9 Hopefully fix some problems 2026-04-27 15:28:08 +02:00
itsfinniii
07716dae17 Revert back to Vite 6 2026-04-27 14:52:26 +02:00
itsfinniii
db65ac52a3 Finish the photo page 2026-04-27 14:37:13 +02:00
itsfinniii
fdc8a0aae6 Make first test for photos 2026-04-26 22:26:51 +02:00
itsfinniii
092d2a4458 Add loading spinner to gallery 2026-04-26 16:25:21 +02:00
itsfinniii
760281f7a4 Start with Changelogs.md file 2026-04-26 16:16:43 +02:00
itsfinniii
157cb9389c Increase spacing for justified layout 2026-04-26 16:13:20 +02:00
itsfinniii
be02d749dd Add pagination to album page 2026-04-26 16:12:57 +02:00
itsfinniii
feac162baa Add Photo Album pages 2026-04-26 15:32:59 +02:00
itsfinniii
f86630bcb0 Create base for Albums 2026-04-26 13:52:56 +02:00
itsfinniii
abaea70c7b Add albums to the project 2026-04-25 15:04:47 +02:00
itsfinniii
1fde3d4f69 Update packages 2026-04-24 18:50:58 +02:00
itsfinniii
7b5a8bc705 Fix the colors of the Footer 2026-04-24 18:30:23 +02:00
itsfinniii
d284010e28 Make text part greyer 2026-04-24 18:21:25 +02:00
itsfinniii
66409ab859 Fix Vite version 2026-04-24 18:20:47 +02:00
itsfinniii
46e705f6ea Fix robots.txt 2026-04-19 22:15:22 +02:00
itsfinniii
3bd4de2f30 Migrate to Astro 6, fix sitemaps 2026-04-19 22:05:28 +02:00
itsfinniii
00e4826744 Upgrade to Astro 6 2026-04-19 22:03:24 +02:00
itsfinniii
2744e6173c Update schema.json 2026-04-19 21:59:54 +02:00
itsfinniii
915879beac Prepare menu for website 2026-04-19 21:58:30 +02:00
itsfinniii
6d4a62fae7 Make footer more responsive 2026-04-19 21:28:29 +02:00
itsfinniii
506a5ed14e Add footer to website 2026-04-19 18:03:32 +02:00
itsfinniii
2374a6bd22 Fix return for the LastProjects 2026-04-12 20:13:08 +02:00
itsfinniii
8bc95d0f50 Only render the last components if there is anything to show, otherwise just show nothing 2026-04-12 18:23:07 +02:00
itsfinniii
3485c4583d Change the last blogs component 2026-04-12 18:20:40 +02:00
itsfinniii
ee949aa76f Fix thumbnail size for Last Projects 2026-04-05 22:50:38 +02:00
itsfinniii
07d2c8628f Fix thumbnail for Last Albums 2026-04-05 22:49:17 +02:00
itsfinniii
d39a98a42f Fix image size for categories and albums 2026-04-05 22:43:26 +02:00
itsfinniii
525422105c Fix the image size for Projects 2026-04-05 22:41:43 +02:00
itsfinniii
a0473094cf Fix image size in Blogs 2026-04-05 22:39:35 +02:00
itsfinniii
36004bddb0 Add development domain to astro.config.image 2026-04-05 22:39:28 +02:00
itsfinniii
89bbbf5595 Fix image resize function 2026-04-05 22:39:14 +02:00
itsfinniii
4ccbc9d9a8 Create PhotoLayout for later 2026-04-05 22:27:18 +02:00
itsfinniii
b73066352e Fix thumbnail for Photo 2026-04-05 22:23:31 +02:00
itsfinniii
f95f792775 Fix some issues with the image hashing 2026-04-05 22:10:06 +02:00
itsfinniii
5c161b8381 Make images the correct size by resizing them 2026-04-04 20:28:47 +02:00
itsfinniii
47e50a3ba4 Create function to calculate the new width and height of an image 2026-04-04 18:36:00 +02:00
itsfinniii
cf12428f98 Add tags to the Project page and Blog page 2026-04-04 18:25:18 +02:00
itsfinniii
67362dad96 Reword category meta description 2026-04-04 18:10:17 +02:00
itsfinniii
82587a6211 Add description for Photo Category page 2026-04-03 22:40:47 +02:00
itsfinniii
c05b50877b Add comment to categories index 2026-04-03 22:38:17 +02:00
itsfinniii
9bf417478a Do the same for the project posts 2026-04-03 22:37:54 +02:00
itsfinniii
4f955a9ec6 Change searchEngine for BlogPost in route 2026-04-03 22:37:36 +02:00
itsfinniii
f1b0d269bf Change searchEngine prop for WebpageLayout in route 2026-04-03 22:37:01 +02:00
itsfinniii
32c698c39a Update some metadata for the photo category page 2026-04-03 22:33:41 +02:00
itsfinniii
aa93600c79 Add OG thumbnail to Photo Category Index 2026-04-03 22:16:17 +02:00
itsfinniii
0b45967d30 Require at least one photo category before actually rendering pages of the photos 2026-04-03 22:12:12 +02:00
itsfinniii
39c26e73c6 Fix vulnerable packages 2026-04-03 22:00:15 +02:00
itsfinniii
abd98ff21b Add category albums page 2026-03-30 21:40:42 +02:00
itsfinniii
3435819f79 Add the photo category index 2026-03-28 20:40:35 +01:00
itsfinniii
cabdbd51cc Fix pathname again 2026-03-28 20:04:59 +01:00
itsfinniii
1e3243aac3 Fix some things in the Directus schema 2026-03-28 19:58:46 +01:00
itsfinniii
54e53d278e Fix project and blog posts 2026-03-28 19:24:57 +01:00
itsfinniii
f4319c4165 Add project posts to the website 2026-03-28 16:59:57 +01:00
itsfinniii
e6977ec7dd Add Blog Posts to website 2026-03-28 16:54:29 +01:00
itsfinniii
7e501c399b Add the Project and Blog index pages (no pagination just yet) 2026-03-28 16:40:54 +01:00
itsfinniii
5cbc906d65 Dont create page if exists is false 2026-03-28 15:58:23 +01:00
itsfinniii
a6e3a48313 Update FAQ list component 2026-03-28 15:55:57 +01:00
itsfinniii
8afacdbe7a Increase size on Last components 2026-03-28 15:42:04 +01:00
itsfinniii
ad25836de7 Add contact to the website, fix some small things 2026-03-28 15:38:55 +01:00
itsfinniii
5476783e0c Remove testing from LastAlbums component 2026-03-28 13:01:34 +01:00
itsfinniii
56cdc5257d Make image width full for LastBlogs and LastProjects 2026-03-28 13:01:02 +01:00
itsfinniii
e841c4f433 Add empty line in LastAlbums component 2026-03-28 13:00:25 +01:00
itsfinniii
0bb52c6818 Create last albums and fix filename_download in GraphQL 2026-03-28 13:00:12 +01:00
itsfinniii
e2598c58cf Do some small changes to the LastBlogs and LastProjects components 2026-03-28 12:32:06 +01:00
itsfinniii
231131cc41 Only show last blogs component if blogs are enabled, otherwise don't render anything 2026-03-26 22:25:46 +01:00
itsfinniii
2940446f66 Only show last projects component if projects are enabled on the website, otherwise don't render anything 2026-03-26 22:25:34 +01:00
itsfinniii
1c41348afe Add last projects component and fixing some small bugs 2026-03-26 22:22:21 +01:00
itsfinniii
82c0905c0e Add IDs to all components of the website 2026-03-26 22:08:04 +01:00
itsfinniii
1fd51a6a3f Create Last Blogs component with responsive design 2026-03-26 22:03:03 +01:00
itsfinniii
1e839680b4 Add colors to the website and calculate text color for background colors 2026-03-26 21:59:04 +01:00
itsfinniii
d94f77a958 Add CSS styling with primary and secondary color to the layout files 2026-03-26 21:49:05 +01:00
itsfinniii
d89c832565 Make reviews component responsive for mobile 2026-03-25 22:24:43 +01:00
itsfinniii
395d5d073a Add some shadows 2026-03-25 22:20:33 +01:00
itsfinniii
d2c04d58f2 Set content of review to the HTML converted markdown 2026-03-25 22:15:27 +01:00
itsfinniii
0f05090b99 Update Reviews.astro 2026-03-25 22:14:48 +01:00
itsfinniii
e179aa1743 Add reviews to the website 2026-03-25 22:14:32 +01:00
itsfinniii
edd6e54859 Add gray 50 background for even equipment table items 2026-03-25 21:01:20 +01:00
itsfinniii
51afdcfb44 Add full width to the Wall Of Text 2026-03-24 21:36:39 +01:00
itsfinniii
21135f4822 Make FAQ component full width 2026-03-24 21:29:19 +01:00
itsfinniii
cd30b1e3df Remove import that is no longer needed in Equipment Table component 2026-03-24 21:26:30 +01:00
itsfinniii
cce651531b Create the Equipment Table component 2026-03-24 21:24:54 +01:00
itsfinniii
c3973ddd1a Add the Frequently Asked Questions component to website 2026-03-23 22:13:21 +01:00
itsfinniii
486d6d9a20 Fix margins of Wall Of Text component 2026-03-23 19:45:31 +01:00
itsfinniii
dbb8d84143 Render upcoming event markdown as HTML 2026-03-23 19:42:31 +01:00
itsfinniii
0d9fe0eaec Fix mobile responsiveness for Upcoming Events 2026-03-23 19:34:43 +01:00
itsfinniii
b24a02e292 Remove unused useEffect import 2026-03-23 19:31:38 +01:00
itsfinniii
8a1f03a7b7 Animate the Upcoming Event component 2026-03-23 19:00:39 +01:00
itsfinniii
eb5f4efd40 Add Upcoming Event 2026-03-20 22:29:21 +01:00
itsfinniii
fac720c4a1 Create the upcoming events component 2026-03-20 21:51:54 +01:00
itsfinniii
dcaf313745 Make Hero responsive 2026-03-20 21:21:35 +01:00
itsfinniii
0d329d1e9c Make Text with Image responsive 2026-03-20 21:18:59 +01:00
itsfinniii
d5c8b166bc Remove console.log(); 2026-03-20 21:14:50 +01:00
itsfinniii
266e5d1b35 Render content of Text With Image component 2026-03-20 21:14:21 +01:00
itsfinniii
94aa8c356e Add Wall of Text component to website 2026-03-20 21:14:01 +01:00
itsfinniii
a5efc7415b Create hero and text with image components 2026-03-20 20:48:53 +01:00
itsfinniii
50dc6ec4c0 Add webpage component with Hero component 2026-03-20 18:07:16 +01:00
itsfinniii
423a4b74dd Update the web page layout 2026-03-20 18:06:59 +01:00
itsfinniii
a0f2c93a23 Correct the image URLs 2026-03-20 18:06:47 +01:00
itsfinniii
d53df4b898 Add Blog and Project layouts with SEO metadata 2026-03-20 18:06:29 +01:00
itsfinniii
b6f3fdd15e Add 'exists' flags to content types and set them 2026-03-20 16:55:33 +01:00
itsfinniii
4bb3fa3671 Add page types, index components, and layout 2026-03-20 16:40:21 +01:00
itsfinniii
cb4cb9e578 Add Tailwind CSS to layout 2026-03-15 22:17:27 +01:00
itsfinniii
6a14aca8ff Support album-based photo lookup & routing 2026-03-15 22:16:07 +01:00
itsfinniii
21d5ba23a4 Add page routing and content fetchers 2026-03-15 18:55:30 +01:00
itsfinniii
bc11be5669 Fix some more routing related things 2026-03-15 13:06:30 +01:00
itsfinniii
4f3cc40041 Finish first part of creating the full list of routes 2026-03-15 12:04:28 +01:00
itsfinniii
ff811327bb Update pages sitemaps 2026-03-15 11:33:38 +01:00
itsfinniii
6e26af71f4 Turn function for converting data into web page into seperate function 2026-03-15 11:28:10 +01:00
itsfinniii
6dcac27de8 Add a method to get all web pages 2026-03-15 11:23:47 +01:00
Quinn Hegeman
c1b89c5823 Update sitemaps for albums 2026-03-08 22:38:18 +01:00
Quinn Hegeman
ad73ab5672 Add a function to get all photo albums 2026-03-08 17:24:39 +01:00
Quinn Hegeman
44437c766b Fix params.page for currentPage in per page blogs sitemap 2026-03-08 16:20:33 +01:00
Quinn Hegeman
a5bc93f4d6 Get all projects and update sitemaps for it immediatly as well 2026-03-08 16:20:21 +01:00
Quinn Hegeman
52d8102d67 Fix the sitemap indexes 2026-03-08 16:03:08 +01:00
Quinn Hegeman
8f2e3bcde1 Update blog sitemap 2026-03-08 15:49:52 +01:00
Quinn Hegeman
f5c25dea75 Add last modified timestamps for all settings within getAllSettings 2026-03-08 15:28:45 +01:00
Quinn Hegeman
4d220e1be7 Add last modified to blogs 2026-03-08 15:16:32 +01:00
Quinn Hegeman
3317926f61 Add function that looks for all blogs 2026-03-08 15:12:19 +01:00
Quinn Hegeman
4d56dce1af Trim the robots text 2026-03-08 13:09:46 +01:00
Quinn Hegeman
2411c6fdc8 Create robots file 2026-03-08 13:04:41 +01:00
Quinn Hegeman
a1b202686f Update sitemap entry to albums.xml 2026-03-08 12:02:52 +01:00
Quinn Hegeman
8f0ede76f8 Fix some > to /> in WebpageLayout 2026-03-07 21:17:57 +01:00
Quinn Hegeman
f971e84f17 Add theme-color to WebpageLayout 2026-03-07 21:14:56 +01:00
Quinn Hegeman
640097c072 Create RSS feed base 2026-03-07 21:12:33 +01:00
Quinn Hegeman
b62865cf04 Fix the layout for the index page during development 2026-03-07 21:03:24 +01:00
Quinn Hegeman
403f8146d9 Create more sitemaps for the categories 2026-03-07 21:00:47 +01:00
Quinn Hegeman
dc22676254 Create the first sitemaps 2026-03-07 20:52:23 +01:00
Quinn Hegeman
5ac9285248 Create WebpageLayout.astro 2026-03-07 20:22:35 +01:00
Quinn Hegeman
e23c077a05 Make a function to get all website settings 2026-03-07 19:22:06 +01:00
Quinn Hegeman
f914b7db1c Set up graphql for requests 2026-03-07 18:51:46 +01:00
Quinn Hegeman
770198bb5b Add env example with Directus 2026-03-07 17:20:28 +01:00
Quinn Hegeman
025a84b2ef Add @ path to tsconfig 2026-03-07 17:20:15 +01:00
Quinn Hegeman
5caf0424cc Create a function for creating a Directus client in Astro 2026-03-07 17:01:25 +01:00
Quinn Hegeman
3865b4b089 Add Astro environment types 2026-03-07 16:57:45 +01:00
231b8cddc3 Merge pull request 'Setup the Astro project' (#3) from astro/setup-project into master
Reviewed-on: #3
2026-03-07 15:39:41 +00:00
Quinn Hegeman
ea54db99a6 Add Directus SDK for later in Astro 2026-03-07 16:28:49 +01:00
Quinn Hegeman
a4c05efa7a Add mdast-util-to-string and reading time plugins for later 2026-03-07 16:24:48 +01:00
Quinn Hegeman
269f5c8eb9 Install Tailwind and Preact 2026-03-07 16:18:49 +01:00
Quinn Hegeman
3e03a9ab27 Add Astro project to the repository 2026-03-07 16:07:09 +01:00
42e52b9811 Merge pull request 'Put pages M2M relation to Relations category in Directus' (#2) from directus/setup-project into master
Reviewed-on: #2
2026-03-07 15:02:34 +00:00
95b3060759 Merge pull request 'Setup Directus to the project' (#1) from directus/setup-project into master
Reviewed-on: #1
2026-03-07 15:02:00 +00:00
135 changed files with 13932 additions and 150 deletions

2
astro/.env.example Normal file
View File

@@ -0,0 +1,2 @@
DIRECTUS_URL="https://"
DIRECTUS_TOKEN=""

24
astro/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/

4
astro/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
astro/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

43
astro/README.md Normal file
View File

@@ -0,0 +1,43 @@
# Astro Starter Kit: Minimal
```sh
npm create astro@latest -- --template minimal
```
> 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun!
## 🚀 Project Structure
Inside of your Astro project, you'll see the following folders and files:
```text
/
├── public/
├── src/
│ └── pages/
│ └── index.astro
└── package.json
```
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
Any static assets, like images, can be placed in the `public/` directory.
## 🧞 Commands
All commands are run from the root of the project, from a terminal:
| Command | Action |
| :------------------------ | :----------------------------------------------- |
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
## 👀 Want to learn more?
Feel free to check [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat).

27
astro/astro.config.mjs Normal file
View File

@@ -0,0 +1,27 @@
// @ts-check
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import tailwindcss from '@tailwindcss/vite';
import graphql from '@rollup/plugin-graphql';
// https://astro.build/config
export default defineConfig({
integrations: [preact()],
output: 'static',
prefetch: true,
image: {
domains: ['development.directus.itsfinniii.com']
},
vite: {
plugins: [graphql(), tailwindcss()],
resolve: {
alias: {
react: "preact/compat",
"react-dom": "preact/compat",
},
},
optimizeDeps: {
exclude: ['@immich/justified-layout-wasm']
}
}
});

20
astro/changelogs.md Normal file
View File

@@ -0,0 +1,20 @@
# 1.0.0.0 - Release
**Release date: **
- Add web pages with the following components:
- Contact
- Equipment Table
- Frequently Asked Questions
- Hero
- Last Albums
- Last Blogs
- Last Projects
- Reviews
- Text with Side Image
- Upcoming Events
- Wall of Text
- Add blogs
- Add projects
- Add photo categories, photo albums and photos
- Add sitemaps
- Add robots.txt

6637
astro/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
astro/package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "astro",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/preact": "^5.1.2",
"@directus/sdk": "^21.2.0",
"@immich/justified-layout-wasm": "^0.4.3",
"@rollup/plugin-graphql": "^2.0.5",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.4",
"astro": "^6.1.9",
"highlight.js": "^11.11.1",
"markdown-it": "^14.1.1",
"markdown-it-highlightjs": "^4.3.0",
"md5": "^2.3.0",
"mdast-util-to-string": "^4.0.0",
"minify-xml": "^4.5.2",
"preact": "^10.28.4",
"react-responsive-masonry": "^2.7.2",
"reading-time": "^1.5.0",
"tailwindcss": "^4.2.4",
"tslib": "^2.8.1"
},
"overrides": {
"vite": "^7.0.0"
},
"devDependencies": {
"@types/markdown-it": "^14.1.2",
"@types/md5": "^2.3.6"
}
}

BIN
astro/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

9
astro/public/favicon.svg Normal file
View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 749 B

1
astro/src/build/files.ts Normal file
View File

@@ -0,0 +1 @@
// This file gets files, and puts them in the public folder before starting the build.

View File

@@ -0,0 +1,57 @@
---
import { getAllPaginatedBlogs } from '@/content/blogs/blogs';
import { getSettings } from '@/content/settings/settings';
import CalendarIcon from '@/icons/CalendarIcon.astro';
import { getImageSize, getImageUrl } from '@/lib/images';
import { markdownToHtml } from '@/lib/markdown';
import { getBlogRoute } from '@/lib/routing';
import { Image } from 'astro:assets';
interface Props {
page: BlogIndex;
}
const { page } = Astro.props;
const { pageNumber } = page;
const settings = await getSettings();
const blogs = await getAllPaginatedBlogs(settings, pageNumber);
---
<div
id={`blogindex-${pageNumber}`}
class="flex lg:flex-col flex-col py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18 w-full"
>
<div class="flex flex-col justify-center items-center gap-2.5">
<h1 class="text-5xl font-bold">{ settings.blog.title }</h1>
{ settings.blog.subtext !== null && (
<div set:html={markdownToHtml(settings.blog.subtext)}></div>
) }
</div>
<div class="grid grid-cols-2 gap-6">
{ blogs.map((blog) => {
const imageSize = getImageSize(blog.searchEngine.thumbnail.width, blog.searchEngine.thumbnail.height, 0.5);
return (
<a href={getBlogRoute(settings.blog, blog)} class={`flex flex-col gap-2`}>
<Image
src={getImageUrl(blog.searchEngine.thumbnail.url)}
alt={blog.title}
class="flex rounded-2xl shadow-md w-full"
width={imageSize.width}
height={imageSize.height}
/>
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-[28px]">{blog.title}</h4>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={20} height={20} />
<div>{blog.date}</div>
</div>
</div>
</a>
)
}) }
</div>
</div>

View File

@@ -0,0 +1,43 @@
---
import CalendarIcon from '@/icons/CalendarIcon.astro';
import { getImageSize, getImageUrl } from '@/lib/images';
import { markdownToHtml } from '@/lib/markdown';
import { getTypographyClasses } from '@/styles/markdownClasses';
import { Image } from 'astro:assets';
import LastBlogs from '../web/LastBlogs.astro';
interface Props {
blog: BlogPost;
}
const { blog } = Astro.props;
---
<div
id={`blog-${blog.id}`}
class="flex flex-row justify-center items-center"
>
<div class="flex lg:flex-col flex-col py-12 px-12 gap-y-8 gap-x-18 lg:max-w-[50%]">
<div class="flex flex-col gap-3">
<h1 class="font-semibold text-5xl">{blog.title}</h1>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={20} height={20} />
<div>{blog.date}</div>
</div>
</div>
<div class="aspect-1200/630 w-full max-w-full overflow-hidden">
<div class="w-full h-full rounded-2xl shadow-md object-cover">
<Image
src={blog.searchEngine.thumbnail.url}
width={blog.searchEngine.thumbnail.width}
height={blog.searchEngine.thumbnail.height}
alt={blog.title}
class="rounded-2xl"
/>
</div>
</div>
<div set:html={markdownToHtml(blog.content)} class={`${getTypographyClasses()} min-w-full`}></div>
</div>
</div>

View File

@@ -0,0 +1,24 @@
---
interface Props {
page: number;
totalPages: number;
urlTemplate: string;
}
const { page, totalPages, urlTemplate } = Astro.props;
---
<div class="flex flex-row gap-2">
{ totalPages < 7 && (
<>
{ [...Array(totalPages)].map((_: number, i: number) => (
<a
href={`${i + 1 === 1 ? urlTemplate : `${urlTemplate}/${i + 1}`}`}
class={`flex select-none hover:cursor-pointer text-lg justify-center items-center
${(i + 1 === page) ? "bg-(--ptc) text-(--ptt)" : "bg-neutral-200"} hover:bg-(--stc) hover:text-(--stt) duration-300 shadow-md rounded-full w-12 h-12`.trim()}>
<span>{i + 1}</span>
</a>
)) }
</>
) }
</div>

View File

@@ -0,0 +1,72 @@
---
import { getFooter } from "@/content/footer/footer";
import { Image } from "astro:assets";
const footer = await getFooter();
---
<footer class="w-full mt-4">
<div class="flex flex-col pt-12 pb-8 px-12 lg:container mx-auto">
<div class="flex md:flex-row flex-col justify-center pt-12 pb-8 md:px-1 px-0 lg:container md:mx-auto md:gap-y-8 gap-y-12 md:gap-x-24 gap-x-0">
{ (footer.title !== null || footer.logo !== null) && (
<div class="flex flex-col gap-3">
{ footer.title !== null && <h2 class="text-5xl font-bold">{footer.title}</h2>}
{ footer.logo !== null && (
<Image
src={footer.logo.url}
width={footer.logo.width}
height={footer.logo.height}
alt={footer.title ?? ""}
class="md:w-50 w-[50%] h-auto"
/>
) }
</div>
) }
{ footer.columns.map((column) => (
<div class="flex flex-col gap-3 w-fit">
<h2 class="text-4xl font-bold">{column.title}</h2>
<div class="flex flex-col gap-3 w-fit">
{column.links.map((link) => (
<a class="text-lg text-neutral-800 hover:text-(--ptc) duration-300" href={link.url}>
<span class="w-fit">{link.text}</span>
</a>
))}
</div>
</div>
)) }
</div>
{ footer.socials !== null && (
<div class="flex flex-row gap-3 justify-center">
{ footer.socials.map((social) => (
<a href={social.url} target="_blank" class="bg-neutral-300 hover:bg-neutral-200 duration-300 shadow-sm rounded-full">
<div class="p-2">
<Image
src={social.icon.url}
width={32}
height={32}
alt={social.name}
/>
</div>
</a>
)) }
</div>
) }
</div>
{ (footer.copyright !== null || footer.secondaryLinks !== null) && (
<div class="border-t border-t-neutral-200 w-full bg-neutral-50">
<div class="flex md:flex-row flex-col justify-between py-8 px-12 lg:container mx-auto gap-y-8 gap-x-24">
{ footer.copyright !== null && (<div class="text-neutral-600">{footer.copyright}</div>) }
{ footer.secondaryLinks !== null && (
<div class="flex flex-row gap-1.5">
{ footer.secondaryLinks.map((link) => (
<a class="text-neutral-600 hover:text-(--ptc) duration-300 w-fit" href={link.url}>{link.text}</a>
)) }
</div>
) }
</div>
</div>
) }
</footer>

View File

@@ -0,0 +1,55 @@
---
import { getAlbumRoute, getPhotoRoute } from '@/lib/routing';
import { AlbumPhotos } from './Album.tsx';
import { getImageSize, getImageUrl } from '@/lib/images';
import { getSettings } from '@/content/settings/settings';
import Pagination from '../common/Pagination.astro';
interface Props {
page: PhotoAlbumPage;
}
const settings = await getSettings();
const album = Astro.props.page;
const pageNumber = Astro.props.page.pageNumber;
const totalAlbumPages = Math.ceil(album.photos.length / settings.photo.album.perPage);
const sliceStartNumber = (pageNumber - 1) * settings.photo.album.perPage;
const sliceEndNumber = pageNumber * settings.photo.album.perPage;
const remappedPhotos: PhotoAlbumGalleryItem[] = [];
album.photos.slice(sliceStartNumber, sliceEndNumber).forEach((photo) => {
const resizedImage = getImageSize(photo.photo.width, photo.photo.height, 0.756);
remappedPhotos.push({
id: photo.id,
url: getPhotoRoute(settings.photo, album, photo),
photo: {
url: getImageUrl(photo.photo.url),
width: resizedImage.width,
height: resizedImage.height
},
text: photo.text
});
});
---
<div
id={`album-${album.id}`}
class="flex lg:flex-col flex-col py-12 px-12 lg:container mx-auto gap-y-10 gap-x-18 w-full"
>
<div class="flex flex-col gap-7">
<h1 class="text-5xl font-bold">{album.title}</h1>
<AlbumPhotos client:only photos={remappedPhotos} />
{ totalAlbumPages > 1 && (
<Pagination
page={pageNumber}
totalPages={totalAlbumPages}
urlTemplate={getAlbumRoute(settings.photo, album)}
/>
) }
</div>
</div>

View File

@@ -0,0 +1,87 @@
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
import { JustifiedLayout } from '@immich/justified-layout-wasm';
import { LoadingSpinner } from "@/icons/jsx/loadingSpinner";
export function AlbumPhotos(props: { photos: PhotoAlbumGalleryItem[] }) {
const containerRef = useRef(null);
const [ hasMounted, setHasMounted ] = useState<boolean>(false);
const [ layout, setLayout ] = useState<JustifiedLayout | null>(null);
const [ containerWidth, setContainerWidth ] = useState<number | null>(null);
const [ containerHeight, setContainerHeight ] = useState<number | null>(null);
useEffect(() => {
setHasMounted(true);
}, []);
useLayoutEffect(() => {
if (!hasMounted) {
return;
}
const observer = new ResizeObserver((entries) => {
setContainerWidth(entries[0].contentRect.width);
});
if (containerRef.current) {
observer.observe(containerRef.current);
}
return () => observer.disconnect();
}, [ hasMounted ]);
useEffect(() => {
if (containerWidth === null || !hasMounted) {
return;
}
const aspectRatios = new Float32Array(props.photos.map((photo => photo.photo.width / photo.photo.height)));
const justifiedLayout = new JustifiedLayout(aspectRatios, {
rowHeight: 265,
rowWidth: containerWidth,
spacing: 10,
heightTolerance: 0.11
});
setContainerHeight(justifiedLayout.containerHeight);
setLayout(justifiedLayout);
}, [ containerWidth, hasMounted ])
return (
<div ref={containerRef} id={`albumgallery`}>
{ layout !== null ? (
<div class="relative w-full" style={{ height: containerHeight }}>
{ props.photos.map((photo, index: number) => {
const layoutPosition = layout.getPosition(index);
return (
<a
href={photo.url}
key={`photo-${index}`}
class="group absolute overflow-hidden bg-neutral-200"
style={{
top: layoutPosition.top,
left: layoutPosition.left,
width: layoutPosition.width,
height: layoutPosition.height
}}
>
<img
src={photo.photo.url}
alt={photo.text ?? ""}
class="group-hover:scale-[101.5%] duration-200 w-full h-full"
loading="lazy"
/>
</a>
)
}) }
</div>
) : (
<div class="flex ">
<LoadingSpinner width={50} height={50} />
</div>
) }
</div>
)
}

View File

@@ -0,0 +1,49 @@
---
import { getCategoryAlbums } from "@/content/photos/albums";
import { getSettings } from "@/content/settings/settings";
import { getImageUrl } from "@/lib/images";
import { getAlbumRoute } from "@/lib/routing";
import { Image } from "astro:assets";
interface Props {
category: PhotoCategory;
}
const category = Astro.props.category;
const settings = await getSettings();
const categoryAlbums = await getCategoryAlbums(settings, category.category.url);
---
<div
id={`categoryindex`}
class="flex lg:flex-col flex-col py-12 px-12 lg:container mx-auto gap-y-10 gap-x-18 w-full"
>
<div class="flex flex-col justify-center items-center gap-2.5">
<h1 class="text-5xl font-bold">{categoryAlbums[0].category.title}</h1>
</div>
<div class="flex flex-col gap-5">
{ categoryAlbums.map((album) => (
<div class="flex flex-row justify-center items-center">
<a href={getAlbumRoute(settings.photo, album)} class="group relative block w-[70%] overflow-hidden rounded-2xl shadow-md">
<div>
<Image
src={getImageUrl(album.thumbnail.url)}
alt={album.title}
width={album.thumbnail.width}
height={album.thumbnail.height}
class="rounded-2xl transition-transform duration-300 group-hover:scale-102"
/>
<div class="absolute inset-0 bg-black/70 flex items-center justify-center p-6 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<h3 class="text-white text-5xl font-bold text-center tracking-tight">
{album.title}
</h3>
</div>
</div>
</a>
</div>
)) }
</div>
</div>

View File

@@ -0,0 +1,43 @@
---
import { getAllCategories } from "@/content/photos/categories";
import { getSettings } from "@/content/settings/settings"
import { getImageUrl } from "@/lib/images";
import { getCategoryRoute } from "@/lib/routing";
import { Image } from "astro:assets";
const settings = await getSettings();
const categories = await getAllCategories(settings);
---
<div
id={`categoryindex`}
class="flex lg:flex-col flex-col py-12 px-12 lg:container mx-auto gap-y-10 gap-x-18 w-full"
>
<div class="flex flex-col justify-center items-center gap-2.5">
<h1 class="text-5xl font-bold">Categories</h1>
</div>
<div class="flex flex-col gap-5">
{ categories.map((category) => (
<div class="flex flex-row justify-center items-center">
<a href={getCategoryRoute(settings.photo, category)} class="group relative block w-[70%] overflow-hidden rounded-2xl shadow-md">
<div>
<Image
src={getImageUrl(category.thumbnail.url)}
alt={category.title}
width={category.thumbnail.width}
height={category.thumbnail.height}
class="rounded-2xl transition-transform duration-300 group-hover:scale-102"
/>
<div class="absolute inset-0 bg-black/70 flex items-center justify-center p-6 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<h3 class="text-white text-5xl font-bold text-center tracking-tight">
{category.title}
</h3>
</div>
</div>
</a>
</div>
)) }
</div>
</div>

View File

@@ -0,0 +1,100 @@
---
import { getAlbum } from '@/content/photos/albums';
import { getSettings } from '@/content/settings/settings';
import ChevronUp from '@/icons/ChevronUp.astro';
import Download from '@/icons/Download.astro';
import Close from '@/icons/Close.astro';
import { getImageSize, getImageUrl } from '@/lib/images';
import { getAlbumRoute, getPhotoRoute } from '@/lib/routing';
import { getImage } from 'astro:assets';
import { Image } from 'astro:assets';
import { getPhotoHash } from '@/lib/hash';
interface Props {
page: PhotoPage;
}
const photo = Astro.props.page;
const settings = await getSettings();
const album = await getAlbum(settings, photo.album.url);
const photoIndex = album.photos.findIndex(p => p.id === photo.id);
let previousUrl: string | null = null;
let nextUrl: string | null = null;
// Check for previous photo
if (photoIndex > 0) {
previousUrl = getPhotoRoute(settings.photo, album, album.photos[photoIndex - 1]);
}
// Check for next photo
if (photoIndex + 1 < album.photos.length) {
nextUrl = getPhotoRoute(settings.photo, album, album.photos[photoIndex + 1]);
}
const albumPageNumber = Math.ceil((photoIndex + 1) / settings.photo.album.perPage);
const returnUrl = albumPageNumber > 1
? `${getAlbumRoute(settings.photo, album)}/${albumPageNumber}`
: getAlbumRoute(settings.photo, album);
const resizedImageSize = getImageSize(photo.photo.width, photo.photo.height, 1);
const downloadImageSize = getImageSize(photo.photo.width, photo.photo.height, 5);
const downloadUrl = await getImage({
src: getImageUrl(photo.photo.url),
width: downloadImageSize.width,
height: downloadImageSize.height,
format: "jpeg",
quality: 100
});
const downloadFileName = `${album.url.replaceAll("/", "")}-${getPhotoHash(photo)}.jpeg`;
---
<div class="h-screen flex flex-col justify-center items-center">
<div class="flex flex-col justify-center items-center h-full">
<Image
src={getImageUrl(photo.photo.url)}
width={resizedImageSize.width}
height={resizedImageSize.height}
alt={photo.text ?? ""}
class="h-full w-full object-contain"
/>
</div>
<div class="flex flex-row gap-6 absolute top-8 right-8 text-white py-2.5 px-5 bg-[#000000aa] rounded-full z-10">
<a data-astro-prefetch href={downloadUrl.src} download={downloadFileName}>
<Download width={36} height={36} />
</a>
<a data-astro-prefetch href={returnUrl}>
<Close width={36} height={36} />
</a>
</div>
{ photo.text !== null && (
<div class="absolute bottom-0 text-white text-xl bg-[#000000aa] w-full px-20 py-8">{photo.text.trim()}</div>
) }
{ previousUrl !== null && (
<a
data-astro-prefetch
href={previousUrl}
class="absolute left-8 text-white p-3 bg-[#000000aa] rounded-full z-10 rotate-270"
>
<ChevronUp width={28} height={28} />
</a>
) }
{ nextUrl !== null && (
<a
data-astro-prefetch
href={nextUrl}
class="absolute right-8 text-white p-3 bg-[#000000aa] rounded-full z-10 rotate-90"
>
<ChevronUp width={28} height={28} />
</a>
) }
</div>

View File

@@ -0,0 +1,57 @@
---
import { getSettings } from '@/content/settings/settings';
import { getAllPaginatedProjects } from '@/content/projects/projects';
import { markdownToHtml } from '@/lib/markdown';
import { Image } from 'astro:assets';
import { getProjectRoute } from '@/lib/routing';
import CalendarIcon from '@/icons/CalendarIcon.astro';
import { getImageSize, getImageUrl } from '@/lib/images';
import { promise } from 'astro:schema';
interface Props {
page: ProjectIndex;
}
const { page } = Astro.props;
const { pageNumber } = page;
const settings = await getSettings();
const projects = await getAllPaginatedProjects(settings, pageNumber);
---
<div
id={`projectindex-${pageNumber}`}
class="flex lg:flex-col flex-col py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18 w-full"
>
<div class="flex flex-col justify-center items-center gap-2.5">
<h1 class="text-5xl font-bold">{ settings.project.title }</h1>
{ settings.project.subtext !== null && (
<div set:html={markdownToHtml(settings.project.subtext)}></div>
) }
</div>
<div class="grid grid-cols-2 gap-6">
{ projects.map((project) => {
const imageSize = getImageSize(project.searchEngine.thumbnail.width, project.searchEngine.thumbnail.height, 0.5);
return (
<a href={getProjectRoute(settings.project, project)} class={`flex flex-col gap-2`}>
<Image
src={getImageUrl(project.searchEngine.thumbnail.url)}
alt={project.title}
class="flex rounded-2xl shadow-md w-full"
width={imageSize.width}
height={imageSize.height}
/>
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-[28px]">{project.title}</h4>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={20} height={20} />
<div>{project.date}</div>
</div>
</div>
</a>
)
}) }
</div>
</div>

View File

@@ -0,0 +1,44 @@
---
import CalendarIcon from '@/icons/CalendarIcon.astro';
import { getImageSize, getImageUrl } from '@/lib/images';
import { markdownToHtml } from '@/lib/markdown';
import { getTypographyClasses } from '@/styles/markdownClasses';
import { Image } from 'astro:assets';
interface Props {
project: ProjectPost;
}
const { project } = Astro.props;
const imageSize = getImageSize(project.searchEngine.thumbnail.width, project.searchEngine.thumbnail.height, 1);
---
<div
id={`project-${project.id}`}
class="flex flex-row justify-center items-center"
>
<div class="flex lg:flex-col flex-col py-12 px-12 gap-y-8 gap-x-18 lg:max-w-[50%]">
<div class="flex flex-col gap-3">
<h1 class="font-semibold text-5xl">{project.title}</h1>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={20} height={20} />
<div>{project.date}</div>
</div>
</div>
<div class="aspect-1200/630 w-full max-w-full overflow-hidden">
<div class="w-full h-full rounded-2xl shadow-md object-cover">
<Image
src={getImageUrl(project.searchEngine.thumbnail.url)}
width={imageSize.width}
height={imageSize.height}
alt={project.title}
class="rounded-2xl"
/>
</div>
</div>
<div set:html={markdownToHtml(project.content)} class={`${getTypographyClasses()} min-w-full`}></div>
</div>
</div>

View File

@@ -0,0 +1,34 @@
---
import { markdownToHtml } from '@/lib/markdown';
import { Image } from 'astro:assets';
interface Props {
contact: ContactComponent;
}
const contact = Astro.props.contact;
---
<div
id={`contact-${contact.id}`}
class="flex lg:flex-row flex-col lg:justify-center justify-center items-center py-12 px-12 lg:container mx-auto gap-y-8 lg:gap-x-28 gap-x-18 lg:text-left text-center"
>
<div class="flex flex-col gap-1.5">
<h2 class="text-5xl font-bold">{contact.title}</h2>
<div set:html={markdownToHtml(contact.text)}></div>
</div>
<div class="flex flex-col gap-2">
{ contact.methods.map((method) => (
<a href={method.url} target="_blank" style={{ "--sc": method.color }} class="flex flex-row items-center gap-2 text-lg hover:text-(--sc) duration-200">
<Image
src={method.icon.url}
alt={method.title}
width="30"
height="30"
/>
<span>{method.title}</span>
</a>
)) }
</div>
</div>

View File

@@ -0,0 +1,45 @@
---
import { markdownToHtml } from '@/lib/markdown';
import { Image } from 'astro:assets';
interface Props {
equipment: EquipmentTableComponent;
}
const equipment = Astro.props.equipment;
---
<div
id={`equipment-${equipment.id}`}
class="flex lg:flex-row flex-col lg:justify-center justify-center py-12 px-12 lg:container mx-auto gap-y-8 lg:gap-x-28 gap-x-18 lg:text-left text-center"
>
<div class="flex flex-col gap-1.5">
<h2 class="text-5xl font-bold">{equipment.title}</h2>
{ equipment.text !== null && (
<div set:html={markdownToHtml(equipment.text)}></div>
) }
</div>
<table class="w-fit text-lg">
<tbody>
{ equipment.items.map((item, index: number) => (
<tr class="odd:bg-gray-100 even:bg-gray-50 my-2">
<th class={`text-right pr-4 py-0.5 leading-tight ps-4 ${index === 0 && "rounded-tl-2xl"} ${(index + 1) === equipment.items.length && "rounded-bl-2xl shadow-sm"}`}>
<div class="flex flex-row justify-end items-center gap-1.5">
{ item.icon !== null && (
<Image
src={item.icon.url}
alt={item.text}
width="24"
height="24"
/>
) }
<span>{item.title}</span>
</div>
</th>
<td class={`text-left leading-tight pe-4 ${index === 0 && "rounded-tr-2xl"} ${(index + 1) === equipment.items.length && "rounded-br-2xl shadow-sm"}`}>{item.text}</td>
</tr>
)) }
</tbody>
</table>
</div>

View File

@@ -0,0 +1,25 @@
---
import { markdownToHtml } from '@/lib/markdown';
import { QuestionList } from '@/components/web/subcomponents/QuestionList.tsx';
interface Props {
faq: FrequentlyAskedQuestionsComponent;
}
const faq = Astro.props.faq;
---
<div
id={`faq-${faq.id}`}
class="flex lg:flex-row flex-col justify-between py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18 w-full"
>
<div class="flex flex-col gap-2.5 lg:w-[50%] w-full">
<h2 class="text-5xl font-bold">{faq.title}</h2>
{ faq.text !== null && (
<div set:html={markdownToHtml(faq.text)}></div>
) }
</div>
<div class="lg:w-[50%] w-full">
<QuestionList client:load questions={faq.questions} />
</div>
</div>

View File

@@ -0,0 +1,32 @@
---
import { Image } from 'astro:assets';
interface Props {
hero: HeroComponent;
}
const hero = Astro.props.hero;
---
<div id={`hero-${hero.id}`} class="flex w-full">
<div class="flex w-full h-screen static">
<Image
src={hero.backgroundImage.url}
width={hero.backgroundImage.width}
height={hero.backgroundImage.height}
class="flex w-full h-screen object-cover z-1"
alt=""
/>
<div class="absolute flex items-center w-full h-screen bg-[#00000082] z-10">
<div class="absolute text-white lg:left-40 lg:p-8 p-16 z-30">
<div class="flex flex-col gap-2.5 text-left">
<h1 class="text-white font-bold md:text-8xl sm:text-7xl text-6xl w-fit">{hero.title}</h1>
{ hero.text !== null && (
<div class="lg:text-2xl text-lg max-w-170">{hero.text}</div>
) }
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,95 @@
---
import { getLastAlbums } from "@/content/photos/albums";
import { getSettings } from "@/content/settings/settings";
import CalendarIcon from "@/icons/CalendarIcon.astro";
import { getImageSize, getImageUrl } from "@/lib/images";
import { getAlbumRoute } from "@/lib/routing";
import { Image } from "astro:assets";
interface Props {
albums: LastGalleriesComponent;
}
function calculateSizeClasses(amount: number, length: number) {
if (amount === 2 || length <= 2) {
return "lg:w-[45%] w-full";
}
else {
return "lg:w-[31%] w-full";
}
}
const albums = Astro.props.albums;
const settings = await getSettings();
const lastAlbums = await getLastAlbums(albums.amount);
const size = calculateSizeClasses(albums.amount, lastAlbums.length);
---
{ (settings.photo.enabled && lastAlbums.length > 0) && (
<div
id={`lastalbums-${albums.id}`}
class="flex lg:flex-col flex-col py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18 w-full"
>
<div class="flex flex-row justify-between items-center w-full">
<h2 class="text-4xl font-bold">{albums.title}</h2>
<div>
<a
href={settings.photo.categoryIndex.indexRouteTemplate}
class="text-(--ptt) bg-(--ptc) hover:text-(--stt) hover:bg-(--stc) duration-200 py-3 px-5 rounded-full text-lg"
>
{albums.readMoreButtonText}
</a>
</div>
</div>
{ lastAlbums.length >= 4 ? (
<div class="grid lg:grid-cols-2 lg:grid-rows-2 grid-cols-1 grid-rows-4 gap-x-10 gap-y-8">
{ lastAlbums.map((album) => {
const imageSize = getImageSize(album.thumbnail.width, album.thumbnail.height, 0.5);
return (
<a href={getAlbumRoute(settings.photo, album)} class={`w-full flex flex-col gap-2`}>
<Image
src={getImageUrl(album.thumbnail.url)}
alt={album.title}
class="flex rounded-2xl shadow-md w-full"
width={imageSize.width}
height={imageSize.height}
/>
<h4 class="font-semibold text-[28px]">{album.title}</h4>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={20} height={20} />
<div>{album.startDate}</div>
</div>
</a>
)
}) }
</div>
) : (
<div class="flex flex-col lg:flex-row lg:justify-between gap-y-6">
{ lastAlbums.map((album) => {
const imageSize = getImageSize(album.thumbnail.width, album.thumbnail.height, 0.5);
return (
<a href={getAlbumRoute(settings.photo, album)} class={`${size} flex flex-col gap-2`}>
<Image
src={getImageUrl(album.thumbnail.url)}
alt={album.title}
class="flex rounded-2xl shadow-md w-full"
width={imageSize.width}
height={imageSize.height}
/>
<h4 class="font-semibold text-[28px]">{album.title}</h4>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={20} height={20} />
<div>{album.startDate}</div>
</div>
</a>
)
}) }
</div>
) }
</div>
) }

View File

@@ -0,0 +1,68 @@
---
import { getLastBlogs } from '@/content/blogs/blogs';
import { getSettings } from '@/content/settings/settings';
import CalendarIcon from '@/icons/CalendarIcon.astro';
import { getImageSize, getImageUrl } from '@/lib/images';
import { getBlogRoute } from '@/lib/routing';
import { Image } from 'astro:assets';
interface Props {
blogs: LastBlogsComponent;
}
function calculateSizeClasses(amount: number, length: number) {
if (amount === 2 || length <= 2) {
return "lg:w-[45%] w-full";
}
else {
return "lg:w-[31%] w-full";
}
}
const blogs = Astro.props.blogs;
const settings = await getSettings();
const lastBlogs = await getLastBlogs(blogs.amount);
const size = calculateSizeClasses(blogs.amount, lastBlogs.length);
---
{ (settings.blog.enabled && lastBlogs.length > 0) && (
<div
id={`lastblogs-${blogs.id}`}
class="flex lg:flex-col flex-col py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18 w-full"
>
<div class="flex flex-row justify-between items-center w-full">
<h2 class="text-4xl font-bold">{blogs.title}</h2>
<div>
<a
href={settings.blog.indexRouteTemplate}
class="text-(--ptt) bg-(--ptc) hover:text-(--stt) hover:bg-(--stc) duration-200 py-3 px-5 rounded-full text-lg"
>
{blogs.readMoreButtonText}
</a>
</div>
</div>
<div class="flex flex-col lg:flex-row lg:justify-between gap-y-6">
{ lastBlogs.map((blog) => {
const imageSize = getImageSize(blog.searchEngine.thumbnail.width, blog.searchEngine.thumbnail.height, 0.5);
return (
<a href={getBlogRoute(settings.blog, blog)} class={`${size} flex flex-col gap-2`}>
<Image
src={getImageUrl(blog.searchEngine.thumbnail.url)}
alt={blog.title}
class="flex rounded-2xl shadow-md w-full"
width={imageSize.width}
height={imageSize.height}
/>
<h4 class="font-semibold text-[28px]">{blog.title}</h4>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={20} height={20} />
<div>{blog.date}</div>
</div>
</a>
)
}) }
</div>
</div>
) }

View File

@@ -0,0 +1,68 @@
---
import { getLastProjects } from '@/content/projects/projects';
import { getSettings } from '@/content/settings/settings';
import CalendarIcon from '@/icons/CalendarIcon.astro';
import { getImageSize, getImageUrl } from '@/lib/images';
import { getProjectRoute } from '@/lib/routing';
import { Image } from 'astro:assets';
interface Props {
projects: LastProjectsComponent;
}
function calculateSizeClasses(amount: number, length: number) {
if (amount === 2 || length <= 2) {
return "lg:w-[45%] w-full";
}
else {
return "lg:w-[31%] w-full";
}
}
const projects = Astro.props.projects;
const settings = await getSettings();
const lastProjects = await getLastProjects(projects.amount);
const size = calculateSizeClasses(projects.amount, lastProjects.length);
---
{ (settings.project.enabled && lastProjects.length > 0) && (
<div
id={`lastprojects-${projects.id}`}
class="flex lg:flex-col flex-col py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18 w-full"
>
<div class="flex flex-row justify-between items-center w-full">
<h2 class="text-4xl font-bold">{projects.title}</h2>
<div>
<a
href={settings.project.indexRouteTemplate}
class="text-(--ptt) bg-(--ptc) hover:text-(--stt) hover:bg-(--stc) duration-200 py-3 px-5 rounded-full text-lg"
>
{projects.readMoreButtonText}
</a>
</div>
</div>
<div class="flex flex-col lg:flex-row lg:justify-between gap-y-6">
{ lastProjects.map((project) => {
const imageSize = getImageSize(project.searchEngine.thumbnail.width, project.searchEngine.thumbnail.height, 0.5);
return (
<a href={getProjectRoute(settings.project, project)} class={`${size} flex flex-col gap-2`}>
<Image
src={getImageUrl(project.searchEngine.thumbnail.url)}
alt={project.title}
class="flex rounded-2xl shadow-md w-full"
width={imageSize.width}
height={imageSize.height}
/>
<h4 class="font-semibold text-[28px]">{project.title}</h4>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={20} height={20} />
<div>{project.date}</div>
</div>
</a>
)
}) }
</div>
</div>
) }

View File

@@ -0,0 +1,53 @@
---
import { markdownToHtml } from '@/lib/markdown';
import StarRating from './subcomponents/StarRating.astro';
interface Props {
reviews: ReviewListComponent;
}
const reviews = Astro.props.reviews;
let totalStars: number = 0;
const totalReviews: number = reviews.reviews.length;
reviews.reviews.forEach((review) => {
totalStars = totalStars + review.stars;
});
const averageStars = Math.round((totalStars / totalReviews) * 10) / 10;
const reviewsToShow = reviews.reviews.slice(0, 5);
---
<div
id={`reviews-${reviews.id}`}
class="flex lg:flex-row flex-col py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18 w-full"
>
<div class="flex flex-col lg:gap-5 gap-2.25 lg:min-w-[32.5%]">
<div class="flex flex-col">
<h2 class="text-3xl font-bold">{reviews.title}</h2>
<div>{reviews.text}</div>
</div>
<div class="flex flex-col gap-1.5 text-lg">
<StarRating size="big" stars={averageStars} />
<p class="italic">{averageStars}</span> stars out of {totalReviews} {totalReviews === 1 ? "review" : "reviews"}.</p>
</div>
</div>
<div class="flex flex-col gap-6.5">
{ reviewsToShow.map((review) => (
<div class="flex flex-col justify-center gap-3 bg-neutral-100 py-4 px-5.5 rounded-2xl shadow-sm">
<div class="flex flex-col justify-center gap-1.25">
<h4 class="text-2xl font-semibold">{review.name}</h4>
<div>
<StarRating size="small" stars={review.stars} />
</div>
</div>
<div set:html={markdownToHtml(review.review)}>
</div>
</div>
)) }
</div>
</div>

View File

@@ -0,0 +1,37 @@
---
import { markdownToHtml } from '@/lib/markdown';
import { Image } from 'astro:assets';
interface Props {
textWithImage: TextWithImageComponent;
}
const textWithImage = Astro.props.textWithImage;
const imageSize = () => {
if (textWithImage.imageSize === "small") return "aspect-2/1";
else if (textWithImage.imageSize === "medium") return "aspect-26/17";
else return "aspect-39/29";
}
---
<div
id={`textwithimage-${textWithImage.id}`}
class={`flex ${textWithImage.imageSide === "right" ? "lg:flex-row flex-col" : "lg:flex-row-reverse flex-col"} justify-between items-center py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18`}
>
<div class="flex flex-col gap-2.5 lg:w-[50%] w-full">
<h2 class="text-5xl font-bold">{textWithImage.title}</h2>
{ textWithImage.text !== null && (
<div set:html={markdownToHtml(textWithImage.text)}></div>
) }
</div>
<div class="lg:w-[50%] w-full">
<Image
src={textWithImage.image.url}
width={textWithImage.image.width}
height={textWithImage.image.height}
class={`rounded-2xl shadow-sm ${imageSize()} object-cover`}
alt=""
/>
</div>
</div>

View File

@@ -0,0 +1,57 @@
---
import CalendarIcon from '@/icons/CalendarIcon.astro';
import { Image } from 'astro:assets';
import { upcomingEvent as UpcomingEvent } from './subcomponents/UpcomingEvent';
import { markdownToHtml } from '@/lib/markdown';
interface Props {
upcomingEvents: UpcomingEventsComponent;
}
const upcomingEvents = Astro.props.upcomingEvents;
---
<div
id={`upcomingevents-${upcomingEvents.id}`}
class="flex flex-col justify-between items-center py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18"
>
<h1 class="text-5xl font-bold">{upcomingEvents.title}</h1>
<div class="flex flex-col items-start gap-7 w-full">
{ upcomingEvents.events.map((event) => (
<div id={event.id} class="flex lg:flex-row flex-col items-center justify-center lg:gap-y-8 gap-y-5 gap-x-18">
<div class="lg:w-[50%] w-full">
<Image
src={event.thumbnail.url}
width={event.thumbnail.width}
height={event.thumbnail.height}
class={`aspect-15/9 rounded-2xl shadow-md object-cover`}
alt=""
/>
</div>
<div class="flex flex-col gap-2.5 lg:w-[40%] w-full">
<h3 class="text-3xl font-semibold">{event.title}</h3>
<div class="flex flex-row items-center gap-1.5 text-neutral-900">
<CalendarIcon width={24} height={24} />
{ event.endDate !== null ? (
<p>{event.startDate} - {event.endDate}</p>
) : (
<p>{event.startDate}</p>
) }
</div>
<div class="mt-3.5">
<UpcomingEvent
client:load
event={{
title: event.title,
description: markdownToHtml(event.description),
startDate: event.startDate,
endDate: event.endDate
}}
/>
</div>
</div>
</div>
)) }
</div>
</div>

View File

@@ -0,0 +1,17 @@
---
import { markdownToHtml } from "@/lib/markdown";
interface Props {
wallOfText: WallOfTextComponent;
}
const wallOfText = Astro.props.wallOfText;
---
<div
id={`walloftext-${wallOfText.id}`}
class="flex flex-col py-12 px-12 lg:container mx-auto gap-y-8 gap-x-18 w-full"
>
<h2 class="text-5xl font-bold">{wallOfText.title}</h2>
<div set:html={markdownToHtml(wallOfText.text)}></div>
</div>

View File

@@ -0,0 +1,37 @@
import { useState } from "preact/hooks";
export function QuestionList(props: { questions: FrequentlyAskedQuestion[]; }) {
const [ open, setOpen ] = useState<number | null>(null);
const toggle = (index: number) => {
setOpen(open === index ? null : index);
};
return (
<div className="w-full">
{props.questions.map((question, index: number) => (
<div
key={index}
onClick={() => toggle(index)}
className={`w-full overflow-hidden border-l border-r border-t cursor-pointer ${open === index ? "bg-(--ptc) text-(--ptt)" : "bg-white"} ${index === 0 ? "rounded-t-2xl border-t border-(--ptc)" : ""} ${(index + 1) === props.questions.length ? "rounded-b-2xl border-b border-(--ptc) shadow-md" : "border-(--ptc)"}`}
>
<h4 className="text-lg font-semibold py-3 px-5 select-none">
{question.question}
</h4>
<div
className={`grid transition-[grid-template-rows,opacity] duration-300 ease-in-out ${
open === index ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
}`}
>
<div className="overflow-hidden">
<div className="px-5 pb-5 text-sm text-gray-700">
{question.answer}
</div>
</div>
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,71 @@
---
interface Props {
stars: number;
size: "big" | "small";
}
function roundToCustomHalf(value: number) {
const whole = Math.floor(value);
const decimal = value - whole;
if (decimal < 0.25) return whole;
if (decimal < 0.75) return whole + 0.5;
return whole + 1;
}
const stars = roundToCustomHalf(Astro.props.stars);
const totalStars = 5;
---
<div class="flex flex-row gap-[4.25px]">
{Array.from({ length: totalStars }).map((_, i) => {
const starValue = i + 1;
let type = "empty";
if (stars >= starValue) {
type = "full";
} else if (stars >= starValue - 0.5) {
type = "half";
}
return (
<svg width={Astro.props.size === "big" ? 32 : 20} height={Astro.props.size === "big" ? 32 : 20} viewBox="3 0 24 24">
{type === "full" && (
<path
fill="gold"
d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22
12 18.56 5.82 22 7 14.14 2 9.27
8.91 8.26 12 2z"
/>
)}
{type === "half" && (
<>
<defs>
<linearGradient id={`half-${i}`}>
<stop offset="50%" stop-color="gold" />
<stop offset="50%" stop-color="lightgray" />
</linearGradient>
</defs>
<path
fill={`url(#half-${i})`}
d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22
12 18.56 5.82 22 7 14.14 2 9.27
8.91 8.26 12 2z"
/>
</>
)}
{type === "empty" && (
<path
fill="lightgray"
d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22
12 18.56 5.82 22 7 14.14 2 9.27
8.91 8.26 12 2z"
/>
)}
</svg>
);
})}
</div>

View File

@@ -0,0 +1,51 @@
import { CalendarIcon } from "@/icons/jsx/calendarIcon";
import { useState } from "preact/hooks"
export function upcomingEvent(props: { event: UpcomingEventProps }) {
const [ isOpen, setIsOpen ] = useState<boolean>(false);
const [ isVisible, setIsVisible ] = useState<boolean>(false);
const open = () => {
setIsOpen(true);
setTimeout(() => setIsVisible(true), 10); // allow DOM mount
};
const close = () => {
setIsVisible(false);
setTimeout(() => setIsOpen(false), 200); // match animation duration
};
return (
<div>
<div className="hover:cursor-pointer text-indigo-400 underline w-fit text-lg font-semibold" onClick={open}>{"Read more >>"}</div>
{ isOpen && (
<div
onClick={close}
className={`fixed inset-0 flex items-center justify-center bg-[#000000bb] shadow-md z-999999999999 duration-200 ${isVisible ? "opacity-100" : "opacity-0"}`}
>
<div
onClick={(e) => e.stopPropagation()}
className={`relative bg-white rounded-2xl shadow-xl max-w-md w-full p-6 transition-all duration-200 ${isVisible ? "opacity-100 scale-100" : "opacity-0 scale-95"}`}
>
<div className="flex flex-col gap-1.5">
<h4 className="text-2xl font-semibold">{props.event.title}</h4>
<div class="flex flex-row items-center gap-1.5 text-neutral-900 text-sm">
<CalendarIcon width={24} height={24} />
{ props.event.endDate !== null ? (
<p>{props.event.startDate} - {props.event.endDate}</p>
) : (
<p>{props.event.startDate}</p>
) }
</div>
<div className="mt-3" dangerouslySetInnerHTML={{ __html: props.event.description }}>
</div>
</div>
</div>
</div>
) }
</div>
)
}

View File

@@ -0,0 +1,37 @@
---
import FrequentlyAskedQuestions from '../web/FrequentlyAskedQuestions.astro';
import Hero from '../web/Hero.astro';
import TextWithImage from '../web/TextWithImage.astro';
import UpcomingEvents from '../web/UpcomingEvents.astro';
import WallOfText from '../web/WallOfText.astro';
import EquipmentTable from '../web/EquipmentTable.astro';
import Reviews from '../web/Reviews.astro';
import LastBlogs from '../web/LastBlogs.astro';
import LastProjects from '../web/LastProjects.astro';
import LastAlbums from '../web/LastAlbums.astro';
import Contact from '../web/Contact.astro';
interface Props {
webpage: WebpageComponent[];
}
const components = Astro.props.webpage;
---
<div class="flex flex-col">
{ components.map((component) => (
<Fragment>
{ component.component === "Hero" && <Hero hero={component} /> }
{ component.component === "TextWithImage" && <TextWithImage textWithImage={component} /> }
{ component.component === "WallOfText" && <WallOfText wallOfText={component} /> }
{ component.component === "UpcomingEvents" && <UpcomingEvents upcomingEvents={component} /> }
{ component.component === "FrequentlyAskedQuestions" && <FrequentlyAskedQuestions faq={component} /> }
{ component.component === "EquipmentTable" && <EquipmentTable equipment={component} /> }
{ component.component === "Reviews" && <Reviews reviews={component} /> }
{ component.component === "Contact" && <Contact contact={component} /> }
{ component.component === "LastBlogs" && <LastBlogs blogs={component} /> }
{ component.component === "LastProjects" && <LastProjects projects={component} /> }
{ component.component === "LastGalleries" && <LastAlbums albums={component} /> }
</Fragment>
)) }
</div>

View File

@@ -0,0 +1,329 @@
import { createDirectusConnection } from "@/lib/directus";
import { print } from 'graphql';
import getBlogs from '@/graphql/blogs/getBlogs.graphql';
import getBlogPost from '@/graphql/blogs/getBlog.graphql';
import getLastBlogPosts from '@/graphql/blogs/getLastBlogPosts.graphql';
import getPaginatedBlogs from '@/graphql/blogs/getPaginatedBlogs.graphql';
import { formatDate } from "@/lib/dates";
import { getImageSize, getImageUrl } from "@/lib/images";
import { getImage } from "astro:assets";
export async function getAllBlogs(settings: GlobalSettings): Promise<BlogPost[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getBlogs), {
date: formatDate(new Date(), "%Y-%M-%D")
});
let blogs: BlogPost[] = [];
result["Blogs"].forEach((blogRecord: any) => {
let dates: string[] = [
settings.blog.lastModified.toISOString(),
settings.website.lastModified.toISOString(),
blogRecord["date_created"],
blogRecord["date_updated"],
blogRecord["search_engine"][0]["date_created"],
blogRecord["search_engine"][0]["date_updated"],
blogRecord["search_engine"][0]["thumbnail"]["created_on"]
];
const blogThumbnailImage =
getImageSize(blogRecord["search_engine"][0]["thumbnail"]["width"], blogRecord["search_engine"][0]["thumbnail"]["height"], 0.756);
const blog: BlogPost = {
exists: true,
type: "BlogPost",
id: blogRecord["id"],
lastModified: new Date(),
title: blogRecord["title"],
content: blogRecord["content"],
date: blogRecord["date"],
url: blogRecord["url"],
thumbnail: {
url: blogRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: blogRecord["search_engine"][0]["thumbnail"]["width"],
height: blogRecord["search_engine"][0]["thumbnail"]["height"]
},
searchEngine: {
title: blogRecord["search_engine"][0]["title"],
description: blogRecord["search_engine"][0]["description"],
allowCrawlers: blogRecord["search_engine"][0]["allow_crawler"],
canonical: blogRecord["search_engine"][0]["canonical"],
priority: blogRecord["search_engine"][0]["priority"],
thumbnail: {
url: blogRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: blogThumbnailImage.width,
height: blogThumbnailImage.height
}
},
tags: []
};
blogRecord["tags"].forEach((tagRecord: any) => {
blog["tags"].push({
text: tagRecord["Tags_id"]["text"],
code: tagRecord["Tags_id"]["code"],
color: tagRecord["Tags_id"]["color"]
});
dates.push(tagRecord["Tags_id"]["date_created"]);
dates.push(tagRecord["Tags_id"]["date_updated"]);
});
if (dates.filter(e => e !== null).length === 0) {
blog.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
blog.lastModified = new Date(sortedDates[0]);
}
blogs.push(blog);
});
return blogs;
}
export async function getBlog(settings: GlobalSettings, route: string): Promise<BlogPost> {
const client = await createDirectusConnection();
const result = await client.query(print(getBlogPost), {
route: route
});
const blogRecord = result["Blogs"][0];
let dates: string[] = [
settings.blog.lastModified.toISOString(),
settings.website.lastModified.toISOString(),
blogRecord["date_created"],
blogRecord["date_updated"],
blogRecord["search_engine"][0]["date_created"],
blogRecord["search_engine"][0]["date_updated"],
blogRecord["search_engine"][0]["thumbnail"]["created_on"]
];
const blogThumbnailImage = getImageSize(blogRecord["search_engine"][0]["thumbnail"]["width"],
blogRecord["search_engine"][0]["thumbnail"]["height"], 0.756);
const thumbnail = await getImage({
src: getImageUrl(blogRecord["search_engine"][0]["thumbnail"]["filename_disk"]),
width: blogThumbnailImage.width,
height: blogThumbnailImage.height,
format: "jpeg"
});
const blog: BlogPost = {
exists: true,
type: "BlogPost",
id: blogRecord["id"],
lastModified: new Date(),
title: blogRecord["title"],
content: blogRecord["content"],
date: blogRecord["date"],
url: blogRecord["url"],
thumbnail: {
url: blogRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: blogRecord["search_engine"][0]["thumbnail"]["width"],
height: blogRecord["search_engine"][0]["thumbnail"]["height"]
},
searchEngine: {
title: blogRecord["search_engine"][0]["title"],
description: blogRecord["search_engine"][0]["description"],
allowCrawlers: blogRecord["search_engine"][0]["allow_crawler"],
canonical: blogRecord["search_engine"][0]["canonical"],
priority: blogRecord["search_engine"][0]["priority"],
thumbnail: {
url: `${settings.website.domainName}${thumbnail.src}`,
width: blogThumbnailImage.width,
height: blogThumbnailImage.height
}
},
tags: []
};
blogRecord["tags"].forEach((tagRecord: any) => {
blog["tags"].push({
text: tagRecord["Tags_id"]["text"],
code: tagRecord["Tags_id"]["code"],
color: tagRecord["Tags_id"]["color"]
});
dates.push(tagRecord["Tags_id"]["date_created"]);
dates.push(tagRecord["Tags_id"]["date_updated"]);
});
if (dates.filter(e => e !== null).length === 0) {
blog.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
blog.lastModified = new Date(sortedDates[0]);
}
return blog;
}
export async function getLastBlogs(amount: number): Promise<BlogPost[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getLastBlogPosts), {
date: formatDate(new Date(), "%Y-%M-%D"),
amount: amount
});
let blogs: BlogPost[] = [];
result["Blogs"].forEach((blogRecord: any) => {
let dates: string[] = [
blogRecord["date_created"],
blogRecord["date_updated"],
blogRecord["search_engine"][0]["date_created"],
blogRecord["search_engine"][0]["date_updated"],
blogRecord["search_engine"][0]["thumbnail"]["created_on"]
];
const blogThumbnailImage =
getImageSize(blogRecord["search_engine"][0]["thumbnail"]["width"], blogRecord["search_engine"][0]["thumbnail"]["height"], 0.756);
const blog: BlogPost = {
exists: true,
type: "BlogPost",
id: blogRecord["id"],
lastModified: new Date(),
title: blogRecord["title"],
content: blogRecord["content"],
date: blogRecord["date"],
url: blogRecord["url"],
thumbnail: {
url: blogRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: blogRecord["search_engine"][0]["thumbnail"]["width"],
height: blogRecord["search_engine"][0]["thumbnail"]["height"]
},
searchEngine: {
title: blogRecord["search_engine"][0]["title"],
description: blogRecord["search_engine"][0]["description"],
allowCrawlers: blogRecord["search_engine"][0]["allow_crawler"],
canonical: blogRecord["search_engine"][0]["canonical"],
priority: blogRecord["search_engine"][0]["priority"],
thumbnail: {
url: blogRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: blogThumbnailImage.width,
height: blogThumbnailImage.height
}
},
tags: []
};
blogRecord["tags"].forEach((tagRecord: any) => {
blog["tags"].push({
text: tagRecord["Tags_id"]["text"],
code: tagRecord["Tags_id"]["code"],
color: tagRecord["Tags_id"]["color"]
});
dates.push(tagRecord["Tags_id"]["date_created"]);
dates.push(tagRecord["Tags_id"]["date_updated"]);
});
if (dates.filter(e => e !== null).length === 0) {
blog.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
blog.lastModified = new Date(sortedDates[0]);
}
blogs.push(blog);
});
return blogs;
}
export async function getAllPaginatedBlogs(settings: GlobalSettings, page: number): Promise<BlogPost[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getPaginatedBlogs), {
date: formatDate(new Date(), "%Y-%M-%D"),
limit: 8,
pageNumber: page
});
let blogs: BlogPost[] = [];
result["Blogs"].forEach((blogRecord: any) => {
let dates: string[] = [
settings.blog.lastModified.toISOString(),
settings.website.lastModified.toISOString(),
blogRecord["date_created"],
blogRecord["date_updated"],
blogRecord["search_engine"][0]["date_created"],
blogRecord["search_engine"][0]["date_updated"],
blogRecord["search_engine"][0]["thumbnail"]["created_on"]
];
const blogThumbnailImage =
getImageSize(blogRecord["search_engine"][0]["thumbnail"]["width"], blogRecord["search_engine"][0]["thumbnail"]["height"], 0.756);
const blog: BlogPost = {
exists: true,
type: "BlogPost",
id: blogRecord["id"],
lastModified: new Date(),
title: blogRecord["title"],
content: blogRecord["content"],
date: blogRecord["date"],
url: blogRecord["url"],
thumbnail: {
url: blogRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: blogRecord["search_engine"][0]["thumbnail"]["width"],
height: blogRecord["search_engine"][0]["thumbnail"]["height"]
},
searchEngine: {
title: blogRecord["search_engine"][0]["title"],
description: blogRecord["search_engine"][0]["description"],
allowCrawlers: blogRecord["search_engine"][0]["allow_crawler"],
canonical: blogRecord["search_engine"][0]["canonical"],
priority: blogRecord["search_engine"][0]["priority"],
thumbnail: {
url: blogRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: blogThumbnailImage.width,
height: blogThumbnailImage.height
}
},
tags: []
};
blogRecord["tags"].forEach((tagRecord: any) => {
blog["tags"].push({
text: tagRecord["Tags_id"]["text"],
code: tagRecord["Tags_id"]["code"],
color: tagRecord["Tags_id"]["color"]
});
dates.push(tagRecord["Tags_id"]["date_created"]);
dates.push(tagRecord["Tags_id"]["date_updated"]);
});
if (dates.filter(e => e !== null).length === 0) {
blog.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
blog.lastModified = new Date(sortedDates[0]);
}
blogs.push(blog);
});
return blogs;
}

View File

@@ -0,0 +1,110 @@
import { createDirectusConnection } from "@/lib/directus";
import { print } from 'graphql';
import type { Footer, FooterColumn, FooterSecondaryLink, FooterSocial } from "@/types/footers/footer";
import getFooterQuery from '@/graphql/footer/getFooter.graphql';
import { getImageUrl } from "@/lib/images";
export async function getFooter(): Promise<Footer> {
const client = await createDirectusConnection();
const result = await client.query(print(getFooterQuery));
const footerRecord = result['Footer'];
let dates: string[] = [
footerRecord['date_created'],
footerRecord['date_updated']
];
let footer: Footer = {
id: footerRecord['id'],
title: footerRecord['title'],
logo: {
url: getImageUrl(footerRecord['logo']['filename_disk']),
width: footerRecord['logo']['width'],
height: footerRecord['logo']['height']
},
copyright: footerRecord['copyright'],
columns: [],
socials: null,
secondaryLinks: null,
lastModified: new Date()
};
if (footerRecord['columns'] !== null) {
footerRecord['columns'].forEach((footerColumn: any) => {
const column: FooterColumn = {
id: footerColumn['id'],
title: footerColumn['title'],
links: []
};
footerColumn['links'].forEach((columnLink: any) => {
column.links.push({
id: columnLink['id'],
text: columnLink['text'],
url: columnLink['url']
});
dates.push(columnLink['date_created']);
dates.push(columnLink['date_updated']);
});
footer.columns.push(column);
dates.push(footerColumn['date_created']);
dates.push(footerColumn['date_updated']);
});
}
if (footerRecord['socials'] !== null) {
let socials: FooterSocial[] = [];
footerRecord['socials'].forEach((footerSocial: any) => {
socials.push({
id: footerSocial['id'],
name: footerSocial['name'],
url: footerSocial['url'],
icon: {
url: getImageUrl(footerSocial['icon']['filename_disk']),
width: footerSocial['icon']['width'],
height: footerSocial['icon']['height']
}
});
dates.push(footerSocial['date_created']);
dates.push(footerSocial['date_updated']);
});
footer.socials = socials;
}
if (footerRecord['secondary_links'] !== null) {
let secondaryLinks: FooterSecondaryLink[] = [];
footerRecord['secondary_links'].forEach((footerSecondaryLink: any) => {
secondaryLinks.push({
id: footerSecondaryLink['id'],
text: footerSecondaryLink['text'],
url: footerSecondaryLink['url']
});
dates.push(footerSecondaryLink['date_created']);
dates.push(footerSecondaryLink['date_updated']);
});
footer.secondaryLinks = secondaryLinks;
}
if (dates.filter(e => e !== null).length === 0) {
footer.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
footer.lastModified = new Date(sortedDates[0]);
}
return footer;
}

View File

@@ -0,0 +1,39 @@
import { createDirectusConnection } from "@/lib/directus";
import { print } from "graphql";
import getMenuQuery from "@/graphql/menu/getMenu.graphql";
export async function getMenu(): Promise<Menu> {
const client = await createDirectusConnection();
const result = await client.query(print(getMenuQuery));
const menuRecord = result['Menu'];
let menu: Menu = {
id: menuRecord['id'],
items: []
};
menuRecord['items'].forEach((menuItem: any) => {
if (menuItem['collection'] === "Menu_Column") {
let menuColumnItem: MenuColumn = {
id: menuItem['item']['id'],
type: "Column",
title: menuItem['item']['title'],
links: []
};
menuItem['item']['links'].forEach((menuItemLink: any) => {
menuColumnItem.links.push({
id: menuItemLink['id'],
type: "Link",
text: menuItemLink['text'],
url: menuItemLink['url']
});
});
menu.items.push(menuColumnItem);
}
});
return menu;
}

View File

@@ -0,0 +1,377 @@
import { createDirectusConnection } from "@/lib/directus";
import { print } from 'graphql';
import { formatDate } from "@/lib/dates";
import getAllPages from "@/graphql/pages/getAllPages.graphql";
import getPage from "@/graphql/pages/getPage.graphql";
import { getImageSize, getImageUrl } from "@/lib/images";
export function dataToPage(pageRecord: any): WebPage {
let dates: string[] = [
pageRecord["date_created"],
pageRecord["date_updated"],
pageRecord["search_engine"][0]["date_created"],
pageRecord["search_engine"][0]["date_updated"]
];
const searchEngine = pageRecord["search_engine"][0];
let components: WebpageComponent[] = [];
pageRecord["components"].forEach((componentRecord: any) => {
const component = componentRecord["item"];
switch (componentRecord["item"]["__typename"]) {
case "Hero":
const resizedHeroImage =
getImageSize(component["background_image"]["width"], component["background_image"]["height"], 2.5);
let heroComponent: HeroComponent = {
component: "Hero",
id: component["hero_id"],
title: component["hero_title"],
text: component["hero_text"],
backgroundImage: {
url: getImageUrl(component["background_image"]["filename_disk"]),
width: resizedHeroImage.width,
height: resizedHeroImage.height
}
};
components.push(heroComponent);
dates.push(component["hero_created"]);
dates.push(component["hero_updated"]);
dates.push(component["background_image"]["created_on"]);
break;
case "Text_With_Side_Image":
const resizedTextWithSideImage =
getImageSize(component["image"]["width"], component["image"]["height"], 1.5);
let textWithImageComponent: TextWithImageComponent = {
component: "TextWithImage",
id: component["twsi_id"],
title: component["twsi_title"],
text: component["twsi_text"],
imageSide: component["twsi_image_side"],
imageSize: component["twsi_image_size"],
image: {
url: getImageUrl(component["image"]["filename_disk"]),
width: resizedTextWithSideImage.width,
height: resizedTextWithSideImage.height
}
};
components.push(textWithImageComponent);
dates.push(component["twsi_created"]);
dates.push(component["twsi_updated"]);
dates.push(component["image"]["created_on"]);
break;
case "Wall_Of_Text":
let wallOfTextComponent: WallOfTextComponent = {
component: "WallOfText",
id: component["wot_id"],
title: component["wot_title"],
text: component["wot_text"]
};
components.push(wallOfTextComponent);
dates.push(component["wot_created"]);
dates.push(component["wot_updated"]);
break;
case "Frequently_Asked_Questions":
let faqComponent: FrequentlyAskedQuestionsComponent = {
component: "FrequentlyAskedQuestions",
id: component["faq_id"],
title: component["faq_title"],
text: component["faq_text"],
questions: []
};
component["questions"].forEach((faqQuestionRecord: any) => {
faqComponent.questions.push({
id: faqQuestionRecord["id"],
question: faqQuestionRecord["question"],
answer: faqQuestionRecord["answer"]
});
dates.push(faqQuestionRecord["date_created"]);
dates.push(faqQuestionRecord["date_updated"]);
});
components.push(faqComponent);
dates.push(component["faq_created"]);
dates.push(component["faq_updated"]);
break;
case "Upcoming_Events":
let upcomingEventsComponent: UpcomingEventsComponent = {
component: "UpcomingEvents",
id: component["ue_id"],
title: component["ue_title"],
text: component["ue_text"],
events: []
};
component["events"].forEach((eventRecord: any) => {
const resizedThumbnailImage =
getImageSize(eventRecord["thumbnail"]["width"], eventRecord["thumbnail"]["height"], 1);
upcomingEventsComponent.events.push({
id: eventRecord["id"],
title: eventRecord["title"],
description: eventRecord["description"],
location: eventRecord["location"],
mapLocation: [
eventRecord["map_location"]["coordinates"][1],
eventRecord["map_location"]["coordinates"][0]
],
thumbnail: {
url: getImageUrl(eventRecord["thumbnail"]["filename_disk"]),
width: resizedThumbnailImage.width,
height: resizedThumbnailImage.height
},
startDate: eventRecord["start_date"],
endDate: eventRecord["end_date"]
});
dates.push(eventRecord["date_created"]);
dates.push(eventRecord["date_updated"]);
});
components.push(upcomingEventsComponent);
dates.push(component["ue_created"]);
dates.push(component["ue_updated"]);
break;
case "Equipment_Table":
let equipmentTableComponent: EquipmentTableComponent = {
component: "EquipmentTable",
id: component["et_id"],
title: component["et_title"],
text: component["et_text"],
items: []
};
component["items"].forEach((itemRecord: any) => {
equipmentTableComponent.items.push({
id: itemRecord["id"],
title: itemRecord["title"],
text: itemRecord["text"],
icon: {
url: getImageUrl(itemRecord["icon"]["filename_disk"]),
width: itemRecord["icon"]["width"],
height: itemRecord["icon"]["height"]
}
});
dates.push(itemRecord["date_created"]);
dates.push(itemRecord["date_updated"]);
dates.push(itemRecord["icon"]["created_on"]);
});
components.push(equipmentTableComponent);
dates.push(component["et_created"]);
dates.push(component["et_updated"]);
break;
case "Review_List":
let reviewsComponent: ReviewListComponent = {
component: "Reviews",
id: component["rl_id"],
title: component["rl_title"],
text: component["rl_text"],
reviews: []
};
component["reviews"].forEach((reviewRecord: any) => {
const reviewThumbnailImage =
getImageSize(reviewRecord["thumbnail"]["width"], reviewRecord["thumbnail"]["height"], 1);
reviewsComponent.reviews.push({
id: reviewRecord["id"],
name: reviewRecord["name"],
review: reviewRecord["review"],
stars: reviewRecord["stars"],
date: reviewRecord["date"],
thumbnail: {
url: getImageUrl(reviewRecord["thumbnail"]["filename_disk"]),
width: reviewThumbnailImage.width,
height: reviewThumbnailImage.height
}
});
dates.push(reviewRecord["date_created"]);
dates.push(reviewRecord["date_updated"]);
dates.push(reviewRecord["thumbnail"]["created_on"]);
});
components.push(reviewsComponent);
dates.push(component["rl_created"]);
dates.push(component["rl_updated"]);
break;
case "Contact":
let contactComponent: ContactComponent = {
component: "Contact",
id: component["c_id"],
title: component["c_title"],
text: component["c_text"],
methods: []
};
component["methods"].forEach((contactMethodRecord: any) => {
contactComponent.methods.push({
id: contactMethodRecord["id"],
title: contactMethodRecord["title"],
url: contactMethodRecord["url"],
color: contactMethodRecord["color"],
icon: {
url: getImageUrl(contactMethodRecord["icon"]["filename_disk"]),
width: contactMethodRecord["icon"]["width"],
height: contactMethodRecord["icon"]["height"]
}
});
dates.push(contactMethodRecord["date_created"]);
dates.push(contactMethodRecord["date_updated"]);
dates.push(contactMethodRecord["icon"]["created_on"]);
});
components.push(contactComponent);
dates.push(component["c_created"]);
dates.push(component["c_updated"]);
break;
case "Last_Blogs":
let lastBlogsComponent: LastBlogsComponent = {
component: "LastBlogs",
id: component["lb_id"],
title: component["lb_title"],
readMoreButtonText: component["lb_read_more_button_text"],
amount: component["lb_amount"]
};
components.push(lastBlogsComponent);
dates.push(component["lb_created"]);
dates.push(component["lb_updated"]);
break;
case "Last_Projects":
let lastProjectsComponent: LastProjectsComponent = {
component: "LastProjects",
id: component["lp_id"],
title: component["lp_title"],
readMoreButtonText: component["lp_read_more_button_text"],
amount: component["lp_amount"]
};
components.push(lastProjectsComponent);
dates.push(component["lp_created"]);
dates.push(component["lp_updated"]);
break;
case "Last_Galleries":
let lastGalleriesComponent: LastGalleriesComponent = {
component: "LastGalleries",
id: component["lg_id"],
title: component["lg_title"],
readMoreButtonText: component["lg_read_more_button_text"],
amount: component["lg_amount"]
};
components.push(lastGalleriesComponent);
dates.push(component["lg_created"]);
dates.push(component["lg_updated"]);
break;
default:
break;
}
});
let lastModified: Date;
if (dates.filter(e => e !== null).length === 0) {
lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
lastModified = new Date(sortedDates[0]);
}
const thumbnailImage =
getImageSize(searchEngine["thumbnail"]["width"], searchEngine["thumbnail"]["width"], 0.756);
let page: WebPage = {
type: "Webpage",
exists: true,
id: pageRecord["id"],
lastModified: lastModified,
url: pageRecord["url"],
searchEngine: {
title: searchEngine["title"],
description: searchEngine["description"],
canonical: searchEngine["canonical"],
allowCrawlers: searchEngine["allow_crawler"],
priority: searchEngine["priority"],
thumbnail: {
url: getImageUrl(searchEngine["thumbnail"]["filename_disk"]),
height: thumbnailImage.height,
width: thumbnailImage.width
}
},
components: components
}
return page;
}
export async function getAllWebpages(): Promise<WebPage[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getAllPages), {
date: formatDate(new Date(), "%Y-%M-%D")
});
let pages: WebPage[] = [];
result["Pages"].forEach((pageRecord: any) => {
pages.push(dataToPage(pageRecord));
});
return pages;
}
export async function getWebpage(route: string): Promise<WebPage | null> {
const client = await createDirectusConnection();
const result = await client.query(print(getPage), {
date: formatDate(new Date(), "%Y-%M-%D"),
route: route
});
if (result["Pages"].length === 0) {
return {
type: "Webpage",
exists: false
};
}
const page = dataToPage(result["Pages"][0]);
if (!page.exists) {
return {
type: "Webpage",
exists: false
};
}
return {
...page,
type: "Webpage",
exists: true
}
}

View File

@@ -0,0 +1,347 @@
import { createDirectusConnection } from "@/lib/directus";
import { print } from "graphql";
import getAlbums from '@/graphql/photos/getAlbums.graphql';
import getAlbumItem from '@/graphql/photos/getAlbum.graphql';
import getLastAlbumsQuery from '@/graphql/photos/getLastAlbums.graphql';
import getCategoryAlbumQuery from '@/graphql/photos/getCategoryAlbum.graphql';
import { formatDate } from "@/lib/dates";
import { getImageSize } from "@/lib/images";
export async function getAllAlbums(settings: GlobalSettings): Promise<PhotoAlbum[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getAlbums), {
date: formatDate(new Date(), "%Y-%M-%D")
});
let albums: PhotoAlbum[] = [];
result["Photo_Albums"].forEach((albumRecord: any) => {
let dates: string[] = [
settings.website.lastModified.toISOString(),
settings.photo.lastModified.toISOString(),
albumRecord["date_created"],
albumRecord["date_updated"],
albumRecord["thumbnail"]["created_on"],
];
const categoryThumbnailImage =
getImageSize(albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["width"], albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["height"], 1.5);
const thumbnailImage =
getImageSize(albumRecord["thumbnail"]["width"], albumRecord["thumbnail"]["height"], 0.756);
const album: PhotoAlbum = {
exists: true,
type: "PhotoAlbum",
id: albumRecord["id"],
title: albumRecord["title"],
description: albumRecord["description"],
url: albumRecord["url"],
startDate: albumRecord["start_date"],
endDate: albumRecord["end_date"],
location: albumRecord["location"],
category: {
id: albumRecord["category"][0]["Photo_Categories_id"]["id"],
title: albumRecord["category"][0]["Photo_Categories_id"]["title"],
url: albumRecord["category"][0]["Photo_Categories_id"]["url"],
thumbnail: {
url: albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["filename_disk"],
width: categoryThumbnailImage.width,
height: categoryThumbnailImage.height
}
},
thumbnail: {
url: albumRecord["thumbnail"]["filename_disk"],
width: thumbnailImage.width,
height: thumbnailImage.height
},
photos: [],
lastModified: new Date()
};
albumRecord["photos"].forEach((photoRecord: any) => {
album.photos.push({
id: photoRecord["id"],
photo: {
url: photoRecord["photo"]["filename_disk"],
width: photoRecord["photo"]["width"],
height: photoRecord["photo"]["height"]
},
text: photoRecord["text"]
});
dates.push(photoRecord["date_created"]);
dates.push(photoRecord["date_updated"]);
dates.push(photoRecord["photo"]["created_on"]);
});
if (dates.filter(e => e !== null).length === 0) {
album.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
album.lastModified = new Date(sortedDates[0]);
}
albums.push(album);
});
return albums;
}
export async function getAlbum(settings: GlobalSettings, route: string): Promise<PhotoAlbum> {
const client = await createDirectusConnection();
const result = await client.query(print(getAlbumItem), {
route: route
});
const albumRecord = result["Photo_Albums"][0];
let dates: string[] = [
settings.website.lastModified.toISOString(),
settings.photo.lastModified.toISOString(),
albumRecord["date_created"],
albumRecord["date_updated"],
albumRecord["thumbnail"]["created_on"],
];
const categoryThumbnailImage =
getImageSize(albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["width"], albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["height"], 1.5);
const thumbnailImage =
getImageSize(albumRecord["thumbnail"]["width"], albumRecord["thumbnail"]["height"], 0.756);
const album: PhotoAlbum = {
exists: true,
type: "PhotoAlbum",
id: albumRecord["id"],
title: albumRecord["title"],
description: albumRecord["description"],
url: albumRecord["url"],
startDate: albumRecord["start_date"],
endDate: albumRecord["end_date"],
location: albumRecord["location"],
category: {
id: albumRecord["category"][0]["Photo_Categories_id"]["id"],
title: albumRecord["category"][0]["Photo_Categories_id"]["title"],
url: albumRecord["category"][0]["Photo_Categories_id"]["url"],
thumbnail: {
url: albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["filename_disk"],
width: categoryThumbnailImage.width,
height: categoryThumbnailImage.height
}
},
thumbnail: {
url: albumRecord["thumbnail"]["filename_download"],
width: thumbnailImage.width,
height: thumbnailImage.height
},
photos: [],
lastModified: new Date()
};
albumRecord["photos"].forEach((photoRecord: any) => {
album.photos.push({
id: photoRecord["id"],
photo: {
url: photoRecord["photo"]["filename_disk"],
width: photoRecord["photo"]["width"],
height: photoRecord["photo"]["height"]
},
text: photoRecord["text"]
});
dates.push(photoRecord["date_created"]);
dates.push(photoRecord["date_updated"]);
dates.push(photoRecord["photo"]["created_on"]);
});
if (dates.filter(e => e !== null).length === 0) {
album.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
album.lastModified = new Date(sortedDates[0]);
}
return album;
}
export async function getLastAlbums(amount: number) {
const client = await createDirectusConnection();
const result = await client.query(print(getLastAlbumsQuery), {
date: formatDate(new Date(), "%Y-%M-%D"),
limit: amount
});
let albums: PhotoAlbum[] = [];
result["Photo_Albums"].forEach((albumRecord: any) => {
let dates: string[] = [
albumRecord["date_created"],
albumRecord["date_updated"],
albumRecord["thumbnail"]["created_on"],
];
const categoryThumbnailImage =
getImageSize(albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["width"], albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["height"], 1.5);
const thumbnailImage =
getImageSize(albumRecord["thumbnail"]["width"], albumRecord["thumbnail"]["height"], 0.756);
const album: PhotoAlbum = {
exists: true,
type: "PhotoAlbum",
id: albumRecord["id"],
title: albumRecord["title"],
description: albumRecord["description"],
url: albumRecord["url"],
startDate: albumRecord["start_date"],
endDate: albumRecord["end_date"],
location: albumRecord["location"],
category: {
id: albumRecord["category"][0]["Photo_Categories_id"]["id"],
title: albumRecord["category"][0]["Photo_Categories_id"]["title"],
url: albumRecord["category"][0]["Photo_Categories_id"]["url"],
thumbnail: {
url: albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["filename_disk"],
width: categoryThumbnailImage.width,
height: categoryThumbnailImage.height
}
},
thumbnail: {
url: albumRecord["thumbnail"]["filename_disk"],
width: thumbnailImage.width,
height: thumbnailImage.height
},
photos: [],
lastModified: new Date()
};
albumRecord["photos"].forEach((photoRecord: any) => {
album.photos.push({
id: photoRecord["id"],
photo: {
url: photoRecord["photo"]["filename_disk"],
width: photoRecord["photo"]["width"],
height: photoRecord["photo"]["height"]
},
text: photoRecord["text"]
});
dates.push(photoRecord["date_created"]);
dates.push(photoRecord["date_updated"]);
dates.push(photoRecord["photo"]["created_on"]);
});
if (dates.filter(e => e !== null).length === 0) {
album.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
album.lastModified = new Date(sortedDates[0]);
}
albums.push(album);
});
return albums;
}
export async function getCategoryAlbums(settings: GlobalSettings, route: string): Promise<PhotoAlbum[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getCategoryAlbumQuery), {
date: formatDate(new Date(), "%Y-%M-%D"),
categoryUrl: route
});
let albums: PhotoAlbum[] = [];
result["Photo_Albums"].forEach((albumRecord: any) => {
let dates: string[] = [
settings.website.lastModified.toISOString(),
settings.photo.lastModified.toISOString(),
albumRecord["date_created"],
albumRecord["date_updated"],
albumRecord["thumbnail"]["created_on"],
];
const categoryThumbnailImage =
getImageSize(albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["width"], albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["height"], 1.5);
const thumbnailImage =
getImageSize(albumRecord["thumbnail"]["width"], albumRecord["thumbnail"]["height"], 0.756);
const album: PhotoAlbum = {
exists: true,
type: "PhotoAlbum",
id: albumRecord["id"],
title: albumRecord["title"],
description: albumRecord["description"],
url: albumRecord["url"],
startDate: albumRecord["start_date"],
endDate: albumRecord["end_date"],
location: albumRecord["location"],
category: {
id: albumRecord["category"][0]["Photo_Categories_id"]["id"],
title: albumRecord["category"][0]["Photo_Categories_id"]["title"],
url: albumRecord["category"][0]["Photo_Categories_id"]["url"],
thumbnail: {
url: albumRecord["category"][0]["Photo_Categories_id"]["thumbnail"]["filename_disk"],
width: categoryThumbnailImage.width,
height: categoryThumbnailImage.height
}
},
thumbnail: {
url: albumRecord["thumbnail"]["filename_disk"],
width: thumbnailImage.width,
height: thumbnailImage.height
},
photos: [],
lastModified: new Date()
};
albumRecord["photos"].forEach((photoRecord: any) => {
const imageSize =
getImageSize(photoRecord["photo"]["width"], photoRecord["photo"]["height"], 0.8);
album.photos.push({
id: photoRecord["id"],
photo: {
url: photoRecord["photo"]["filename_disk"],
width: photoRecord["photo"]["width"],
height: photoRecord["photo"]["height"]
},
text: photoRecord["text"]
});
dates.push(photoRecord["date_created"]);
dates.push(photoRecord["date_updated"]);
dates.push(photoRecord["photo"]["created_on"]);
});
if (dates.filter(e => e !== null).length === 0) {
album.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
album.lastModified = new Date(sortedDates[0]);
}
albums.push(album);
});
return albums;
}

View File

@@ -0,0 +1,56 @@
import { createDirectusConnection } from "@/lib/directus";
import { print } from "graphql";
import getCategories from '@/graphql/photos/getCategories.graphql';
import getCategory from '@/graphql/photos/getCategory.graphql';
import { getImageSize } from "@/lib/images";
export async function getAllCategories(settings: GlobalSettings): Promise<PhotoAlbumCategory[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getCategories));
let categories: PhotoAlbumCategory[] = [];
result["Photo_Categories"].forEach((photoCategoryRecord: any) => {
const imageSize =
getImageSize(photoCategoryRecord["thumbnail"]["width"], photoCategoryRecord["thumbnail"]["height"], 1.5);
categories.push({
id: photoCategoryRecord["id"],
title: photoCategoryRecord["title"],
url: photoCategoryRecord["url"],
thumbnail: {
url: photoCategoryRecord["thumbnail"]["filename_disk"],
width: imageSize.width,
height: imageSize.height
}
});
});
return categories;
}
export async function getPhotoCategory(url: string): Promise<PhotoAlbumCategory> {
const client = await createDirectusConnection();
const result = await client.query(print(getCategory), {
url: url
});
const item = result["Photo_Categories"][0];
const imageSize =
getImageSize(item["thumbnail"]["width"], item["thumbnail"]["height"], 1.5);
let categories: PhotoAlbumCategory = {
id: item["id"],
title: item["title"],
url: item["url"],
thumbnail: {
url: item["thumbnail"]["filename_disk"],
width: imageSize.width,
height: imageSize.height
}
};
return categories;
}

View File

@@ -0,0 +1,45 @@
import { createDirectusConnection } from "@/lib/directus";
import { print } from "graphql";
import getPhotos from '@/graphql/photos/getPhotos.graphql';
import md5 from "md5";
export async function getPhotoFromHash(albumUrl: string, hash: string): Promise<PhotoAlbumItem | null> {
const client = await createDirectusConnection();
const result = await client.query(print(getPhotos), {
albumUrl: albumUrl
});
let object: PhotoAlbumItem | null = null;
result["Photo_Albums"][0]["photos"].forEach((photo: any) => {
/*
* I have decided to not put the getImageSize here, it can mess up the
* hashing, or anything else. It seems smarter to do this in the photo's and galleries.
*/
const hashObject = md5(JSON.stringify({
id: photo.id,
url: photo.photo.filename_disk,
width: photo.photo.width,
height: photo.photo.height
}));
if (hashObject.substring(hashObject.length - 10) === hash) {
object = {
id: photo.id,
text: photo.text,
photo: {
url: photo.photo.filename_disk,
width: photo.photo.width,
height: photo.photo.height
},
album: {
url: result["Photo_Albums"][0].url,
title: result["Photo_Albums"][0].title
}
}
}
});
return object;
}

View File

@@ -0,0 +1,301 @@
import { formatDate } from "@/lib/dates";
import { createDirectusConnection } from "@/lib/directus";
import { print } from "graphql";
import getProjects from '@/graphql/projects/getProjects.graphql';
import getProjectPost from '@/graphql/projects/getProject.graphql';
import getLastProjectsQuery from '@/graphql/projects/getLastProjects.graphql';
import { getImageSize } from "@/lib/images";
export async function getAllProjects(settings: GlobalSettings): Promise<ProjectPost[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getProjects), {
date: formatDate(new Date(), "%Y-%M-%D")
});
let projects: ProjectPost[] = [];
result["Projects"].forEach((projectRecord: any) => {
let dates: string[] = [
settings.project.lastModified.toISOString(),
settings.website.lastModified.toISOString(),
projectRecord["date_created"],
projectRecord["date_updated"],
projectRecord["search_engine"][0]["date_created"],
projectRecord["search_engine"][0]["date_updated"],
projectRecord["search_engine"][0]["thumbnail"]["created_on"]
];
const projectThumbnailImage =
getImageSize(projectRecord["search_engine"][0]["thumbnail"]["width"], projectRecord["search_engine"][0]["thumbnail"]["height"], 0.756)
const project: ProjectPost = {
exists: true,
type: "ProjectPost",
lastModified: new Date(),
id: projectRecord["id"],
title: projectRecord["title"],
content: projectRecord["content"],
date: projectRecord["date"],
url: projectRecord["url"],
searchEngine: {
title: projectRecord["search_engine"][0]["title"],
description: projectRecord["search_engine"][0]["description"],
allowCrawlers: projectRecord["search_engine"][0]["allow_crawler"],
canonical: projectRecord["search_engine"][0]["canonical"],
priority: projectRecord["search_engine"][0]["priority"],
thumbnail: {
url: projectRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: projectThumbnailImage.width,
height: projectThumbnailImage.height
}
},
tags: []
};
projectRecord["tags"].forEach((tagRecord: any) => {
project["tags"].push({
text: tagRecord["Tags_id"]["text"],
code: tagRecord["Tags_id"]["code"],
color: tagRecord["Tags_id"]["color"]
});
dates.push(tagRecord["Tags_id"]["date_created"]);
dates.push(tagRecord["Tags_id"]["date_updated"]);
});
if (dates.filter(e => e !== null).length === 0) {
project.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
project.lastModified = new Date(sortedDates[0]);
}
projects.push(project);
});
return projects;
}
export async function getProject(settings: GlobalSettings, route: string): Promise<ProjectPost> {
const client = await createDirectusConnection();
const result = await client.query(print(getProjectPost), {
route: route
});
const projectRecord = result["Projects"][0];
let dates: string[] = [
settings.project.lastModified.toISOString(),
settings.website.lastModified.toISOString(),
projectRecord["date_created"],
projectRecord["date_updated"],
projectRecord["search_engine"][0]["date_created"],
projectRecord["search_engine"][0]["date_updated"],
projectRecord["search_engine"][0]["thumbnail"]["created_on"]
];
const projectThumbnailImage =
getImageSize(projectRecord["search_engine"][0]["thumbnail"]["width"], projectRecord["search_engine"][0]["thumbnail"]["height"], 0.756)
const project: ProjectPost = {
type: "ProjectPost",
exists: true,
lastModified: new Date(),
id: projectRecord["id"],
title: projectRecord["title"],
content: projectRecord["content"],
date: projectRecord["date"],
url: projectRecord["url"],
searchEngine: {
title: projectRecord["search_engine"][0]["title"],
description: projectRecord["search_engine"][0]["description"],
allowCrawlers: projectRecord["search_engine"][0]["allow_crawler"],
canonical: projectRecord["search_engine"][0]["canonical"],
priority: projectRecord["search_engine"][0]["priority"],
thumbnail: {
url: projectRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: projectThumbnailImage.width,
height: projectThumbnailImage.height
}
},
tags: []
};
projectRecord["tags"].forEach((tagRecord: any) => {
project["tags"].push({
text: tagRecord["Tags_id"]["text"],
code: tagRecord["Tags_id"]["code"],
color: tagRecord["Tags_id"]["color"]
});
dates.push(tagRecord["Tags_id"]["date_created"]);
dates.push(tagRecord["Tags_id"]["date_updated"]);
});
if (dates.filter(e => e !== null).length === 0) {
project.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
project.lastModified = new Date(sortedDates[0]);
}
return project;
}
export async function getLastProjects(amount: number): Promise<ProjectPost[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getLastProjectsQuery), {
date: formatDate(new Date(), "%Y-%M-%D"),
amount: amount
});
let projects: ProjectPost[] = [];
result["Projects"].forEach((projectRecord: any) => {
let dates: string[] = [
projectRecord["date_created"],
projectRecord["date_updated"],
projectRecord["search_engine"][0]["date_created"],
projectRecord["search_engine"][0]["date_updated"],
projectRecord["search_engine"][0]["thumbnail"]["created_on"]
];
const projectThumbnailImage =
getImageSize(projectRecord["search_engine"][0]["thumbnail"]["width"], projectRecord["search_engine"][0]["thumbnail"]["height"], 0.756)
const project: ProjectPost = {
exists: true,
type: "ProjectPost",
lastModified: new Date(),
id: projectRecord["id"],
title: projectRecord["title"],
content: projectRecord["content"],
date: projectRecord["date"],
url: projectRecord["url"],
searchEngine: {
title: projectRecord["search_engine"][0]["title"],
description: projectRecord["search_engine"][0]["description"],
allowCrawlers: projectRecord["search_engine"][0]["allow_crawler"],
canonical: projectRecord["search_engine"][0]["canonical"],
priority: projectRecord["search_engine"][0]["priority"],
thumbnail: {
url: projectRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: projectThumbnailImage.width,
height: projectThumbnailImage.height
}
},
tags: []
};
projectRecord["tags"].forEach((tagRecord: any) => {
project["tags"].push({
text: tagRecord["Tags_id"]["text"],
code: tagRecord["Tags_id"]["code"],
color: tagRecord["Tags_id"]["color"]
});
dates.push(tagRecord["Tags_id"]["date_created"]);
dates.push(tagRecord["Tags_id"]["date_updated"]);
});
if (dates.filter(e => e !== null).length === 0) {
project.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
project.lastModified = new Date(sortedDates[0]);
}
projects.push(project);
});
return projects;
}
export async function getAllPaginatedProjects(settings: GlobalSettings, page: number): Promise<ProjectPost[]> {
const client = await createDirectusConnection();
const result = await client.query(print(getProjects), {
date: formatDate(new Date(), "%Y-%M-%D"),
limit: 6,
pageNumber: page
});
let projects: ProjectPost[] = [];
result["Projects"].forEach((projectRecord: any) => {
let dates: string[] = [
settings.project.lastModified.toISOString(),
settings.website.lastModified.toISOString(),
projectRecord["date_created"],
projectRecord["date_updated"],
projectRecord["search_engine"][0]["date_created"],
projectRecord["search_engine"][0]["date_updated"],
projectRecord["search_engine"][0]["thumbnail"]["created_on"]
];
const projectThumbnailImage =
getImageSize(projectRecord["search_engine"][0]["thumbnail"]["width"], projectRecord["search_engine"][0]["thumbnail"]["height"], 0.756)
const project: ProjectPost = {
exists: true,
type: "ProjectPost",
lastModified: new Date(),
id: projectRecord["id"],
title: projectRecord["title"],
content: projectRecord["content"],
date: projectRecord["date"],
url: projectRecord["url"],
searchEngine: {
title: projectRecord["search_engine"][0]["title"],
description: projectRecord["search_engine"][0]["description"],
allowCrawlers: projectRecord["search_engine"][0]["allow_crawler"],
canonical: projectRecord["search_engine"][0]["canonical"],
priority: projectRecord["search_engine"][0]["priority"],
thumbnail: {
url: projectRecord["search_engine"][0]["thumbnail"]["filename_disk"],
width: projectThumbnailImage.width,
height: projectThumbnailImage.height
}
},
tags: []
};
projectRecord["tags"].forEach((tagRecord: any) => {
project["tags"].push({
text: tagRecord["Tags_id"]["text"],
code: tagRecord["Tags_id"]["code"],
color: tagRecord["Tags_id"]["color"]
});
dates.push(tagRecord["Tags_id"]["date_created"]);
dates.push(tagRecord["Tags_id"]["date_updated"]);
});
if (dates.filter(e => e !== null).length === 0) {
project.lastModified = new Date();
}
else {
const sortedDates: string[] = dates.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
project.lastModified = new Date(sortedDates[0]);
}
projects.push(project);
});
return projects;
}

View File

@@ -0,0 +1,15 @@
import { createDirectusConnection } from "@/lib/directus";
import { print } from 'graphql';
import getRobotsQuery from '@/graphql/settings/robots.graphql';
export async function getRobotsSettings(): Promise<RobotsSettings> {
const client = await createDirectusConnection();
const result = await client.query(print(getRobotsQuery));
const robotsResult = result["Robots"];
return {
crawlers: robotsResult["crawlers"],
extraContent: robotsResult["extra_content"]
};
}

View File

@@ -0,0 +1,169 @@
import { print } from 'graphql';
import { createDirectusConnection } from "@/lib/directus";
import getSettingsQuery from '@/graphql/settings/settings.graphql';
export async function getSettings(): Promise<GlobalSettings> {
const client = await createDirectusConnection();
const result = await client.query(print(getSettingsQuery));
const websiteResults = result["Website_Settings"];
const websiteSettings: WebsiteSettings = {
domainName: websiteResults["domain_name"],
titleTemplate: websiteResults["title_template"],
applicationName: websiteResults["application_name"],
colors: {
primary: websiteResults["primary_color"],
secondary: websiteResults["secondary_color"]
},
author: {
name: websiteResults["author_name"],
url: websiteResults["author_url"]
},
owner: websiteResults["owner"],
designer: websiteResults["designer"],
developer: websiteResults["developer"],
copyright: websiteResults["copyright"],
twitter: {
id: websiteResults["twitter_id"],
handle: websiteResults["twitter_handle"]
},
lastModified: websiteResults["date_updated"] !== null ?
new Date(websiteResults["date_updated"]) :
new Date(websiteResults["date_created"])
};
const blogResults = result["Blog_Settings"];
const blogSettings: BlogSettings = {
enabled: blogResults["enabled"],
title: blogResults["title"],
subtext: blogResults["subtext"],
indexRouteTemplate: blogResults["index_route_template"],
blogRouteTemplate: blogResults["blog_route_template"],
lastModified: blogResults["date_updated"] !== null ?
new Date(blogResults["date_updated"]) :
new Date(blogResults["date_created"])
};
const projectResults = result["Project_Settings"];
const projectSettings: ProjectSettings = {
enabled: projectResults["enabled"],
title: projectResults["title"],
subtext: projectResults["subtext"],
indexRouteTemplate: projectResults["index_route_template"],
projectRouteTemplate: projectResults["project_route_template"],
lastModified: projectResults["date_updated"] !== null ?
new Date(projectResults["date_updated"]) :
new Date(projectResults["date_created"])
};
const photoResults = result["Photo_Settings"];
let photoResultsLastModifiedTimestamps: string[] = [
photoResults["date_created"],
photoResults["date_updated"],
photoResults["category_icons"]["date_created"],
photoResults["category_icons"]["date_updated"],
photoResults["category_icons"]["photos_icon"]["created_on"],
photoResults["category_icons"]["location_icon"]["created_on"],
photoResults["category_icons"]["date_icon"]["created_on"],
photoResults["photo_icons"]["date_created"],
photoResults["photo_icons"]["date_updated"],
photoResults["photo_icons"]["previous_icon"]["created_on"],
photoResults["photo_icons"]["next_icon"]["created_on"],
photoResults["photo_icons"]["close_icon"]["created_on"],
photoResults["photo_icons"]["download_icon"]["created_on"]
];
const photoResultsLastModified = photoResultsLastModifiedTimestamps.sort((a: string, b: string) => {
return new Date(b).getTime() - new Date(a).getTime();
});
const photoSettings: WebsitePhotoSettings = {
enabled: photoResults["enabled"],
categoryIndex: {
indexRouteTemplate: photoResults["categories_index_route_template_url"]
},
category: {
routeTemplate: photoResults["category_route_template_url"],
perPage: photoResults["albums_per_category_page"],
icons: {
photos: {
url: photoResults["category_icons"]["photos_icon"]["filename_download"],
height: photoResults["category_icons"]["photos_icon"]["height"],
width: photoResults["category_icons"]["photos_icon"]["width"]
},
location: {
url: photoResults["category_icons"]["location_icon"]["filename_download"],
height: photoResults["category_icons"]["location_icon"]["height"],
width: photoResults["category_icons"]["location_icon"]["width"]
},
date: {
url: photoResults["category_icons"]["date_icon"]["filename_download"],
height: photoResults["category_icons"]["date_icon"]["height"],
width: photoResults["category_icons"]["date_icon"]["width"]
}
}
},
album: {
routeTemplate: photoResults["album_route_template_url"],
perPage: photoResults["photos_per_album_page"]
},
photo: {
routeTemplate: photoResults["photo_route_template_url"],
icons: {
previous: {
url: photoResults["photo_icons"]["previous_icon"]["filename_download"],
height: photoResults["photo_icons"]["previous_icon"]["height"],
width: photoResults["photo_icons"]["previous_icon"]["width"]
},
next: {
url: photoResults["photo_icons"]["next_icon"]["filename_download"],
height: photoResults["photo_icons"]["next_icon"]["height"],
width: photoResults["photo_icons"]["next_icon"]["width"]
},
close: {
url: photoResults["photo_icons"]["close_icon"]["filename_download"],
height: photoResults["photo_icons"]["close_icon"]["height"],
width: photoResults["photo_icons"]["close_icon"]["width"]
},
download: {
url: photoResults["photo_icons"]["download_icon"]["filename_download"],
height: photoResults["photo_icons"]["download_icon"]["height"],
width: photoResults["photo_icons"]["download_icon"]["width"]
}
}
},
lastModified: new Date(photoResultsLastModified[0])
};
const sitemapResults = result["Sitemap_Settings"];
const sitemapSettings: SitemapSettings = {
perPage: sitemapResults["per_page"],
lastModified: sitemapResults["date_updated"] !== null ?
new Date(sitemapResults["date_updated"]) :
new Date(sitemapResults["date_created"])
};
const pluginResults = result["Plugin_Settings"];
const pluginSettings: PluginSettings = {
swetrix: {
id: pluginResults["swetrix_id"],
url: pluginResults["swetrix_url"]
},
lastModified: pluginResults["date_updated"] !== null ?
new Date(pluginResults["date_updated"]) :
new Date(pluginResults["date_created"])
}
if (pluginResults["swetrix_id"] === null && pluginResults["swetrix_url"] === null) {
pluginSettings.swetrix = null;
}
return {
website: websiteSettings,
blog: blogSettings,
project: projectSettings,
photo: photoSettings,
sitemap: sitemapSettings,
plugins: pluginSettings
}
}

8
astro/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
interface ImportMetaEnv {
readonly DIRECTUS_TOKEN: string;
readonly DIRECTUS_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

5
astro/src/graphql.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.graphql' {
import { DocumentNode } from 'graphql';
const Schema: DocumentNode;
export default Schema;
}

View File

@@ -0,0 +1,39 @@
query getAllBlogs($route: String!) {
Blogs(sort: ["-date", "-date_created"], filter: { status: { _eq: "published" }, url: { _eq: $route } }) {
id,
date_created,
date_updated,
status,
title,
url,
date,
content,
tags {
Tags_id {
id,
date_created,
date_updated,
text,
code,
color
}
},
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
}
}
}

View File

@@ -0,0 +1,39 @@
query getAllBlogs($date: String!) {
Blogs(sort: ["-date", "-date_created"], filter: { status: { _eq: "published" }, date: { _lte: $date } }) {
id,
date_created,
date_updated,
status,
title,
url,
date,
content,
tags {
Tags_id {
id,
date_created,
date_updated,
text,
code,
color
}
},
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
}
}
}

View File

@@ -0,0 +1,39 @@
query getLastBlogPosts($date: String!, $amount: Int!) {
Blogs(sort: ["-date", "-date_created"], filter: { status: { _eq: "published" }, date: { _lte: $date } }, limit: $amount) {
id,
date_created,
date_updated,
status,
title,
url,
date,
content,
tags {
Tags_id {
id,
date_created,
date_updated,
text,
code,
color
}
},
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
}
}
}

View File

@@ -0,0 +1,39 @@
query getPaginatedBlogs($date: String!, $pageNumber: Int!, $limit: Int!) {
Blogs(sort: ["-date", "-date_created"], filter: { status: { _eq: "published" }, date: { _lte: $date } }, limit: $limit, page: $pageNumber) {
id,
date_created,
date_updated,
status,
title,
url,
date,
content,
tags {
Tags_id {
id,
date_created,
date_updated,
text,
code,
color
}
},
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
}
}
}

View File

@@ -0,0 +1,50 @@
query getFooter {
Footer {
id,
date_created,
date_updated,
title,
logo {
id,
created_on,
filename_disk,
width,
height
},
copyright,
columns {
id,
date_created,
date_updated,
title,
links {
id,
date_created,
date_updated,
text,
url
}
},
socials {
id,
date_created,
date_updated,
name,
url,
icon {
id,
created_on,
filename_disk,
width,
height
}
},
secondary_links {
id,
date_created,
date_updated,
text,
url
}
}
}

View File

@@ -0,0 +1,35 @@
query getMenu {
Menu {
id,
date_created,
date_updated,
items {
id,
item {
...on Menu_Column {
__typename,
id,
date_created,
date_updated,
title,
links {
id,
date_created,
date_updated,
text,
url
}
}
...on Menu_Link {
__typename,
id,
date_created,
date_updated,
text,
url
}
}
}
}
}

View File

@@ -0,0 +1,224 @@
query getAllPages($date: String!) {
Pages(filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
status,
url,
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
},
components {
id,
__typename,
item {
__typename,
...on Hero {
hero_id: id,
hero_created: date_created,
hero_updated: date_updated,
hero_title: title,
hero_text: subtext,
background_image {
id,
created_on,
filename_disk,
width,
height
}
hero_image: background_image {
id,
created_on,
filename_disk,
width,
height
}
background_image {
id,
created_on,
filename_disk,
width,
height
}
}
...on Text_With_Side_Image {
twsi_id: id,
twsi_created: date_created,
twsi_updated: date_updated,
twsi_title: title,
twsi_text: text,
image {
id,
created_on,
filename_disk,
width,
height
},
twsi_image_side: image_side,
twsi_image_size: image_size
}
...on Wall_Of_Text {
wot_id: id,
wot_created: date_created,
wot_updated: date_updated,
wot_title: title,
wot_text: text
}
...on Frequently_Asked_Questions {
faq_id: id,
faq_created: date_created,
faq_updated: date_updated,
faq_title: title,
faq_text: text,
questions {
id,
date_created,
date_updated,
question,
answer
}
}
...on Upcoming_Events {
ue_id: id,
ue_created: date_created,
ue_updated: date_updated,
ue_title: title,
ue_text: text,
events(filter: { start_date: { _gte: $date } }) {
id,
date_created,
date_updated,
title,
description,
location,
map_location,
start_date,
end_date,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
}
...on Equipment_Table {
et_id: id,
et_created: date_created,
et_updated: date_updated,
et_title: title,
et_text: text,
items {
id,
date_created,
date_updated,
title,
text,
icon {
id,
created_on,
filename_disk,
width,
height
}
}
}
...on Review_List {
rl_id: id,
rl_created: date_created,
rl_updated: date_updated,
rl_title: title,
rl_text: text,
reviews(sort: ["date"], filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
name,
review,
date,
stars,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
}
...on Contact {
c_id: id,
c_created: date_created,
c_updated: date_updated,
c_title: title,
c_text: text,
methods {
id,
date_created,
date_updated,
title,
url,
icon {
id,
created_on,
filename_disk,
width,
height
},
color
}
}
...on Last_Blogs {
lb_id: id,
lb_created: date_created,
lb_updated: date_updated,
lb_title: title,
lb_read_more_button_text: read_more_button_text,
lb_amount: amount
}
...on Last_Projects {
lp_id: id,
lp_created: date_created,
lp_updated: date_updated,
lp_title: title,
lp_read_more_button_text: read_more_button_text,
lp_amount: amount
}
...on Last_Galleries {
lg_id: id,
lg_created: date_created,
lg_updated: date_updated,
lg_title: title,
lg_read_more_button_text: read_more_button_text,
lg_amount: amount
}
}
}
}
}

View File

@@ -0,0 +1,224 @@
query getAllPages($date: String!, $route: String!) {
Pages(filter: { status: { _eq: "published" }, url: { _eq: $route } }) {
id,
date_created,
date_updated,
status,
url,
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
},
components {
id,
__typename,
item {
__typename,
...on Hero {
hero_id: id,
hero_created: date_created,
hero_updated: date_updated,
hero_title: title,
hero_text: subtext,
background_image {
id,
created_on,
filename_disk,
width,
height
}
hero_image: background_image {
id,
created_on,
filename_disk,
width,
height
}
background_image {
id,
created_on,
filename_disk,
width,
height
}
}
...on Text_With_Side_Image {
twsi_id: id,
twsi_created: date_created,
twsi_updated: date_updated,
twsi_title: title,
twsi_text: text,
image {
id,
created_on,
filename_disk,
width,
height
},
twsi_image_side: image_side,
twsi_image_size: image_size
}
...on Wall_Of_Text {
wot_id: id,
wot_created: date_created,
wot_updated: date_updated,
wot_title: title,
wot_text: text
}
...on Frequently_Asked_Questions {
faq_id: id,
faq_created: date_created,
faq_updated: date_updated,
faq_title: title,
faq_text: text,
questions {
id,
date_created,
date_updated,
question,
answer
}
}
...on Upcoming_Events {
ue_id: id,
ue_created: date_created,
ue_updated: date_updated,
ue_title: title,
ue_text: text,
events(filter: { start_date: { _gte: $date } }) {
id,
date_created,
date_updated,
title,
description,
location,
map_location,
start_date,
end_date,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
}
...on Equipment_Table {
et_id: id,
et_created: date_created,
et_updated: date_updated,
et_title: title,
et_text: text,
items {
id,
date_created,
date_updated,
title,
text,
icon {
id,
created_on,
filename_disk,
width,
height
}
}
}
...on Review_List {
rl_id: id,
rl_created: date_created,
rl_updated: date_updated,
rl_title: title,
rl_text: text,
reviews(sort: ["date"], filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
name,
review,
date,
stars,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
}
...on Contact {
c_id: id,
c_created: date_created,
c_updated: date_updated,
c_title: title,
c_text: text,
methods {
id,
date_created,
date_updated,
title,
url,
icon {
id,
created_on,
filename_disk,
width,
height
},
color
}
}
...on Last_Blogs {
lb_id: id,
lb_created: date_created,
lb_updated: date_updated,
lb_title: title,
lb_read_more_button_text: read_more_button_text,
lb_amount: amount
}
...on Last_Projects {
lp_id: id,
lp_created: date_created,
lp_updated: date_updated,
lp_title: title,
lp_read_more_button_text: read_more_button_text,
lp_amount: amount
}
...on Last_Galleries {
lg_id: id,
lg_created: date_created,
lg_updated: date_updated,
lg_title: title,
lg_read_more_button_text: read_more_button_text,
lg_amount: amount
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
query getAllAlbums($route: String!) {
Photo_Albums(sort: ["-start_date", "-date_created"], filter: { status: { _eq: "published" }, url: { _eq: $route }, category: { Photo_Categories_id: { status: { _eq: "published" } } } }) {
id,
date_created,
date_updated,
title,
description,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
start_date,
end_date,
location,
category {
Photo_Categories_id {
id,
status,
date_created,
date_updated,
title,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
},
photos(filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
photo {
id,
created_on,
filename_disk,
width,
height
},
text,
sort
}
}
}

View File

@@ -0,0 +1,51 @@
query getAllAlbums($date: String!) {
Photo_Albums(sort: ["-start_date", "-date_created"], filter: { status: { _eq: "published" }, start_date: { _lte: $date }, category: { Photo_Categories_id: { status: { _eq: "published" } } } }) {
id,
date_created,
date_updated,
title,
description,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
start_date,
end_date,
location,
category {
Photo_Categories_id {
id,
status,
date_created,
date_updated,
title,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
},
photos(filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
photo {
id,
created_on,
filename_disk,
width,
height
},
text,
sort
}
}
}

View File

@@ -0,0 +1,17 @@
query getAllCategories {
Photo_Categories(filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
status,
title,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
}

View File

@@ -0,0 +1,17 @@
query getAllCategories($url: String!) {
Photo_Categories(filter: { status: { _eq: "published" }, url: { _eq: $url } }) {
id,
date_created,
date_updated,
status,
title,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
}

View File

@@ -0,0 +1,51 @@
query getCategoryAlbums($date: String!, $categoryUrl: String!) {
Photo_Albums(sort: ["-start_date", "-date_created"], filter: { status: { _eq: "published" }, start_date: { _lte: $date }, category: { Photo_Categories_id: { status: { _eq: "published" }, url: { _eq: $categoryUrl } } } }) {
id,
date_created,
date_updated,
title,
description,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
start_date,
end_date,
location,
category {
Photo_Categories_id {
id,
status,
date_created,
date_updated,
title,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
},
photos(filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
photo {
id,
created_on,
filename_disk,
width,
height
},
text,
sort
}
}
}

View File

@@ -0,0 +1,51 @@
query getLastAlbums($date: String!, $limit: Int!) {
Photo_Albums(sort: ["-start_date", "-date_created"], filter: { status: { _eq: "published" }, start_date: { _lte: $date }, category: { Photo_Categories_id: { status: { _eq: "published" } } } }, limit: $limit) {
id,
date_created,
date_updated,
title,
description,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
start_date,
end_date,
location,
category {
Photo_Categories_id {
id,
status,
date_created,
date_updated,
title,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
},
photos(filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
photo {
id,
created_on,
filename_disk,
width,
height
},
text,
sort
}
}
}

View File

@@ -0,0 +1,51 @@
query getPhotos($albumUrl: String!) {
Photo_Albums(sort: ["-start_date", "-date_created"], filter: { status: { _eq: "published" }, url: { _eq: $albumUrl }, category: { Photo_Categories_id: { status: { _eq: "published" } } } }) {
id,
date_created,
date_updated,
title,
description,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
start_date,
end_date,
location,
category {
Photo_Categories_id {
id,
status,
date_created,
date_updated,
title,
url,
thumbnail {
id,
created_on,
filename_disk,
width,
height
}
}
},
photos(filter: { status: { _eq: "published" } }) {
id,
date_created,
date_updated,
photo {
id,
created_on,
filename_disk,
width,
height
},
text,
sort
}
}
}

View File

@@ -0,0 +1,39 @@
query getLastProjects($date: String!, $amount: Int!) {
Projects(sort: ["-date", "-date_created"], filter: { status: { _eq: "published" }, date: { _lte: $date } }, limit: $amount) {
id,
date_created,
date_updated,
status,
title,
url,
date,
content,
tags {
Tags_id {
id,
date_created,
date_updated,
text,
code,
color
}
},
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
}
}
}

View File

@@ -0,0 +1,39 @@
query getAllProjects($date: String!, $pageNumber: Int!, $limit: Int!) {
Projects(sort: ["-date", "-date_created"], filter: { status: { _eq: "published" }, date: { _lte: $date } }, limit: $limit, page: $pageNumber) {
id,
date_created,
date_updated,
status,
title,
url,
date,
content,
tags {
Tags_id {
id,
date_created,
date_updated,
text,
code,
color
}
},
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
}
}
}

View File

@@ -0,0 +1,39 @@
query getAllProjects($route: String!) {
Projects(sort: ["-date", "-date_created"], filter: { status: { _eq: "published" }, url: { _eq: $route } }) {
id,
date_created,
date_updated,
status,
title,
url,
date,
content,
tags {
Tags_id {
id,
date_created,
date_updated,
text,
code,
color
}
},
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
}
}
}

View File

@@ -0,0 +1,39 @@
query getAllProjects($date: String!) {
Projects(sort: ["-date", "-date_created"], filter: { status: { _eq: "published" }, date: { _lte: $date } }) {
id,
date_created,
date_updated,
status,
title,
url,
date,
content,
tags {
Tags_id {
id,
date_created,
date_updated,
text,
code,
color
}
},
search_engine {
id,
date_created,
date_updated,
title,
description,
thumbnail {
id,
created_on,
filename_disk,
width,
height
},
canonical,
allow_crawler,
priority
}
}
}

View File

@@ -0,0 +1,9 @@
query Robots {
Robots {
id,
date_created,
date_updated,
crawlers,
extra_content
}
}

View File

@@ -0,0 +1,124 @@
query getAllSettings {
Website_Settings {
id,
date_created,
date_updated,
domain_name,
title_template,
application_name,
primary_color,
secondary_color,
author_name,
author_url,
designer,
developer,
owner,
copyright,
twitter_id,
twitter_handle
},
Blog_Settings {
id,
date_created,
date_updated,
enabled,
title,
subtext,
index_route_template,
blog_route_template
},
Project_Settings {
id,
date_created,
date_updated,
enabled,
title,
subtext,
index_route_template,
project_route_template
},
Photo_Settings {
id,
date_created,
date_updated,
enabled,
categories_index_route_template_url,
category_route_template_url,
albums_per_category_page,
category_icons {
id,
date_created,
date_updated,
photos_icon {
id,
created_on,
filename_download,
width,
height
},
location_icon {
id,
created_on,
filename_download,
width,
height
},
date_icon {
id,
created_on,
filename_download,
width,
height
}
},
album_route_template_url,
photos_per_album_page,
photo_route_template_url,
photo_icons {
id,
date_created,
date_updated,
previous_icon {
id,
created_on,
filename_download,
width,
height
},
next_icon {
id,
created_on,
filename_download,
width,
height
},
close_icon {
id,
created_on,
filename_download,
width,
height
},
download_icon {
id,
created_on,
filename_download,
width,
height
}
}
},
Sitemap_Settings {
id,
date_created,
date_updated,
per_page
},
Plugin_Settings {
id,
date_created,
date_updated,
swetrix_id,
swetrix_url
}
}

View File

@@ -0,0 +1,8 @@
---
interface Props {
width?: number;
height?: number;
}
---
<svg xmlns="http://www.w3.org/2000/svg" width={Astro.props.width ?? "24"} height={Astro.props.height ?? "24"} viewBox="0 0 512 512"><rect width="416" height="384" x="48" y="80" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" rx="48"/><circle cx="296" cy="232" r="24" fill="currentColor"/><circle cx="376" cy="232" r="24" fill="currentColor"/><circle cx="296" cy="312" r="24" fill="currentColor"/><circle cx="376" cy="312" r="24" fill="currentColor"/><circle cx="136" cy="312" r="24" fill="currentColor"/><circle cx="216" cy="312" r="24" fill="currentColor"/><circle cx="136" cy="392" r="24" fill="currentColor"/><circle cx="216" cy="392" r="24" fill="currentColor"/><circle cx="296" cy="392" r="24" fill="currentColor"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M128 48v32m256-32v32"/><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" d="M464 160H48"/></svg>

View File

@@ -0,0 +1,10 @@
---
interface Props {
width?: number;
height?: number;
}
---
<svg xmlns="http://www.w3.org/2000/svg" width={Astro.props.width ?? "24"} height={Astro.props.height ?? "24"} viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m4 15l8-8l8 8" />
</svg>

View File

@@ -0,0 +1,10 @@
---
interface Props {
width?: number;
height?: number;
}
---
<svg xmlns="http://www.w3.org/2000/svg" width={Astro.props.width ?? "24"} height={Astro.props.height ?? "24"} viewBox="0 0 24 24">
<path fill="currentColor" d="M6.4 19L5 17.6l5.6-5.6L5 6.4L6.4 5l5.6 5.6L17.6 5L19 6.4L13.4 12l5.6 5.6l-1.4 1.4l-5.6-5.6z" />
</svg>

View File

@@ -0,0 +1,10 @@
---
interface Props {
width?: number;
height?: number;
}
---
<svg xmlns="http://www.w3.org/2000/svg" width={Astro.props.width ?? "24"} height={Astro.props.height ?? "24"} viewBox="0 0 24 24">
<path fill="currentColor" d="m12 16l-5-5l1.4-1.45l2.6 2.6V4h2v8.15l2.6-2.6L17 11zm-6 4q-.825 0-1.412-.587T4 18v-3h2v3h12v-3h2v3q0 .825-.587 1.413T18 20z" />
</svg>

View File

@@ -0,0 +1,5 @@
export function CalendarIcon(props: { width?: number, height?: number }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={props.width ?? "24"} height={props.height ?? "24"} viewBox="0 0 512 512"><rect width="416" height="384" x="48" y="80" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" rx="48"/><circle cx="296" cy="232" r="24" fill="currentColor"/><circle cx="376" cy="232" r="24" fill="currentColor"/><circle cx="296" cy="312" r="24" fill="currentColor"/><circle cx="376" cy="312" r="24" fill="currentColor"/><circle cx="136" cy="312" r="24" fill="currentColor"/><circle cx="216" cy="312" r="24" fill="currentColor"/><circle cx="136" cy="392" r="24" fill="currentColor"/><circle cx="216" cy="392" r="24" fill="currentColor"/><circle cx="296" cy="392" r="24" fill="currentColor"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M128 48v32m256-32v32"/><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" d="M464 160H48"/></svg>
)
}

View File

@@ -0,0 +1,5 @@
export function LoadingSpinner(props: { width?: number, height?: number }) {
return (
<svg xmlns="http://www.w3.org/2000/svg" width={props.width ?? "24"} height={props.height ?? "24"} viewBox="0 0 24 24"><circle cx="12" cy="2" r="0" fill="currentColor"><animate attributeName="r" begin="0" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="currentColor" transform="rotate(45 12 12)"><animate attributeName="r" begin="0.125s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="currentColor" transform="rotate(90 12 12)"><animate attributeName="r" begin="0.25s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="currentColor" transform="rotate(135 12 12)"><animate attributeName="r" begin="0.375s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="currentColor" transform="rotate(180 12 12)"><animate attributeName="r" begin="0.5s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="currentColor" transform="rotate(225 12 12)"><animate attributeName="r" begin="0.625s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="currentColor" transform="rotate(270 12 12)"><animate attributeName="r" begin="0.75s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="currentColor" transform="rotate(315 12 12)"><animate attributeName="r" begin="0.875s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle></svg>
)
}

View File

@@ -0,0 +1,87 @@
---
import '@/styles/global.css';
import { getSettings } from "@/content/settings/settings";
import { getTextColor } from '@/lib/colors';
import Footer from '@/components/footer/Footer.astro';
interface Props {
settings: BlogLayoutProps;
}
const pageSettings = Astro.props.settings.searchEngine;
const settings = await getSettings();
const css = {
"--ptc": settings.website.colors.primary,
"--stc": settings.website.colors.secondary ?? settings.website.colors.primary,
"--ptt": getTextColor(settings.website.colors.primary),
"--stt": settings.website.colors.secondary
? getTextColor(settings.website.colors.secondary)
: getTextColor(settings.website.colors.primary)
};
---
<!DOCTYPE html>
<html>
<head>
<!-- High Priority Metadata -->
<meta charset="utf-8" />
<meta name="lanuage" content="en" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content={settings.website.colors.primary} />
<!-- High Priority Page Metadata -->
<title>{settings.website.titleTemplate.replaceAll("%T", pageSettings.title)}</title>
<!-- Medium Priority Metadata -->
<meta name="msapplication-TileColor" content={settings.website.colors.primary} />
<meta name="msapplication-TileImage" content="" />
<link rel="sitemap" href="/sitemap/index.xml" />
<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="RSS" />
<link rel="canonical" href={`${settings.website.domainName}/`} />
<meta name="robots" content="index,follow" />
<meta name="keywords" content={[].join(',')} />
<!-- Low Priority Page Metadata -->
<meta name="description" content={pageSettings.description} />
<meta property="og:type" content="article" />
<meta property="og:locale" content="en-GB" />
<meta property="og:title" content={settings.website.titleTemplate.replaceAll("%T", pageSettings.title)} />
<meta property="og:description" content={pageSettings.description} />
<meta property="og:image:url" content={pageSettings.thumbnail.url} />
<meta property="og:url" content={`${settings.website.domainName}${Astro.url.pathname}`} />
<meta property="og:site_name" content={settings.website.applicationName} />
<meta property="article:author" content={settings.website.author.name} />
<meta property="article:tags" content={[].join(',')} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={settings.website.titleTemplate.replaceAll("%T", pageSettings.title)} />
<meta name="twitter:description" content={pageSettings.description} />
<meta name="twitter:image" content={pageSettings.thumbnail.url} />
<meta name="twitter:url" content={`${settings.website.domainName}${Astro.url.pathname}`} />
<meta name="twitter:site" content={settings.website.twitter.handle} />
<meta name="twitter:creator" content={settings.website.twitter.handle} />
<meta name="pagename" content={pageSettings.title} />
<meta name="category" content="webpage" />
<!-- Low Priority Metadata -->
<meta name="copyright" content={settings.website.copyright} />
<meta name="author" content={`${settings.website.author.name}, ${settings.website.author.url}`} />
<meta name="designer" content={settings.website.designer} />
<meta name="owner" content={settings.website.owner} />
<meta name="developer" content={settings.website.developer} />
<meta name="application-name" content={settings.website.applicationName} />
<!-- Scripts and Style -->
</head>
<body style={ css } class="bg-[#fcfcfc] flex flex-col min-h-screen">
<slot class="grow" name="content" />
<Footer />
</body>
</html>

View File

@@ -0,0 +1,82 @@
---
import '@/styles/global.css';
import { getSettings } from "@/content/settings/settings";
import { getTextColor } from '@/lib/colors';
import Footer from '@/components/footer/Footer.astro';
interface Props {
settings: WebpageLayoutProps;
}
const pageSettings = Astro.props.settings.searchEngine;
const settings = await getSettings();
const css = {
"--ptc": settings.website.colors.primary,
"--stc": settings.website.colors.secondary ?? settings.website.colors.primary,
"--ptt": getTextColor(settings.website.colors.primary),
"--stt": settings.website.colors.secondary
? getTextColor(settings.website.colors.secondary)
: getTextColor(settings.website.colors.primary)
};
---
<!DOCTYPE html>
<html>
<head>
<!-- High Priority Metadata -->
<meta charset="utf-8" />
<meta name="lanuage" content="en" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content={settings.website.colors.primary} />
<!-- High Priority Page Metadata -->
<title>{settings.website.titleTemplate.replaceAll("%T", pageSettings.title)}</title>
<!-- Medium Priority Metadata -->
<meta name="msapplication-TileColor" content={settings.website.colors.primary} />
<meta name="msapplication-TileImage" content="" />
<link rel="sitemap" href="/sitemap/index.xml" />
<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="RSS" />
<link rel="canonical" href={`${settings.website.domainName}/`} />
<meta name="robots" content="index,follow" />
<!-- Low Priority Page Metadata -->
<meta name="description" content={pageSettings.description} />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en-GB" />
<meta property="og:title" content={settings.website.titleTemplate.replaceAll("%T", pageSettings.title)} />
<meta property="og:description" content={pageSettings.description} />
<meta property="og:image:url" content={pageSettings.thumbnail.url} />
<meta property="og:url" content={`${settings.website.domainName}${Astro.url.pathname}`} />
<meta property="og:site_name" content={settings.website.applicationName} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={settings.website.titleTemplate.replaceAll("%T", pageSettings.title)} />
<meta name="twitter:description" content={pageSettings.description} />
<meta name="twitter:image" content={pageSettings.thumbnail.url} />
<meta name="twitter:url" content={`${settings.website.domainName}${Astro.url.pathname}`} />
<meta name="twitter:site" content={settings.website.twitter.handle} />
<meta name="twitter:creator" content={settings.website.twitter.handle} />
<meta name="pagename" content={pageSettings.title} />
<meta name="category" content="webpage" />
<!-- Low Priority Metadata -->
<meta name="copyright" content={settings.website.copyright} />
<meta name="author" content={`${settings.website.author.name}, ${settings.website.author.url}`} />
<meta name="designer" content={settings.website.designer} />
<meta name="owner" content={settings.website.owner} />
<meta name="developer" content={settings.website.developer} />
<meta name="application-name" content={settings.website.applicationName} />
<!-- Scripts and Style -->
</head>
<body style={ css } class="bg-neutral-950 flex flex-col min-h-screen">
<slot class="grow" name="content" />
</body>
</html>

View File

@@ -0,0 +1,87 @@
---
import '@/styles/global.css';
import { getSettings } from "@/content/settings/settings";
import { getTextColor } from '@/lib/colors';
import Footer from '@/components/footer/Footer.astro';
interface Props {
settings: BlogLayoutProps;
}
const pageSettings = Astro.props.settings.searchEngine;
const settings = await getSettings();
const css = {
"--ptc": settings.website.colors.primary,
"--stc": settings.website.colors.secondary ?? settings.website.colors.primary,
"--ptt": getTextColor(settings.website.colors.primary),
"--stt": settings.website.colors.secondary
? getTextColor(settings.website.colors.secondary)
: getTextColor(settings.website.colors.primary)
};
---
<!DOCTYPE html>
<html>
<head>
<!-- High Priority Metadata -->
<meta charset="utf-8" />
<meta name="lanuage" content="en" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content={settings.website.colors.primary} />
<!-- High Priority Page Metadata -->
<title>{settings.website.titleTemplate.replaceAll("%T", pageSettings.title)}</title>
<!-- Medium Priority Metadata -->
<meta name="msapplication-TileColor" content={settings.website.colors.primary} />
<meta name="msapplication-TileImage" content="" />
<link rel="sitemap" href="/sitemap/index.xml" />
<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="RSS" />
<link rel="canonical" href={`${settings.website.domainName}/`} />
<meta name="robots" content="index,follow" />
<meta name="keywords" content={[].join(',')} />
<!-- Low Priority Page Metadata -->
<meta name="description" content={pageSettings.description} />
<meta property="og:type" content="article" />
<meta property="og:locale" content="en-GB" />
<meta property="og:title" content={settings.website.titleTemplate.replaceAll("%T", pageSettings.title)} />
<meta property="og:description" content={pageSettings.description} />
<meta property="og:image:url" content={pageSettings.thumbnail.url} />
<meta property="og:url" content={`${settings.website.domainName}${Astro.url.pathname}`} />
<meta property="og:site_name" content={settings.website.applicationName} />
<meta property="article:author" content={settings.website.author.name} />
<meta property="article:tags" content={[].join(',')} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={settings.website.titleTemplate.replaceAll("%T", pageSettings.title)} />
<meta name="twitter:description" content={pageSettings.description} />
<meta name="twitter:image" content={pageSettings.thumbnail.url} />
<meta name="twitter:url" content={`${settings.website.domainName}${Astro.url.pathname}`} />
<meta name="twitter:site" content={settings.website.twitter.handle} />
<meta name="twitter:creator" content={settings.website.twitter.handle} />
<meta name="pagename" content={pageSettings.title} />
<meta name="category" content="webpage" />
<!-- Low Priority Metadata -->
<meta name="copyright" content={settings.website.copyright} />
<meta name="author" content={`${settings.website.author.name}, ${settings.website.author.url}`} />
<meta name="designer" content={settings.website.designer} />
<meta name="owner" content={settings.website.owner} />
<meta name="developer" content={settings.website.developer} />
<meta name="application-name" content={settings.website.applicationName} />
<!-- Scripts and Style -->
</head>
<body style={ css } class="bg-[#fcfcfc] flex flex-col min-h-screen">
<slot class="grow" name="content" />
<Footer />
</body>
</html>

View File

@@ -0,0 +1,84 @@
---
import '@/styles/global.css';
import { getSettings } from "@/content/settings/settings";
import { getTextColor } from '@/lib/colors';
import Footer from '@/components/footer/Footer.astro';
interface Props {
settings: WebpageLayoutProps;
}
const pageSettings = Astro.props.settings.searchEngine;
const settings = await getSettings();
const css = {
"--ptc": settings.website.colors.primary,
"--stc": settings.website.colors.secondary ?? settings.website.colors.primary,
"--ptt": getTextColor(settings.website.colors.primary),
"--stt": settings.website.colors.secondary
? getTextColor(settings.website.colors.secondary)
: getTextColor(settings.website.colors.primary)
};
---
<!DOCTYPE html>
<html>
<head>
<!-- High Priority Metadata -->
<meta charset="utf-8" />
<meta name="lanuage" content="en" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width" />
<meta name="theme-color" content={settings.website.colors.primary} />
<!-- High Priority Page Metadata -->
<title>{settings.website.titleTemplate.replaceAll("%T", pageSettings.title)}</title>
<!-- Medium Priority Metadata -->
<meta name="msapplication-TileColor" content={settings.website.colors.primary} />
<meta name="msapplication-TileImage" content="" />
<link rel="sitemap" href="/sitemap/index.xml" />
<link rel="alternate" type="application/rss+xml" href="/rss.xml" title="RSS" />
<link rel="canonical" href={`${settings.website.domainName}/`} />
<meta name="robots" content="index,follow" />
<!-- Low Priority Page Metadata -->
<meta name="description" content={pageSettings.description} />
<meta property="og:type" content="website" />
<meta property="og:locale" content="en-GB" />
<meta property="og:title" content={settings.website.titleTemplate.replaceAll("%T", pageSettings.title)} />
<meta property="og:description" content={pageSettings.description} />
<meta property="og:image:url" content={pageSettings.thumbnail.url} />
<meta property="og:url" content={`${settings.website.domainName}${Astro.url.pathname}`} />
<meta property="og:site_name" content={settings.website.applicationName} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={settings.website.titleTemplate.replaceAll("%T", pageSettings.title)} />
<meta name="twitter:description" content={pageSettings.description} />
<meta name="twitter:image" content={pageSettings.thumbnail.url} />
<meta name="twitter:url" content={`${settings.website.domainName}${Astro.url.pathname}`} />
<meta name="twitter:site" content={settings.website.twitter.handle} />
<meta name="twitter:creator" content={settings.website.twitter.handle} />
<meta name="pagename" content={pageSettings.title} />
<meta name="category" content="webpage" />
<!-- Low Priority Metadata -->
<meta name="copyright" content={settings.website.copyright} />
<meta name="author" content={`${settings.website.author.name}, ${settings.website.author.url}`} />
<meta name="designer" content={settings.website.designer} />
<meta name="owner" content={settings.website.owner} />
<meta name="developer" content={settings.website.developer} />
<meta name="application-name" content={settings.website.applicationName} />
<!-- Scripts and Style -->
</head>
<body style={ css } class="bg-[#fcfcfc] flex flex-col min-h-screen">
<slot class="grow" name="content" />
<Footer />
</body>
</html>

23
astro/src/lib/colors.ts Normal file
View File

@@ -0,0 +1,23 @@
export function getTextColor(bgColor: string) {
// Remove # if present
const hex = bgColor.replace('#', '');
// Convert hex to RGB
const r = parseInt(hex.substring(0, 2), 16) / 255;
const g = parseInt(hex.substring(2, 4), 16) / 255;
const b = parseInt(hex.substring(4, 6), 16) / 255;
// Convert to linear RGB
const toLinear = (c: any) =>
c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
const R = toLinear(r);
const G = toLinear(g);
const B = toLinear(b);
// Calculate luminance
const luminance = 0.2126 * R + 0.7152 * G + 0.0722 * B;
// Return white for dark backgrounds, black for light backgrounds
return luminance > 0.179 ? '#000000' : '#fcfcfc';
}

6
astro/src/lib/dates.ts Normal file
View File

@@ -0,0 +1,6 @@
export function formatDate(date: Date, format: string) {
return format
.replaceAll("%Y", date.getFullYear().toString())
.replaceAll("%M", (date.getMonth() + 1).toString().padStart(2, '0'))
.replaceAll("%D", date.getDate().toString().padStart(2, '0'));
}

View File

@@ -0,0 +1,9 @@
import { createDirectus, graphql, staticToken } from "@directus/sdk";
export async function createDirectusConnection() {
const directus = await createDirectus(import.meta.env.DIRECTUS_URL)
.with(graphql())
.with(staticToken(import.meta.env.DIRECTUS_TOKEN));
return directus;
}

12
astro/src/lib/hash.ts Normal file
View File

@@ -0,0 +1,12 @@
import md5 from "md5";
export function getPhotoHash(photo: PhotoAlbumPhoto) {
const hash = md5(JSON.stringify({
id: photo.id,
url: photo.photo.url,
width: photo.photo.width,
height: photo.photo.height
}));
return hash.substring(hash.length - 10);
}

22
astro/src/lib/images.ts Normal file
View File

@@ -0,0 +1,22 @@
export function getImageUrl(url: string) {
return `${import.meta.env.DIRECTUS_URL}assets/${url}`;
}
export function getImageSize(width: number, height: number, targetMegapixels: number): ResizedImageResponse {
const originalPixels = width * height;
const targetPixels = targetMegapixels * 1000 * 1000;
if (originalPixels <= targetPixels) {
return {
width,
height
}
}
const scale = Math.sqrt(targetPixels / originalPixels);
return {
width: Math.round(scale * width),
height: Math.round(scale * height)
}
}

View File

@@ -0,0 +1,9 @@
import markdownit from "markdown-it";
import markdownitHighlightjs from "markdown-it-highlightjs";
export function markdownToHtml(markdown: string) {
const md = markdownit()
.use(markdownitHighlightjs);
return md.render(markdown);
}

261
astro/src/lib/pages.ts Normal file
View File

@@ -0,0 +1,261 @@
import { getBlog } from "@/content/blogs/blogs";
import { getWebpage } from "@/content/pages/pages";
import { getAlbum } from "@/content/photos/albums";
import { getAllCategories, getPhotoCategory } from "@/content/photos/categories";
import { getPhotoFromHash } from "@/content/photos/photos";
import { getProject } from "@/content/projects/projects";
import { getImageSize } from "./images";
import { getImage } from "astro:assets";
export async function getPage(settings: GlobalSettings, route: string): Promise<PageType | null> {
// Blog Index
if (regexToRoute({ template: settings.blog.indexRouteTemplate, allowPagination: true }).regex.test(route)) {
const { regex, keys } = regexToRoute({ template: settings.blog.indexRouteTemplate, allowPagination: true });
const match = route.match(regex);
const params: Record<string, string> = {};
if (!match) return null;
keys.forEach((key, i) => {
params[key] = match[i + 1];
});
return {
route: route,
pageType: "BlogIndex",
page: {
type: "BlogIndex",
exists: true,
pageNumber: params["page"] !== undefined ? Number(params["page"]) : 1
}
};
}
// Blog Post
else if (regexToRoute({ template: settings.blog.blogRouteTemplate, allowPagination: false }).regex.test(route)) {
const { regex, keys } = regexToRoute({ template: settings.blog.blogRouteTemplate, allowPagination: false });
const match = route.match(regex);
const params: Record<string, string> = {};
if (!match) return null;
keys.forEach((key, i) => {
params[key] = match[i + 1];
});
const blog = await getBlog(settings, `/${params["R"]}`);
return {
route: route,
pageType: "BlogPost",
page: blog
};
}
// Project Index
else if (regexToRoute({ template: settings.project.indexRouteTemplate, allowPagination: true }).regex.test(route)) {
const { regex, keys } = regexToRoute({ template: settings.project.indexRouteTemplate, allowPagination: true });
const match = route.match(regex);
const params: Record<string, string> = {};
if (!match) return null;
keys.forEach((key, i) => {
params[key] = match[i + 1];
});
return {
route: route,
pageType: "ProjectIndex",
page: {
type: "ProjectIndex",
exists: true,
pageNumber: params["page"] !== undefined ? Number(params["page"]) : 1
}
};
}
// Project Post
else if (regexToRoute({ template: settings.project.projectRouteTemplate, allowPagination: false }).regex.test(route)) {
const { regex, keys } = regexToRoute({ template: settings.project.projectRouteTemplate, allowPagination: false });
const match = route.match(regex);
const params: Record<string, string> = {};
if (!match) return null;
keys.forEach((key, i) => {
params[key] = match[i + 1];
});
const project = await getProject(settings, `/${params["R"]}`);
return {
route: route,
pageType: "ProjectPost",
page: project
};
}
// Photo Category Index
else if (regexToRoute({ template: settings.photo.categoryIndex.indexRouteTemplate, allowPagination: false }).regex.test(route)) {
const allCategories = await getAllCategories(settings);
const lastCategory = allCategories[0];
return {
route: route,
pageType: "PhotoCategoryIndex",
page: {
category: lastCategory,
type: "PhotoCategoryIndex",
exists: true
}
};
}
// Photo Category / Album List
else if (regexToRoute({ template: settings.photo.category.routeTemplate, allowPagination: true }).regex.test(route)) {
const { regex, keys } = regexToRoute({ template: settings.photo.category.routeTemplate, allowPagination: true });
const match = route.match(regex);
const params: Record<string, string> = {};
if (!match) return null;
keys.forEach((key, i) => {
params[key] = match[i + 1];
});
const category = await getPhotoCategory(`/${params["C"]}`);
return {
route: route,
pageType: "PhotoCategory",
page: {
type: "PhotoCategory",
exists: true,
category: category,
pageNumber: params["page"] !== undefined ? Number(params["page"]) : 1
}
};
}
// Photo Album
else if (regexToRoute({ template: settings.photo.album.routeTemplate, allowPagination: true }).regex.test(route)) {
const { regex, keys } = regexToRoute({ template: settings.photo.album.routeTemplate, allowPagination: true });
const match = route.match(regex);
const params: Record<string, string> = {};
if (!match) return null;
keys.forEach((key, i) => {
params[key] = match[i + 1];
});
const photoAlbum = await getAlbum(settings, `/${params["R"]}`);
return {
route: route,
pageType: "PhotoAlbum",
page: {
...photoAlbum,
pageNumber: params["page"] !== undefined ? Number(params['page']) : 1
}
};
}
// Photograph
else if (regexToRoute({ template: settings.photo.photo.routeTemplate, allowPagination: false }).regex.test(route)) {
const { regex, keys } = regexToRoute({ template: settings.photo.photo.routeTemplate, allowPagination: false });
const match = route.match(regex);
const params: Record<string, string> = {};
if (!match) return null;
keys.forEach((key, i) => {
params[key] = match[i + 1];
});
const photo = await getPhotoFromHash(`/${params["R"]}`, params["H"]);
if (photo === null) {}
return {
route: route,
pageType: "Photo",
page: {
type: "PhotoPage",
exists: true,
id: photo!.id,
photo: photo!.photo,
text: photo!.text,
album: photo!.album
}
};
}
// Regular webpage
else if (regexToRoute({ template: "/", allowPagination: false }).regex.test(route) ||
regexToRoute({ template: "/%R", allowPagination: false }).regex.test(route)) {
const webpageContent = await getWebpage(route);
if (webpageContent === null || !webpageContent.exists) {
return {
route: route,
pageType: "Webpage",
page: {
type: "Webpage",
exists: false
}
}
}
const resizedImage = getImageSize(webpageContent.searchEngine.thumbnail.width,
webpageContent.searchEngine.thumbnail.height, 0.756);
const thumbnail = await getImage({
src: webpageContent.searchEngine.thumbnail.url,
width: resizedImage.width,
height: resizedImage.height,
format: "jpeg"
});
return {
route: route,
pageType: "Webpage",
page: {
type: "Webpage",
exists: true,
id: webpageContent.id,
lastModified: webpageContent.lastModified,
url: webpageContent.url,
searchEngine: {
...webpageContent.searchEngine,
thumbnail: {
url: `${settings.website.domainName}${thumbnail.src}`,
width: resizedImage.width,
height: resizedImage.height
}
},
components: webpageContent.components
}
};
}
else {
return null;
}
}
function regexToRoute(template: PageRegexMatchProps) {
const keys: string[] = [];
let pattern = template.template
.replaceAll("%Y", () => { keys.push("Y"); return "(\\d{4})"; })
.replaceAll("%M", () => { keys.push("M"); return "(\\d{2})"; })
.replaceAll("%D", () => { keys.push("D"); return "(\\d{2})"; })
.replaceAll("%R", () => { keys.push("R"); return "([^/]+)"; })
.replaceAll("%C", () => { keys.push("C"); return "([^/]+)"; })
.replaceAll("%H", () => { keys.push("H"); return "([^/]+)"; })
.replace(/\/+/g, "/");
if (template.allowPagination) {
keys.push("page");
pattern += "(?:\\/(\\d+))?";
}
return {
regex: new RegExp(`^${pattern}$`),
keys
};
}

148
astro/src/lib/routing.ts Normal file
View File

@@ -0,0 +1,148 @@
import { getAllBlogs } from "@/content/blogs/blogs";
import { getAllWebpages } from "@/content/pages/pages";
import { getAllAlbums } from "@/content/photos/albums";
import { getAllProjects } from "@/content/projects/projects";
import { getPhotoHash } from "./hash";
import { getAllCategories } from "@/content/photos/categories";
export async function getAllRoutesList(settings: GlobalSettings): Promise<string[]> {
let routes: string[] = [];
const webpages = await getAllWebpages();
webpages.forEach((webpage) => {
if (webpage.exists) {
routes.push(webpage.url);
}
});
if (settings.blog.enabled) {
const blogs = await getAllBlogs(settings);
for (let i = 0; i < Math.ceil(blogs.length / 6); i++) {
if (i !== 0) {
routes.push(`${settings.blog.indexRouteTemplate}/${i + 1}`);
}
else {
routes.push(settings.blog.indexRouteTemplate);
}
}
blogs.forEach((blog) => {
routes.push(getBlogRoute(settings.blog, blog));
});
}
if (settings.project.enabled) {
const projects = await getAllProjects(settings);
for (let i = 0; i < Math.ceil(projects.length / 4); i++) {
if (i !== 0) {
routes.push(`${settings.project.indexRouteTemplate}/${i + 1}`);
}
else {
routes.push(settings.project.indexRouteTemplate);
}
}
projects.forEach((project) => {
routes.push(getProjectRoute(settings.project, project));
});
}
if (settings.photo.enabled) {
const categories = await getAllCategories(settings);
if (categories.length > 0) {
const galleries = await getAllAlbums(settings);
routes.push(settings.photo.categoryIndex.indexRouteTemplate);
categories.forEach((category) => {
let albums = galleries.filter(g => g.category.id === category.id);
const pages = Math.ceil(albums.length / settings.photo.category.perPage);
const categoryRoute = getCategoryRoute(settings.photo, category);
for (let i = 0; i < pages; i++) {
if (i !== 0) {
routes.push(`${categoryRoute}/${i + 1}`);
}
else {
routes.push(`${categoryRoute}`);
}
}
});
galleries.forEach((gallery) => {
const pages = Math.ceil(gallery.photos.length / settings.photo.album.perPage);
const galleryRoute = getAlbumRoute(settings.photo, gallery);
for (let i = 0; i < pages; i++) {
if (i !== 0) {
routes.push(`${galleryRoute}/${i + 1}`);
}
else {
routes.push(`${galleryRoute}`);
}
}
gallery.photos.forEach((photo) => {
routes.push(getPhotoRoute(settings.photo, gallery, photo));
});
});
}
}
return routes;
}
export function getBlogRoute(blogSettings: BlogSettings, blog: BlogPost) {
const date = new Date(blog.date);
return blogSettings.blogRouteTemplate
.replaceAll("%Y", date.getFullYear().toString())
.replaceAll("%M", (date.getMonth() + 1).toString().padStart(2, '0'))
.replaceAll("%D", date.getDate().toString().padStart(2, '0'))
.replaceAll("%R", blog.url)
.replace(/\/+/g, '/');
}
export function getProjectRoute(projectSettings: ProjectSettings, project: ProjectPost) {
const date = new Date(project.date);
return projectSettings.projectRouteTemplate
.replaceAll("%Y", date.getFullYear().toString())
.replaceAll("%M", (date.getMonth() + 1).toString().padStart(2, '0'))
.replaceAll("%D", date.getDate().toString().padStart(2, '0'))
.replaceAll("%R", project.url)
.replace(/\/+/g, '/');
}
export function getCategoryRoute(photoSettings: WebsitePhotoSettings, category: PhotoAlbumCategory) {
return photoSettings.category.routeTemplate
.replaceAll("%C", category.url)
.replace(/\/+/g, '/');
}
export function getAlbumRoute(photoSettings: WebsitePhotoSettings, album: PhotoAlbum) {
const date = new Date(album.startDate);
return photoSettings.album.routeTemplate
.replaceAll("%Y", date.getFullYear().toString())
.replaceAll("%M", (date.getMonth() + 1).toString().padStart(2, '0'))
.replaceAll("%D", date.getDate().toString().padStart(2, '0'))
.replaceAll("%C", album.category.url)
.replaceAll("%R", album.url)
.replace(/\/+/g, '/');
}
export function getPhotoRoute(photoSettings: WebsitePhotoSettings, album: PhotoAlbum, photo: PhotoAlbumPhoto) {
const date = new Date(album.startDate);
return photoSettings.photo.routeTemplate
.replaceAll("%Y", date.getFullYear().toString())
.replaceAll("%M", (date.getMonth() + 1).toString().padStart(2, '0'))
.replaceAll("%D", date.getDate().toString().padStart(2, '0'))
.replaceAll("%C", album.category.url)
.replaceAll("%R", album.url)
.replaceAll("%H", getPhotoHash(photo))
.replace(/\/+/g, '/');
}

View File

@@ -0,0 +1,183 @@
---
import { getAllRoutesList } from "@/lib/routing";
import { getPage } from "@/lib/pages";
import { getSettings } from "@/content/settings/settings"
import WebpageLayout from "@/layouts/WebpageLayout.astro";
import BlogLayout from "@/layouts/BlogLayout.astro";
import ProjectLayout from "@/layouts/ProjectLayout.astro";
import PhotoLayout from '@/layouts/PhotoLayout.astro';
import BlogIndex from "@/components/blogs/BlogIndex.astro";
import ProjectIndex from "@/components/projects/ProjectIndex.astro";
import Webpage from "@/components/webpage/Webpage.astro";
import BlogPost from "@/components/blogs/BlogPost.astro";
import ProjectPost from "@/components/projects/ProjectPost.astro";
import CategoryIndex from "@/components/photos/CategoryIndex.astro";
import Category from "@/components/photos/Category.astro";
import AlbumPage from "@/components/photos/Album.astro";
import { getImageUrl } from "@/lib/images";
import Photo from "@/components/photos/Photo.astro";
export async function getStaticPaths() {
const settings = await getSettings();
const pages = await getAllRoutesList(settings);
let routes: any[] = [];
pages.forEach((page) => {
routes.push({ params: { route: page } });
});
return routes;
}
const settings = await getSettings();
const pathName = Astro.url.pathname === "/" ? "/" : Astro.url.pathname.replace(/\/$/, "");
const page = await getPage(settings, pathName);
if (page === null || page.page === null || !page.page.exists) {
return new Response("Page not found.", {
status: 404,
statusText: "Not Found"
});
}
---
{ page.page.type === "Webpage" && page.page.exists && (
<WebpageLayout settings={{
searchEngine: page.page.searchEngine
}}>
<Fragment slot="content">
<Webpage webpage={page.page.components} />
</Fragment>
</WebpageLayout>
) }
{ page.page.type === "BlogIndex" && (
<WebpageLayout settings={{
searchEngine: {
title: "Blogs",
description: "",
allowCrawlers: true,
canonical: null,
priority: 65,
thumbnail: {
url: "",
width: 1200,
height: 630
}
}}}>
<Fragment slot="content">
<BlogIndex page={page.page} />
</Fragment>
</WebpageLayout>
) }
{ page.page.type === "BlogPost" && (
<BlogLayout settings={{
searchEngine: page.page.searchEngine,
tags: page.page.tags.map((tag) => tag.text)
}}>
<Fragment slot="content">
<BlogPost blog={page.page} />
</Fragment>
</BlogLayout>
) }
{ page.page.type === "ProjectIndex" && (
<WebpageLayout settings={{
searchEngine: {
title: "Projects",
description: "",
allowCrawlers: true,
canonical: null,
priority: 65,
thumbnail: {
url: "",
width: 1200,
height: 630
}
}}}>
<Fragment slot="content">
<ProjectIndex page={page.page} />
</Fragment>
</WebpageLayout>
) }
{ page.page.type === "ProjectPost" && (
<ProjectLayout settings={{
searchEngine: page.page.searchEngine,
tags: page.page.tags.map((tag) => tag.text)
}}>
<Fragment slot="content">
<ProjectPost project={page.page} />
</Fragment>
</ProjectLayout>
) }
{ page.pageType === "PhotoCategoryIndex" && (
<WebpageLayout settings={{
searchEngine: {
title: "Categories",
description: "See the photo categories on this page, where you can see the different types of photography I do.",
allowCrawlers: true,
canonical: null,
priority: 65,
thumbnail: page.page.category.thumbnail
}}}>
<Fragment slot="content">
<CategoryIndex />
</Fragment>
</WebpageLayout>
) }
{ page.pageType === "PhotoCategory" && (
<WebpageLayout settings={{
searchEngine: {
title: page.page.category.title,
description: `See the photos in the ${page.page.category.title.toLowerCase()} category.`,
allowCrawlers: true,
canonical: null,
priority: 65,
thumbnail: page.page.category.thumbnail
}}}>
<Fragment slot="content">
<Category category={page.page} />
</Fragment>
</WebpageLayout>
) }
{ page.pageType === "PhotoAlbum" && (
<WebpageLayout settings={{
searchEngine: {
title: page.page.title,
description: `See the photos in the ${page.page.category.title.toLowerCase()} category.`,
allowCrawlers: true,
canonical: null,
priority: 65,
thumbnail: page.page.category.thumbnail
}}}>
<Fragment slot="content">
<AlbumPage page={page.page} />
</Fragment>
</WebpageLayout>
) }
{ page.pageType === "Photo" && (
<PhotoLayout settings={{
searchEngine: {
title: page.page.album.title,
description: `See this photo from the album ${page.page.album.title}`,
allowCrawlers: true,
canonical: null,
priority: 65,
thumbnail: {
url: getImageUrl(page.page.photo.url),
width: page.page.photo.width,
height: page.page.photo.height
}
}}}>
<Fragment slot="content">
<Photo page={page.page} />
</Fragment>
</PhotoLayout>
) }

View File

@@ -0,0 +1,66 @@
import { getRobotsSettings } from "@/content/settings/robots";
import { getSettings } from "@/content/settings/settings";
import type { APIRoute } from "astro";
export const GET = (async () => {
const settings = await getSettings();
const robots = await getRobotsSettings();
let crawlers = [
{ id: 'google', name: 'Googlebot' },
{ id: 'bing', name: "Bingbot" },
{ id: "slurp", name: "Slurp" },
{ id: "duckduckgo", name: "DuckDuckBot" },
{ id: "baidu", name: "Baiduspider" },
{ id: "yandex", name: "YandexBot" },
{ id: "sogou", name: "Sogou web spider" },
{ id: "seznam", name: "SeznamBot" },
{ id: "qwantbot", name: "Qwantbot" },
{ id: "naverbot", name: "Naverbot" },
{ id: "coccocbot", name: "Coccocbot" },
{ id: "mojeekbot", name: "Mojeekbot" },
{ id: "ahrefs", name: "Ahrefsbot" },
{ id: "semrush", name: "SemrushBot" },
{ id: "mj12bot", name: "MJ12Bot" },
{ id: "dotbot", name: "DotBot" },
{ id: "petalbot", name: "PetalBot" },
{ id: "gptbot", name: "GPTBot" },
{ id: "ccbot", name: "CCBot" },
{ id: "ia_archiver", name: "ia_archiver" },
{ id: "claudebot", name: "ClaudeBot" },
{ id: "perplexity", name: "PerplexityBot" },
{ id: "facebookexternalhit", name: "facebookexternalhit/1.1" },
{ id: "twitterbot", name: "Twitterbot" },
{ id: "linkedinbot", name: "LinkedInBot" },
{ id: "bytespider", name: "ByteSpider" },
{ id: "applebot", name: "AppleBot" },
{ id: "amazonbot", name: "AmazonBot" }
]
let crawlerContent = "";
crawlers.forEach((crawler) => {
if (robots.crawlers.some(c => c === crawler.id)) {
const crawlerData = crawlers.find(c => c.id === crawler.id);
crawlerContent = crawlerContent +
`User-agent: ${crawlerData!.name}\nAllow: /\nCrawl-delay: 5\nSitemap: ${settings.website.domainName}/sitemap/index.xml`
}
else {
const crawlerData = crawlers.find(c => c.id === crawler.id);
crawlerContent = crawlerContent +
`User-agent: ${crawlerData!.name}\nDisallow: /`
}
crawlerContent = crawlerContent + "\n\n"
});
return new Response(crawlerContent.trim(), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "text/plain"
}
});
}) satisfies APIRoute;

View File

@@ -0,0 +1,27 @@
import { getSettings } from "@/content/settings/settings";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async () => {
const settings = await getSettings();
let sitemapContent = `
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>${settings.website.applicationName}</title>
<description>This is the RSS feed of ${settings.website.applicationName}</description>
<link>${settings.website.domainName}</link>
<lastBuildDate>Sat, 13 Dec 2003 18:30:02 GMT</lastBuildDate>
</channel>
</rss>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;

View File

@@ -0,0 +1,70 @@
import { getAllAlbums } from "@/content/photos/albums";
import { getSettings } from "@/content/settings/settings";
import { getAlbumRoute } from "@/lib/routing";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async ({ params }) => {
const settings = await getSettings();
if (!settings.photo.enabled) {
return new Response(null, {
status: 204,
statusText: "Not Found"
});
}
const currentPage = params.page;
const albums = await getAllAlbums(settings);
const selectedAlbums = albums.slice(
((Number(currentPage) - 1) * settings.sitemap.perPage),
Number(currentPage) * settings.sitemap.perPage - 1
);
let pages: SitemapPage[] = [];
selectedAlbums.forEach((album) => {
pages.push({
url: getAlbumRoute(settings.photo, album),
lastModified: album.lastModified
});
});
let sitemapContent = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map((page) => `
<sitemap>
<loc>${settings.website.domainName}${page.url}</loc>
<lastmod>${page.lastModified.toISOString()}</lastmod>
</sitemap>
`).join('')}
</sitemapindex>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;
export async function getStaticPaths() {
const settings = await getSettings();
const albums = await getAllAlbums(settings);
const albumCount = albums.length;
const perPage = settings.sitemap.perPage;
const pages = Math.ceil(albumCount / perPage);
let items: any[] = [];
for (let i = 0; i < pages; i++) {
items.push({ params: { page: (i + 1).toString() } });
}
return items;
}

View File

@@ -0,0 +1,68 @@
import { getAllAlbums } from "@/content/photos/albums";
import { getSettings } from "@/content/settings/settings";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async () => {
const settings = await getSettings();
if (!settings.photo.enabled) {
return new Response(null, {
status: 204,
statusText: "Not Found"
});
}
const albums = await getAllAlbums(settings);
const albumCount = albums.length;
const perPage = settings.sitemap.perPage;
const pages = Math.ceil(albumCount / perPage);
let sitemaps: SitemapIndex[] = [];
for (let i = 0; i < pages; i++) {
const selectedProjects = albums.slice(
((Number(i + 1) - 1) * settings.sitemap.perPage),
Number(i + 1) * settings.sitemap.perPage - 1
);
let dates = [
settings.sitemap.lastModified,
settings.photo.lastModified,
settings.website.lastModified
];
selectedProjects.forEach((project) => {
dates.push(project.lastModified);
});
const lastModified = dates.sort((a: Date, b: Date) => {
return b.getTime() - a.getTime();
});
sitemaps.push({
url: `/sitemap/albums-${i + 1}.xml`,
lastModified: lastModified[0]
});
}
let sitemapContent = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemaps.map((item) => `
<sitemap>
<loc>${settings.website.domainName}${item.url}</loc>
<lastmod>${item.lastModified.toISOString()}</lastmod>
</sitemap>
`).join('')}
</sitemapindex>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;

View File

@@ -0,0 +1,70 @@
import { getAllBlogs } from "@/content/blogs/blogs";
import { getSettings } from "@/content/settings/settings";
import { getBlogRoute } from "@/lib/routing";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async ({ params }) => {
const settings = await getSettings();
if (!settings.blog.enabled) {
return new Response(null, {
status: 204,
statusText: "Not Found"
});
}
const currentPage = params.page;
const blogs = await getAllBlogs(settings);
const selectedBlogs = blogs.slice(
((Number(currentPage) - 1) * settings.sitemap.perPage),
Number(currentPage) * settings.sitemap.perPage - 1
);
let pages: SitemapPage[] = [];
selectedBlogs.forEach((blog) => {
pages.push({
url: getBlogRoute(settings.blog, blog),
lastModified: blog.lastModified
});
})
let sitemapContent = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map((page) => `
<sitemap>
<loc>${settings.website.domainName}${page.url}</loc>
<lastmod>${page.lastModified.toISOString()}</lastmod>
</sitemap>
`).join('')}
</sitemapindex>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;
export async function getStaticPaths() {
const settings = await getSettings();
const blogs = await getAllBlogs(settings);
const blogCount = blogs.length;
const perPage = settings.sitemap.perPage;
const pages = Math.ceil(blogCount / perPage);
let items: any[] = [];
for (let i = 0; i < pages; i++) {
items.push({ params: { page: (i + 1).toString() } });
}
return items;
}

View File

@@ -0,0 +1,68 @@
import { getAllBlogs } from "@/content/blogs/blogs";
import { getSettings } from "@/content/settings/settings";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async () => {
const settings = await getSettings();
if (!settings.blog.enabled) {
return new Response(null, {
status: 204,
statusText: "Not Found"
});
}
const blogs = await getAllBlogs(settings);
const blogCount = blogs.length;
const perPage = settings.sitemap.perPage;
const pages = Math.ceil(blogCount / perPage);
let sitemaps: SitemapIndex[] = [];
for (let i = 0; i < pages; i++) {
const selectedBlogs = blogs.slice(
((Number(i + 1) - 1) * settings.sitemap.perPage),
Number(i + 1) * settings.sitemap.perPage - 1
);
let dates = [
settings.sitemap.lastModified,
settings.blog.lastModified,
settings.website.lastModified
];
selectedBlogs.forEach((blog) => {
dates.push(blog.lastModified);
});
const lastModified = dates.sort((a: Date, b: Date) => {
return b.getTime() - a.getTime();
});
sitemaps.push({
url: `/sitemap/blogs-${i + 1}.xml`,
lastModified: lastModified[0]
});
}
let sitemapContent = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemaps.map((item) => `
<sitemap>
<loc>${settings.website.domainName}${item.url}</loc>
<lastmod>${item.lastModified.toISOString()}</lastmod>
</sitemap>
`).join('')}
</sitemapindex>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;

View File

@@ -0,0 +1,104 @@
import { getAllBlogs } from "@/content/blogs/blogs";
import { getAllAlbums } from "@/content/photos/albums";
import { getAllProjects } from "@/content/projects/projects";
import { getSettings } from "@/content/settings/settings";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async () => {
const settings = await getSettings();
let sitemapIndex: SitemapIndex[] = [
{
url: "/sitemap/pages.xml",
lastModified: new Date()
}
];
if (settings.blog.enabled) {
const blogLastModifieds = [
settings.blog.lastModified,
settings.sitemap.lastModified,
settings.website.lastModified
];
let blogs = await getAllBlogs(settings);
blogs.forEach((blog) => {
blogLastModifieds.push(blog.lastModified);
});
const lastModifiedBlogs = blogLastModifieds.sort((a: Date, b: Date) => {
return b.getTime() - a.getTime();
});
sitemapIndex.push({
url: "/sitemap/blogs.xml",
lastModified: lastModifiedBlogs[0]
});
};
if (settings.project.enabled) {
const projectLastModifieds = [
settings.project.lastModified,
settings.sitemap.lastModified,
settings.website.lastModified
];
let projects = await getAllProjects(settings);
projects.forEach((project) => {
projectLastModifieds.push(project.lastModified);
});
const lastModifiedProjects = projectLastModifieds.sort((a: Date, b: Date) => {
return b.getTime() - a.getTime();
});
sitemapIndex.push({
url: "/sitemap/projects.xml",
lastModified: lastModifiedProjects[0]
});
};
if (settings.photo.enabled) {
const photoLastModifieds = [
settings.photo.lastModified,
settings.sitemap.lastModified,
settings.website.lastModified
];
let albums = await getAllAlbums(settings);
albums.forEach((album) => {
photoLastModifieds.push(album.lastModified);
});
const lastModifiedAlbums = photoLastModifieds.sort((a: Date, b: Date) => {
return b.getTime() - a.getTime();
});
sitemapIndex.push({
url: "/sitemap/albums.xml",
lastModified: lastModifiedAlbums[0]
});
};
let sitemapContent = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemapIndex.map((item) => `
<sitemap>
<loc>${settings.website.domainName}${item.url}</loc>
<lastmod>${item.lastModified.toISOString()}</lastmod>
</sitemap>
`).join('')}
</sitemapindex>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;

View File

@@ -0,0 +1,64 @@
import { getAllWebpages } from "@/content/pages/pages";
import { getSettings } from "@/content/settings/settings";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async ({ params }) => {
const settings = await getSettings();
const currentPage = params.page;
const webPages = await getAllWebpages();
const selectedPages = webPages.slice(
((Number(currentPage) - 1) * settings.sitemap.perPage),
Number(currentPage) * settings.sitemap.perPage - 1
)
let pages: SitemapPage[] = [];
selectedPages.forEach((page) => {
if (page.exists) {
pages.push({
url: page.url,
lastModified: page.lastModified
});
}
});
let sitemapContent = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map((page) => `
<sitemap>
<loc>${settings.website.domainName}${page.url}</loc>
<lastmod>${page.lastModified.toISOString()}</lastmod>
</sitemap>
`).join('')}
</sitemapindex>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;
export async function getStaticPaths() {
const settings = await getSettings();
const webPages = await getAllWebpages();
const pageCount = webPages.length;
const perPage = settings.sitemap.perPage;
const pages = Math.ceil(pageCount / perPage);
let items: any[] = [];
for (let i = 0; i < pages; i++) {
items.push({ params: { page: (i + 1).toString() } });
}
return items;
}

View File

@@ -0,0 +1,42 @@
import { getAllWebpages } from "@/content/pages/pages";
import { getSettings } from "@/content/settings/settings";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async () => {
const settings = await getSettings();
const webPages = await getAllWebpages();
const pageCount = webPages.length;
const perPage = settings.sitemap.perPage;
const pages = Math.ceil(pageCount / perPage);
let sitemaps: SitemapIndex[] = [];
for (let i = 0; i < pages; i++) {
sitemaps.push({
url: `/sitemap/pages-${i + 1}.xml`,
lastModified: new Date()
});
}
let sitemapContent = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemaps.map((item) => `
<sitemap>
<loc>${settings.website.domainName}${item.url}</loc>
<lastmod>${item.lastModified.toISOString()}</lastmod>
</sitemap>
`).join('')}
</sitemapindex>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;

View File

@@ -0,0 +1,70 @@
import { getAllProjects } from "@/content/projects/projects";
import { getSettings } from "@/content/settings/settings";
import { getProjectRoute } from "@/lib/routing";
import type { APIRoute } from "astro";
import minifyXML from "minify-xml";
export const GET = (async ({ params }) => {
const settings = await getSettings();
if (!settings.project.enabled) {
return new Response(null, {
status: 204,
statusText: "Not Found"
});
}
const currentPage = params.page;
const projects = await getAllProjects(settings);
const selectedProjects = projects.slice(
((Number(currentPage) - 1) * settings.sitemap.perPage),
Number(currentPage) * settings.sitemap.perPage - 1
);
let pages: SitemapPage[] = [];
selectedProjects.forEach((project) => {
pages.push({
url: getProjectRoute(settings.project, project),
lastModified: project.lastModified
});
});
let sitemapContent = `
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map((page) => `
<sitemap>
<loc>${settings.website.domainName}${page.url}</loc>
<lastmod>${page.lastModified.toISOString()}</lastmod>
</sitemap>
`).join('')}
</sitemapindex>
`;
return new Response(minifyXML(sitemapContent), {
status: 200,
statusText: "OK",
headers: {
"Content-Type": "application/xml"
}
});
}) satisfies APIRoute;
export async function getStaticPaths() {
const settings = await getSettings();
const projects = await getAllProjects(settings);
const projectCount = projects.length;
const perPage = settings.sitemap.perPage;
const pages = Math.ceil(projectCount / perPage);
let items: any[] = [];
for (let i = 0; i < pages; i++) {
items.push({ params: { page: (i + 1).toString() } });
}
return items;
}

Some files were not shown because too many files have changed in this diff Show More