Compare commits
9 Commits
draft
...
7e4f1acbf1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e4f1acbf1 | ||
|
|
fcb52ebdc9 | ||
|
|
c0a7f193db | ||
|
|
da9d256b71 | ||
|
|
d03478f81a | ||
|
|
120ce7726f | ||
|
|
bd814e06ce | ||
|
|
cd99d22174 | ||
|
|
128af8a7d5 |
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
public/images/photos/*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||||
39
.github/workflows/deploy.yml
vendored
@@ -1,39 +0,0 @@
|
|||||||
name: Deploy to GitHub Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Trigger the workflow every time you push to the `main` branch
|
|
||||||
# Using a different branch name? Replace `main` with your branch’s name
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
# Allows you to run this workflow manually from the Actions tab on GitHub.
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# Allow this job to clone the repo and create a page deployment
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout your repository using git
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Install, build, and upload your site
|
|
||||||
uses: withastro/action@v3
|
|
||||||
# with:
|
|
||||||
# path: . # The root location of your Astro project inside the repository. (optional)
|
|
||||||
# node-version: 20 # The specific version of Node that should be used to build your site. Defaults to 20. (optional)
|
|
||||||
# package-manager: pnpm@latest # The Node package manager that should be used to install dependencies and build your site. Automatically detected based on your lockfile. (optional)
|
|
||||||
|
|
||||||
deploy:
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v4
|
|
||||||
3986
package-lock.json
generated
@@ -9,11 +9,10 @@
|
|||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.3.5",
|
"@astrojs/mdx": "^5.0.3",
|
||||||
"@astrojs/rss": "^4.0.12",
|
"@astrojs/rss": "^4.0.18",
|
||||||
"@astrojs/sitemap": "^3.5.1",
|
"@astrojs/sitemap": "^3.7.2",
|
||||||
"@astrojs/vue": "^5.1.1",
|
"astro": "^6.1.4",
|
||||||
"astro": "^5.13.7",
|
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-math": "^6.0.0"
|
"remark-math": "^6.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
3300
public/geojson/freiraum/uni_konstanz.geojson
Normal file
BIN
public/images/photography/DSC05932.jpg
Normal file
|
After Width: | Height: | Size: 3.7 MiB |
BIN
public/images/photography/DSC06124.jpg
Normal file
|
After Width: | Height: | Size: 5.7 MiB |
BIN
public/images/photography/DSC06196.jpg
Normal file
|
After Width: | Height: | Size: 3.6 MiB |
BIN
public/images/photography/UKN_Topo_01.jpg
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
public/images/photography/UKN_Topo_02.jpg
Normal file
|
After Width: | Height: | Size: 4.3 MiB |
BIN
public/images/photography/UKN_Topo_03.jpg
Normal file
|
After Width: | Height: | Size: 5.4 MiB |
BIN
public/images/posts/freiraum/homepage.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
@@ -1,25 +1,11 @@
|
|||||||
---
|
---
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
import Socials from "./Socials.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
© {today.getFullYear()} <a href="/about">Anton Pogrebnjak</a>. All rights reserved.
|
© {today.getFullYear()} <a href="/about">Anton</a>. All rights reserved.
|
||||||
<div class="social-links">
|
<Socials />
|
||||||
<a href="https://github.com/Pantonius" target="_blank">
|
|
||||||
<span class="sr-only">Go to my GitHub repo</span>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-hidden="true"
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
astro-icon="social/github"
|
|
||||||
><path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
</footer>
|
||||||
<style>
|
<style>
|
||||||
footer {
|
footer {
|
||||||
@@ -28,12 +14,6 @@ const today = new Date();
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 1em;
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
.social-links a {
|
.social-links a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
|||||||
25
src/components/Freiraum/UKNMap.astro
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<div id="map" style="height: 400px;">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let map = L.map('map').setView([47.689749, 9.187857], 16);
|
||||||
|
L.tileLayer('https://sgx.geodatenzentrum.de/wmts_basemapde/tile/1.0.0/de_basemapde_web_raster_grau/default/GLOBAL_WEBMERCATOR/{z}/{y}/{x}.png', {
|
||||||
|
minZoom: 0,
|
||||||
|
maxZoom: 20,
|
||||||
|
attribution: 'Map data: © <a href="http://www.govdata.de/dl-de/by-2-0">dl-de/by-2-0</a>',
|
||||||
|
ext: 'png'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
let geojson = await fetch('/geojson/freiraum/uni_konstanz.geojson', {
|
||||||
|
method: 'GET',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(res => res.json());
|
||||||
|
|
||||||
|
let geoLayer = L.geoJson(geojson, {
|
||||||
|
style: { fillColor: 'rgba(18, 174, 225, .2)', fillOpacity: 1, stroke: true, color: 'rgba(18, 174, 225, .6)' },
|
||||||
|
}).addTo(map);
|
||||||
|
</script>
|
||||||
49
src/components/Gallery.astro
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import type { Photo } from '../models/Photo.ts';
|
||||||
|
|
||||||
|
const photos: Photo[] = Astro.props.photos;
|
||||||
|
const collection: string = Astro.props.collection;
|
||||||
|
|
||||||
|
const color = Astro.props.color || 'var(--text)';
|
||||||
|
---
|
||||||
|
<section>
|
||||||
|
{
|
||||||
|
photos.map((photo) => (
|
||||||
|
<a href={"/photography/" + photo.id}>
|
||||||
|
<img src={photo.src} />
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style define:vars={{ color }}>
|
||||||
|
section {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 200px;
|
||||||
|
max-height: 12.5rem;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 12.5rem;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
transition: transform .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
img:hover {
|
||||||
|
transform: scale(1.2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { ClientRouter } from "astro:transitions";
|
import { ClientRouter } from "astro:transitions";
|
||||||
import HeaderLink from "./HeaderLink.astro";
|
import HeaderLink from "./HeaderLink.astro";
|
||||||
|
import Socials from "./Socials.astro";
|
||||||
---
|
---
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
@@ -9,25 +10,11 @@ import HeaderLink from "./HeaderLink.astro";
|
|||||||
<img src="/logo.svg" alt="ಠ_ಠ" />
|
<img src="/logo.svg" alt="ಠ_ಠ" />
|
||||||
</a>
|
</a>
|
||||||
<div class="internal-links">
|
<div class="internal-links">
|
||||||
<HeaderLink href="/">Home</HeaderLink>
|
|
||||||
<HeaderLink href="/projects">Projects</HeaderLink>
|
<HeaderLink href="/projects">Projects</HeaderLink>
|
||||||
|
<HeaderLink href="/photography">Photography</HeaderLink>
|
||||||
<HeaderLink href="/blog">Blog</HeaderLink>
|
<HeaderLink href="/blog">Blog</HeaderLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="social-links">
|
<Socials />
|
||||||
<a href="https://github.com/Pantonius" target="_blank">
|
|
||||||
<span class="sr-only">Go to my GitHub repo</span>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
aria-hidden="true"
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
><path
|
|
||||||
fill="currentColor"
|
|
||||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -72,10 +59,6 @@ import HeaderLink from "./HeaderLink.astro";
|
|||||||
border-bottom-color: var(--primary);
|
border-bottom-color: var(--primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-links,
|
|
||||||
.social-links a {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
header .logo img {
|
header .logo img {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { ClientRouter } from "astro:transitions";
|
|||||||
document.addEventListener("mousemove", (e) => {
|
document.addEventListener("mousemove", (e) => {
|
||||||
mouse.x = e.clientX;
|
mouse.x = e.clientX;
|
||||||
mouse.y = e.clientY;
|
mouse.y = e.clientY;
|
||||||
|
draw();
|
||||||
});
|
});
|
||||||
|
|
||||||
let distanceThreshold = .2;
|
let distanceThreshold = .2;
|
||||||
@@ -66,7 +67,5 @@ import { ClientRouter } from "astro:transitions";
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setInterval(() => {
|
|
||||||
draw();
|
draw();
|
||||||
}, 1000 / 60);
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ const color = Astro.props.color || 'var(--text)';
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
font-size: 1em;
|
||||||
|
font-variant-caps: small-caps;
|
||||||
}
|
}
|
||||||
.date {
|
.date {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -101,8 +103,8 @@ const color = Astro.props.color || 'var(--text)';
|
|||||||
ul li:first-child {
|
ul li:first-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
ul li:first-child .title {
|
ul li:first-child .title, ul li .title {
|
||||||
font-size: 1.563em;
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
28
src/components/Socials.astro
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<div class="social-links">
|
||||||
|
<a href="https://github.com/Pantonius" target="_blank">
|
||||||
|
<span class="sr-only">Go to my GitHub repo</span>
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
aria-hidden="true"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
><path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"
|
||||||
|
></path></svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://mastodon.social/@pantonius" target="_blank">
|
||||||
|
<span class="sr-only">Go to my Mastodon profile</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="46" height="46"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M529 243.1C529 145.9 465.3 117.4 465.3 117.4C402.8 88.7 236.7 89 174.8 117.4C174.8 117.4 111.1 145.9 111.1 243.1C111.1 358.8 104.5 502.5 216.7 532.2C257.2 542.9 292 545.2 320 543.6C370.8 540.8 399.3 525.5 399.3 525.5L397.6 488.6C397.6 488.6 361.3 500 320.5 498.7C280.1 497.3 237.5 494.3 230.9 444.7C230.3 440.1 230 435.4 230 430.8C315.6 451.7 388.7 439.9 408.7 437.5C464.8 430.8 513.7 396.2 519.9 364.6C529.7 314.8 528.9 243.1 528.9 243.1zM453.9 368.3L407.3 368.3L407.3 254.1C407.3 204.4 343.3 202.5 343.3 261L343.3 323.5L297 323.5L297 261C297 202.5 233 204.4 233 254.1L233 368.3L186.3 368.3C186.3 246.2 181.1 220.4 204.7 193.3C230.6 164.4 284.5 162.5 308.5 199.4L320.1 218.9L331.7 199.4C355.8 162.3 409.8 164.6 435.5 193.3C459.2 220.6 453.9 246.3 453.9 368.3L453.9 368.3z"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.social-links,
|
||||||
|
.social-links a {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,21 @@
|
|||||||
import { glob } from 'astro/loaders';
|
import { glob, file } from 'astro/loaders';
|
||||||
import { defineCollection, z } from 'astro:content';
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const bachelor = defineCollection({
|
||||||
|
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
||||||
|
loader: glob({ base: './src/content/bachelor', pattern: '**/*.{md,mdx}' }),
|
||||||
|
// Type-check frontmatter using a schema
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
part: z.number(),
|
||||||
|
// Transform string to Date object
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
updatedDate: z.coerce.date().optional(),
|
||||||
|
heroImage: z.string().optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const blog = defineCollection({
|
const blog = defineCollection({
|
||||||
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
||||||
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
|
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
|
||||||
@@ -13,10 +28,26 @@ const blog = defineCollection({
|
|||||||
updatedDate: z.coerce.date().optional(),
|
updatedDate: z.coerce.date().optional(),
|
||||||
heroImage: z.string().optional(),
|
heroImage: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const photography = defineCollection({
|
||||||
|
// Load Markdown and MDX files in the `src/content/projects/` directory.
|
||||||
|
loader: file(
|
||||||
|
'./src/content/photography/index.json'
|
||||||
|
),
|
||||||
|
// Type-check frontmatter using a schema
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
src: z.string(),
|
||||||
|
// Transform string to Date object
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const projects = defineCollection({
|
const projects = defineCollection({
|
||||||
// Load Markdown and MDX files in the `src/content/blog/` directory.
|
// Load Markdown and MDX files in the `src/content/projects/` directory.
|
||||||
loader: glob({
|
loader: glob({
|
||||||
base: './src/content/projects', pattern: '**/*.{md,mdx}'
|
base: './src/content/projects', pattern: '**/*.{md,mdx}'
|
||||||
}),
|
}),
|
||||||
@@ -31,4 +62,4 @@ const projects = defineCollection({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = { blog, projects };
|
export const collections = { bachelor, blog, photography, projects };
|
||||||
|
|||||||
44
src/content/photography/index.json
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "DSC05932",
|
||||||
|
"src": "DSC05932.jpg",
|
||||||
|
"title": "DSC05932",
|
||||||
|
"pubDate": "2026-04-06",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DSC06124",
|
||||||
|
"src": "DSC06124.jpg",
|
||||||
|
"title": "DSC06124",
|
||||||
|
"pubDate": "2026-04-06",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "DSC06196",
|
||||||
|
"src": "DSC06196.jpg",
|
||||||
|
"title": "DSC06196",
|
||||||
|
"pubDate": "2026-04-06",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "UKN_Topo_01",
|
||||||
|
"src": "UKN_Topo_01.jpg",
|
||||||
|
"title": "UKN_Topo_01",
|
||||||
|
"pubDate": "2026-04-06",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "UKN_Topo_02",
|
||||||
|
"src": "UKN_Topo_02.jpg",
|
||||||
|
"title": "UKN_Topo_02",
|
||||||
|
"pubDate": "2026-04-06",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "UKN_Topo_03",
|
||||||
|
"src": "UKN_Topo_03.jpg",
|
||||||
|
"title": "UKN_Topo_03",
|
||||||
|
"pubDate": "2026-04-06",
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
75
src/content/projects/Freiraum.mdx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
title: Freiraum
|
||||||
|
pubDate: 2026-02-23T12:00:00Z
|
||||||
|
description: Freiraum is a room availability checker developed for the students of the University of Konstanz
|
||||||
|
heroImage: /images/posts/freiraum/homepage.png
|
||||||
|
---
|
||||||
|
import UKNMap from '../../components/Freiraum/UKNMap.astro';
|
||||||
|
|
||||||
|
Freiraum is a room availability checker developed for the students of the University of Konstanz. Much like the [Frag Wahltraut tool](../frag-wahltraut), it runs entirely on the client side. Meaning, the server has no real knowledge of any rooms in the university or their status. Instead, the client code fetches the data from a server, that is only available from within the university network.
|
||||||
|
|
||||||
|
# Background
|
||||||
|
During the lecture and exam periods, students are in need of viable working areas. Both for silent and group work. While the library is readily available for silent work, even its vast space can at times (in exam periods) reach full capacity. On the front of working areas for groups, the library is short on rooms that are sound isolated.
|
||||||
|
|
||||||
|
Thankfully, the university itself is much more than its library. Many rooms, that are used throughout the day will occassionally have some free spots, in which students can freely occupy the space and even use it effectively for group work without disturbing others.
|
||||||
|
|
||||||
|
While the university has been working on accommodating the students' need for working spaces, a real long-term solution hasn't left the planning phase (atleast that I am aware of).
|
||||||
|
|
||||||
|
However, anyone within the university network has access to a certain web service, providing the full planned occupancy of many rooms within the university.
|
||||||
|
|
||||||
|
# The Idea
|
||||||
|
The current occupancy tool is very inaccessible:
|
||||||
|
- it's only reachable via an IP-address
|
||||||
|
- its UI is very dense and hard to navigate
|
||||||
|
- if you are searching for any vacancies on a given time and day, you're out of luck -- you have to go through each room by hand and check its availability at that time
|
||||||
|
|
||||||
|
While a replacement is on its way, the student representation decided to leverage the current endpoint and build a tool that:
|
||||||
|
- is easy to navigate
|
||||||
|
- and provides the students with exactly the information they need:
|
||||||
|
|
||||||
|
<q>Which rooms are available throughout the day?</q>
|
||||||
|
|
||||||
|
|
||||||
|
# The Implementation
|
||||||
|
Freiraum mainly uses a timetable format with the rooms indicated by rows and the hours by columns. A vertical red indicator shows the current time over each row.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
While lectures and other occupancies appear grayed-out, the computed free spots inbetween are rendered in a light blue shade, making current and future availabilities tangible at a single glance.
|
||||||
|
|
||||||
|
Each room and event can be clicked for a more detailed overview.
|
||||||
|
|
||||||
|
## Room Details
|
||||||
|
- the **kind of room** (auditorium, seminar, etc.),
|
||||||
|
- **its building**
|
||||||
|
- **its designation**
|
||||||
|
- **its capacity**
|
||||||
|
- other details like the availability of a projector, HDMI or VGA connection, blackboard, etc.
|
||||||
|
|
||||||
|
## Event Details
|
||||||
|
- the **kind of event** (lecture, seminar, etc.),
|
||||||
|
- **its title**,
|
||||||
|
- **the person that booked the slot**.
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
The entire HTML of the timetable is generated via client-side javascript, which leads to pretty involved code, but gets the job done.
|
||||||
|
|
||||||
|
# Future Work / Other Ideas
|
||||||
|
An initial proof of concept also included a map view intended to give a visual queue of where the room is located. I later dismissed the idea for the time being because obtaining the shape and geo data for each room would have entaled a lot of manual work.
|
||||||
|
|
||||||
|
However, I learned that germany, and in fact the european union as well, provides some basic geo data for buildings, their area and roof shape, which is how I extracted this wonderful geojson shape data for each of the university buildings:
|
||||||
|
|
||||||
|
<UKNMap />
|
||||||
|
|
||||||
|
If anyone feels like it, some kind of navigation system might be fun to implement. Though I have heard that many have tried and failed miserably already.
|
||||||
|
|
||||||
|
Sticking with the timetable view, there is still room for filters and search. Possible queries could restrict the displayed rooms to only those of a certain size or kind (who actually wants to study in an auditorium when there is no lecture there?). Even the availability of a projector or blackboard could be filtered for or made directly visible through the use of icons (while trying not to overload the initial overview of the rooms).
|
||||||
|
|
||||||
|
|
||||||
|
# Final Notes
|
||||||
|
Apart from that, I am content with Freiraum in its current form. Part of its charme for me is its simplicity, though I am certain some people would disaggree. The code base is fairly easy to maintain, despite it being written in vanilla javascript (I seem to have a knack for that -- call me a masochist I guess). Often times I can quickly swoop in and change aspects of the UI or logic, without having to search all too long. Granted, I wrote the hecking thing. But the generation code for the actual HTML timetable is the only aspect that feels convoluted to me. All things considered, I feel certain that anyone can pick the code and adapt it to their will. In terms of performance and reliablity it seems to be robust enough that nobody has thrown tomatoes in my face -- yet.
|
||||||
|
|
||||||
|
The actual data endpoint for the room occupancy will move to the university's e-learning platform soon. As of now I have no clue wether the new version on the e-learning platform will provide a similarly accessible view of vacant rooms for students (I highly doubt it). Nor do I know if there is a similar API endpoint, that Freiraum can be attached to; Unless the IT department of the university provides one themselves, Freiraum will probably not be able to exist in its current way.
|
||||||
|
|
||||||
|
Whatever the future may hold, I am happy to have seen that it found use both by students and university employees. It turns out that the simplest of tools can make quite a difference :)
|
||||||
@@ -32,6 +32,8 @@ const { title, description, pubDate, updatedDate, heroImage } = Astro.props;
|
|||||||
integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05"
|
integrity="sha384-+VBxd3r6XgURycqtZ117nYw44OOcIax56Z4dCRWbxyPt0Koah1uHoK0o4+/RRE05"
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
onload="renderMathInElement(document.body);"></script>
|
onload="renderMathInElement(document.body);"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||||
<style>
|
<style>
|
||||||
article {
|
article {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
103
src/layouts/GalleryPhoto.astro
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
import BaseHead from "../components/BaseHead.astro";
|
||||||
|
import Header from "../components/Header.astro";
|
||||||
|
import Main from "../components/Main.astro";
|
||||||
|
import Footer from "../components/Footer.astro";
|
||||||
|
import FormattedDate from "../components/FormattedDate.astro";
|
||||||
|
|
||||||
|
type Props = CollectionEntry<"photography">["data"];
|
||||||
|
|
||||||
|
const { title, description, pubDate, src } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<BaseHead title={title} description={description} image={src} />
|
||||||
|
<style>
|
||||||
|
article {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
/* width: 100%; */
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--primary);
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
/* width: 100%; */
|
||||||
|
height: 2px;
|
||||||
|
background-color: var(--primary);
|
||||||
|
margin: 0.5rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image img {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose {
|
||||||
|
width: 720px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 1em;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 1em 0;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title h1 {
|
||||||
|
margin: 0 0 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
<Main>
|
||||||
|
<article>
|
||||||
|
<div class="hero-image">
|
||||||
|
<img
|
||||||
|
width={1020}
|
||||||
|
height={510}
|
||||||
|
src={src}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="prose">
|
||||||
|
<div class="title">
|
||||||
|
<div class="date">
|
||||||
|
<FormattedDate date={pubDate} />
|
||||||
|
</div>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Main>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
32
src/models/Photo.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export type CollectionPhoto = {
|
||||||
|
id: string;
|
||||||
|
collection: "photography";
|
||||||
|
data: {
|
||||||
|
title: string,
|
||||||
|
src: string,
|
||||||
|
pubDate: Date,
|
||||||
|
description: string
|
||||||
|
};
|
||||||
|
filePath?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Photo = {
|
||||||
|
id: string,
|
||||||
|
title: string;
|
||||||
|
src: string;
|
||||||
|
pubDate: Date;
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
collection: "photography";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertPhoto = (collectionPhoto: CollectionPhoto): Photo => {
|
||||||
|
return {
|
||||||
|
id: collectionPhoto.id,
|
||||||
|
title: collectionPhoto.data.title,
|
||||||
|
src: "/images/photography/" + collectionPhoto.data.src,
|
||||||
|
pubDate: collectionPhoto.data.pubDate,
|
||||||
|
description: collectionPhoto.data.description,
|
||||||
|
collection: collectionPhoto.collection,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ import { SITE_TITLE, SITE_DESCRIPTION } from "../consts";
|
|||||||
<body>
|
<body>
|
||||||
<Header />
|
<Header />
|
||||||
<Main>
|
<Main>
|
||||||
<h1>Anton Pogrebnjak</h1>
|
<h1>Anton</h1>
|
||||||
<div style="max-width: 40rem">
|
<div style="max-width: 40rem">
|
||||||
<p>
|
<p>
|
||||||
Really, I don't have much to tell you about myself. Just look at the amazing stuff I've done and written about. That should suffice.
|
Really, I don't have much to tell you about myself. Just look at the amazing stuff I've done and written about. That should suffice.
|
||||||
|
|||||||
@@ -24,14 +24,15 @@ const posts: Post[] = (await getCollection("projects"))
|
|||||||
<section id="welcome">
|
<section id="welcome">
|
||||||
<h1>Hi 👋, my name is <a href="/about">Anton</a></h1>
|
<h1>Hi 👋, my name is <a href="/about">Anton</a></h1>
|
||||||
<h2 class="typeout">
|
<h2 class="typeout">
|
||||||
and I am passionate about a whole bunch of things
|
Welcome to my realm!
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<h3 class="buttons">
|
<h4 class="buttons">
|
||||||
<a href="/blog">Blog</a>
|
|
||||||
<a href="/projects">Projects</a>
|
<a href="/projects">Projects</a>
|
||||||
|
<a href="/photography">Photography</a>
|
||||||
|
<a href="/blog">Blog</a>
|
||||||
<a href="/about">About</a>
|
<a href="/about">About</a>
|
||||||
</h3>
|
</h4>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="projects">
|
<section id="projects">
|
||||||
@@ -108,14 +109,6 @@ const posts: Post[] = (await getCollection("projects"))
|
|||||||
section {
|
section {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#welcome h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#welcome h2 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
18
src/pages/photography/[...slug].astro
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { render } from 'astro:content';
|
||||||
|
import GalleryPhoto from '../../layouts/GalleryPhoto.astro';
|
||||||
|
import { convertCollectionPhoto, convertPhoto, type Photo } from '../../models/Photo';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const photos: Photo[] = (await getCollection('photography')).map(convertPhoto);
|
||||||
|
return photos.map((photo) => ({
|
||||||
|
params: { slug: photo.id },
|
||||||
|
props: photo,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
type Props = Photo;
|
||||||
|
|
||||||
|
const photo: Photo = Astro.props;
|
||||||
|
---
|
||||||
|
<GalleryPhoto {...photo} />
|
||||||
29
src/pages/photography/index.astro
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import BaseHead from '../../components/BaseHead.astro';
|
||||||
|
import Header from '../../components/Header.astro';
|
||||||
|
import Main from '../../components/Main.astro';
|
||||||
|
import Footer from '../../components/Footer.astro';
|
||||||
|
import { SITE_TITLE, SITE_DESCRIPTION } from '../../consts';
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import Gallery from '../../components/Gallery.astro';
|
||||||
|
import { convertPhoto } from '../../models/Photo';
|
||||||
|
|
||||||
|
const photos: Photo[] = (await getCollection('photography')).map(convertPhoto).sort((a, b) => b.pubDate.valueOf() - a.pubDate.valueOf());
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<BaseHead title={SITE_TITLE} description={SITE_DESCRIPTION} />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
<Main>
|
||||||
|
{photos.length === 0 ? (
|
||||||
|
<h1 style="opacity: .5; user-select: none;">No photos yet 😛</h1>
|
||||||
|
) : ( <Gallery photos={photos} /> )}
|
||||||
|
</Main>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--text: #ffffff;
|
--text: #ffffff;
|
||||||
--text-soft: #ddd;
|
--text-soft: #ddd;
|
||||||
|
--text-softer: #aaa;
|
||||||
--background: #121212;
|
--background: #121212;
|
||||||
--background-soft: #212121;
|
--background-soft: #212121;
|
||||||
--primary: #2df598;
|
--primary: #2df598;
|
||||||
@@ -182,6 +183,20 @@ blockquote {
|
|||||||
font-size: 1.333em;
|
font-size: 1.333em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
q {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.7rem;
|
||||||
|
font-style: oblique;
|
||||||
|
margin: 2rem 0;
|
||||||
|
|
||||||
|
quotes: initial;
|
||||||
|
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--primary);
|
border-top: 1px solid var(--primary);
|
||||||
|
|||||||