Boostrap project
This commit is contained in:
commit
c2500c85e7
38 changed files with 4149 additions and 0 deletions
82
src/routes/(feeds)/posts.[format]/+server.ts
Normal file
82
src/routes/(feeds)/posts.[format]/+server.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import Secrets from '$lib/server/secrets';
|
||||
import type { RequestHandler } from './$types';
|
||||
import GhostContentAPI from '@tryghost/content-api';
|
||||
import { Feed } from 'feed';
|
||||
import { DateTime } from 'luxon';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const trailingSlash = 'never';
|
||||
export const prerender = true;
|
||||
|
||||
export const GET: RequestHandler = async (request) => {
|
||||
//const siteBase = `${request.url.protocol}//${request.url.host}/`
|
||||
const siteBase = `https://public-spaces-preview.pages.dev/`; // TODO: Change!
|
||||
|
||||
const api = new GhostContentAPI({
|
||||
url: Secrets.ghost.url,
|
||||
key: Secrets.ghost.key,
|
||||
version: 'v5.0'
|
||||
});
|
||||
|
||||
const posts = await api.posts.browse({
|
||||
limit: 100,
|
||||
include: ['authors', 'tags']
|
||||
});
|
||||
|
||||
const feed = new Feed({
|
||||
title: 'Public Spaces e.V.',
|
||||
generator: 'Public Spaces e.V. Web',
|
||||
description: 'Die neusten Beiträge von Public Spaces e.V.',
|
||||
language: 'de-DE',
|
||||
link: siteBase,
|
||||
feedLinks: {
|
||||
rss: `${siteBase}posts.rss`,
|
||||
json: `${siteBase}posts.json`,
|
||||
atom: `${siteBase}posts.atom`
|
||||
},
|
||||
id: siteBase,
|
||||
copyright: ''
|
||||
});
|
||||
|
||||
posts.forEach((post) => {
|
||||
const date = DateTime.fromISO(post.updated_at || post.published_at || post.created_at || '');
|
||||
const datePublished = DateTime.fromISO(post.published_at || post.created_at || '');
|
||||
|
||||
feed.addItem({
|
||||
id: `${siteBase}post/${post.slug}`,
|
||||
title: post.title || '',
|
||||
link: `${siteBase}post/${post.slug}`,
|
||||
date: date.toJSDate(),
|
||||
published: datePublished.toJSDate(),
|
||||
description: post.excerpt || '',
|
||||
content: post.html || undefined,
|
||||
author: post.authors?.map((author) => {
|
||||
return {
|
||||
name: author.name
|
||||
};
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
switch (request.params.format) {
|
||||
case 'rss':
|
||||
return new Response(feed.rss2(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/rss+xml'
|
||||
}
|
||||
});
|
||||
case 'atom':
|
||||
return new Response(feed.atom1(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/atom+xml'
|
||||
}
|
||||
});
|
||||
case 'json':
|
||||
return new Response(feed.json1(), {
|
||||
headers: {
|
||||
'Content-Type': 'application/feed+json'
|
||||
}
|
||||
});
|
||||
}
|
||||
throw error(404, 'Ungültiges Format');
|
||||
};
|
26
src/routes/(page)/[slug]/+page.server.ts
Normal file
26
src/routes/(page)/[slug]/+page.server.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import Secrets from '$lib/server/secrets';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import GhostContentAPI from '@tryghost/content-api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const api = new GhostContentAPI({
|
||||
url: Secrets.ghost.url,
|
||||
key: Secrets.ghost.key,
|
||||
version: 'v5.0'
|
||||
});
|
||||
|
||||
let page = null;
|
||||
|
||||
try {
|
||||
page = await api.pages.read({
|
||||
slug: params.slug
|
||||
});
|
||||
} catch (e) {
|
||||
throw error(404, 'Seite Nicht Gefunden');
|
||||
}
|
||||
|
||||
return {
|
||||
page
|
||||
};
|
||||
};
|
31
src/routes/(page)/[slug]/+page.svelte
Normal file
31
src/routes/(page)/[slug]/+page.svelte
Normal file
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.page?.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>{data.page?.title}</h1>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
{@html data.page?.html}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
:global {
|
||||
@include ghostContentContainer;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
@include contentGrid;
|
||||
}
|
||||
</style>
|
24
src/routes/+error.svelte
Normal file
24
src/routes/+error.svelte
Normal file
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
|
||||
const texts: { [key: number]: string } = {
|
||||
0: 'Ein unbekannter Fehler ist aufgetreten',
|
||||
404: 'Die angeforderte Seite wurde leider nicht gefunden.',
|
||||
500: 'Es ist ein interner Serverfehler aufgetreten.'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1>{$page.status}: {$page.error?.message}</h1>
|
||||
|
||||
<p>
|
||||
{texts[$page.status] ?? texts[0]}
|
||||
Bitte versuche es später erneut, oder gehe zurück zur <a href="/">Startseite</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
@include contentGrid;
|
||||
}
|
||||
</style>
|
298
src/routes/+layout.svelte
Normal file
298
src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,298 @@
|
|||
<script lang="ts">
|
||||
import { siTwitter, siInstagram, siTelegram } from 'simple-icons';
|
||||
import { Menu, X } from 'lucide-svelte';
|
||||
import Logo from '../assets/logo/LOGO-public-spaces.svg';
|
||||
import { getContext } from 'svelte';
|
||||
import '../app.scss';
|
||||
import type { LayoutData } from './$types';
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
let menuOpen = false;
|
||||
</script>
|
||||
|
||||
<a href="#navigation" class="sr-only sr-only-focusable a11y-jump">Zur Navigation Springen</a>
|
||||
<a href="#content" class="sr-only sr-only-focusable a11y-jump">Zum Inhalt Springen</a>
|
||||
<div class="primary">
|
||||
<nav id="navigation">
|
||||
<div>
|
||||
<div class="logo-container">
|
||||
<a href="/" class="logo-link" title="Public Spaces e.V.">
|
||||
<img src={Logo} alt="Public Spaces Logo" style={data.isHome ? "opacity: 0; transform: translate(20%, 50%)":""} />
|
||||
<span class="sr-only">Public Spaces e.V.</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class={'offscreen-nav' + (menuOpen ? ' active' : '')}>
|
||||
<a
|
||||
href="/posts"
|
||||
on:click={() => {
|
||||
menuOpen = false;
|
||||
}}>Beiträge</a
|
||||
>
|
||||
<a
|
||||
href="/about"
|
||||
on:click={() => {
|
||||
menuOpen = false;
|
||||
}}>Über Uns</a
|
||||
>
|
||||
<a
|
||||
href="/contact"
|
||||
on:click={() => {
|
||||
menuOpen = false;
|
||||
}}>Kontakt</a
|
||||
>
|
||||
</div>
|
||||
<div class="offscreen-nav-button">
|
||||
<button
|
||||
on:click={() => {
|
||||
menuOpen = !menuOpen;
|
||||
}}
|
||||
>
|
||||
{#if !menuOpen}
|
||||
<Menu size={35} />
|
||||
{/if}
|
||||
{#if menuOpen}
|
||||
<X size={35} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="main" role="main" id="content">
|
||||
<slot />
|
||||
</div>
|
||||
<footer>
|
||||
<div class="grid">
|
||||
<div class="primary-links">
|
||||
<a href="/imprint">Impressum</a>
|
||||
<a href="/data_protection">Datenschutz</a>
|
||||
<a href="/disclaimer">Disclaimer</a>
|
||||
</div>
|
||||
<div class="border" />
|
||||
<div class="socials">
|
||||
<!--<a
|
||||
href="https://twitter.com/TODO"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Twitter"
|
||||
>
|
||||
{@html siTwitter.svg}
|
||||
<span class="sr-only">Twitter @TODO</span>
|
||||
</a>
|
||||
<a
|
||||
href="https://t.me/TODO"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
title="Telegram"
|
||||
>
|
||||
{@html siTelegram.svg}
|
||||
<span class="sr-only">Telegram @TODO</span>
|
||||
</a>-->
|
||||
</div>
|
||||
<div class="border" />
|
||||
<div class="supporters">
|
||||
<span class="title">Unterstützer des Vereins</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="copy-notice">
|
||||
CC-BY-4.0 {new Date().getFullYear()}, Public Spaces e.V.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.primary {
|
||||
@include contentGrid;
|
||||
padding-top: var(--nav-space);
|
||||
|
||||
.main {
|
||||
grid-column: full-start/full-end;
|
||||
min-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 4em;
|
||||
background-color: var(--color-dark-surface);
|
||||
color: var(--color-dark-surface-text);
|
||||
grid-column: full-start/full-end;
|
||||
@include contentGrid;
|
||||
|
||||
.grid {
|
||||
grid-column: wide-start/wide-end;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1px 1fr 1px 1fr;
|
||||
gap: var(--gap);
|
||||
|
||||
@media (max-width: 700px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
> .border {
|
||||
width: 1px;
|
||||
margin: 10px 0;
|
||||
background-color: currentColor;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
> :not(.border) {
|
||||
padding: var(--gap);
|
||||
}
|
||||
|
||||
.primary-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
> a {
|
||||
color: var(--color-dark-surface-text-dim);
|
||||
}
|
||||
}
|
||||
|
||||
.supporters {
|
||||
.title {
|
||||
color: var(--color-dark-surface-text-dim);
|
||||
}
|
||||
}
|
||||
|
||||
.socials {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
a {
|
||||
padding: var(--padding);
|
||||
text-decoration: none;
|
||||
background-color: var(--color-surface);
|
||||
margin: var(--padding);
|
||||
border-radius: var(--border-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:global(svg) {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.copy-notice {
|
||||
text-align: center;
|
||||
padding: var(--gap);
|
||||
}
|
||||
}
|
||||
|
||||
nav {
|
||||
@include contentGrid;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 200;
|
||||
overflow: visible;
|
||||
@include glassSurface;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
.logo-container {
|
||||
margin-right: auto;
|
||||
|
||||
.logo-placeholder,
|
||||
img {
|
||||
transition: opacity .25s, transform .25s;
|
||||
width: 100px;
|
||||
height: 45px;
|
||||
//background-color: var(--color-placeholder);
|
||||
//border-radius: var(--border-radius);
|
||||
}
|
||||
}
|
||||
|
||||
.offscreen-nav-button {
|
||||
width: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
|
||||
transition: width 0.25s, opacity 0.25s;
|
||||
|
||||
> button {
|
||||
height: 100%;
|
||||
padding: 0 var(--gap);
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.offscreen-nav-button {
|
||||
display: block;
|
||||
z-index: 300;
|
||||
pointer-events: auto;
|
||||
width: fit-content;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.offscreen-nav {
|
||||
//display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 195;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 110px;
|
||||
font-size: 1.5em;
|
||||
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
opacity: 0;
|
||||
|
||||
transition: height 0.25s, opacity 0.25s;
|
||||
|
||||
@include glassSurface;
|
||||
|
||||
&.active {
|
||||
overflow: auto;
|
||||
pointer-events: auto;
|
||||
height: 100vh;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
> a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--padding);
|
||||
text-decoration: none;
|
||||
|
||||
&.logo-link {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.a11y-jump:focus,
|
||||
.a11y-jump:active {
|
||||
padding: var(--gap) !important;
|
||||
background-color: var(--color-background);
|
||||
z-index: 9999;
|
||||
}
|
||||
</style>
|
9
src/routes/+layout.ts
Normal file
9
src/routes/+layout.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load = (async ({url}) => {
|
||||
return {
|
||||
isHome: url.pathname == "/"
|
||||
};
|
||||
}) satisfies LayoutLoad;
|
||||
|
||||
export const prerender = true;
|
24
src/routes/+page.server.ts
Normal file
24
src/routes/+page.server.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import type { PageServerLoad } from './$types';
|
||||
import GhostContentAPI from '@tryghost/content-api';
|
||||
import Secrets from '$lib/server/secrets';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const api = new GhostContentAPI({
|
||||
url: Secrets.ghost.url,
|
||||
key: Secrets.ghost.key,
|
||||
version: 'v5.0'
|
||||
});
|
||||
|
||||
const posts = await api.posts.browse({
|
||||
limit: 3,
|
||||
include: ['authors', 'tags']
|
||||
});
|
||||
|
||||
const about = await api.pages.read({
|
||||
slug: 'about'
|
||||
});
|
||||
|
||||
|
||||
|
||||
return { posts, about };
|
||||
};
|
230
src/routes/+page.svelte
Normal file
230
src/routes/+page.svelte
Normal file
|
@ -0,0 +1,230 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { ChevronRight } from 'lucide-svelte';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onDestroy, setContext, onMount, tick } from 'svelte';
|
||||
import Logo from '../assets/logo/LOGO-public-spaces.svg';
|
||||
import HWFLogo from '../assets/logo/LOGO-hamburg-werbefrei.svg';
|
||||
import BWFLogo from '../assets/logo/LOGO-berlin-werbefrei.svg';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Public Spaces e.V.</title>
|
||||
<meta name="title" content={'Public Spaces e.V.'} />
|
||||
<meta
|
||||
name="description"
|
||||
content={'Public Spaces ist Trägerin zweier Voksinitiativen, die sich gegen ein Übermaß von Werbung im öffentlichen Raum richten.'}
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="home-runner">
|
||||
<div class="hero">
|
||||
<div class="hero-runner">
|
||||
<img src={Logo} alt="Public Spaces Logo" />
|
||||
<div class="sub-logo-split">
|
||||
<img src={BWFLogo} alt="Berlin Werbefrei Logo" />
|
||||
<img src={HWFLogo} alt="Hamburg Werbefrei Logo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h2>Über Public Spaces e.V.</h2>
|
||||
<p>
|
||||
Hier entsteht die Webseite des Vereins Public Spaces e.V.
|
||||
</p>
|
||||
<p>
|
||||
Public Spaces ist Trägerin zweier Volksinitiativen, die sich gegen ein Übermaß von Werbung im öffentlichen Raum richten.
|
||||
</p>
|
||||
|
||||
<a href="/about" class="more-cta">Mehr über uns <ChevronRight /></a>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>Neuste Beiträge</h2>
|
||||
|
||||
<div class="posts">
|
||||
{#each data.posts as post}
|
||||
<a href={`/posts/${post.slug}`}>
|
||||
{#if !post.feature_image}
|
||||
<div class="image-placeholder">?</div>
|
||||
{/if}
|
||||
{#if post.feature_image}
|
||||
<img
|
||||
src={post.feature_image}
|
||||
alt={post.feature_image_alt || post.feature_image_caption || 'Article'}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<span class="title">{post.title}</span>
|
||||
<p>{post.excerpt}</p>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<a href="/posts" class="kg-btn">Alle Beiträge sehen</a>
|
||||
<!--<a href="/about" class="more-cta">Alle Beiträge <ChevronRight/></a>-->
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.home-runner {
|
||||
@include contentGrid;
|
||||
margin-top: calc(0px - var(--nav-space));
|
||||
}
|
||||
|
||||
.kg-btn {
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.more-cta {
|
||||
margin: var(--gap) var(--gap) var(--gap) auto;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
gap: var(--padding);
|
||||
font-size: 1.2em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.posts {
|
||||
grid-column: wide-start/wide-end;
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
margin-top: 1em;
|
||||
|
||||
> * {
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
flex-direction: column;
|
||||
|
||||
> * {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
img,
|
||||
.image-placeholder {
|
||||
width: 100%;
|
||||
height: 230px;
|
||||
background-color: var(--color-dark-surface);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.image-placeholder {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 3em;
|
||||
font-weight: 600;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.75em;
|
||||
margin-top: 0.8em;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
p {
|
||||
display: -webkit-box;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hero {
|
||||
grid-column: full-start/full-end;
|
||||
max-height: max(70vmin, 500px);
|
||||
min-height: 100px;
|
||||
height: 40vmax;
|
||||
position: relative;
|
||||
|
||||
|
||||
.hero-runner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
padding-top: 50px;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
color: white;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
> img {
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
height: 90%;
|
||||
|
||||
opacity: 1;
|
||||
transform: translate(0, 0);
|
||||
|
||||
animation-name: homeLogoIn;
|
||||
animation-duration: .25s;
|
||||
}
|
||||
|
||||
.sub-logo-split {
|
||||
margin-top: 20px;
|
||||
height: 30%;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
max-width: var(--layout-width);
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
justify-content: space-around;
|
||||
|
||||
> img {
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@media(max-width: 350px) {
|
||||
flex-direction: column;
|
||||
height: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes homeLogoIn {
|
||||
0% {transform: translate(-10%, -25%); opacity: 0}
|
||||
100% {transform: translate(0, 0); opacity: 1}
|
||||
}
|
||||
</style>
|
18
src/routes/posts/+page.server.ts
Normal file
18
src/routes/posts/+page.server.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import type { PageServerLoad } from './$types';
|
||||
import GhostContentAPI from '@tryghost/content-api';
|
||||
import Secrets from '$lib/server/secrets';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const api = new GhostContentAPI({
|
||||
url: Secrets.ghost.url,
|
||||
key: Secrets.ghost.key,
|
||||
version: 'v5.0'
|
||||
});
|
||||
|
||||
const posts = await api.posts.browse({
|
||||
limit: 100,
|
||||
include: ['authors', 'tags']
|
||||
});
|
||||
|
||||
return { posts };
|
||||
};
|
127
src/routes/posts/+page.svelte
Normal file
127
src/routes/posts/+page.svelte
Normal file
|
@ -0,0 +1,127 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Beiträge | Public Spaces e.V.</title>
|
||||
<meta name="title" content={'Beiträge'} />
|
||||
<meta
|
||||
name="description"
|
||||
content={'Lies die neusten Beiträge von Public Spaces e.V. Hier findest du Pressemitteilungen, Artikel und andere Inhalte.'}
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<h1>Beiträge</h1>
|
||||
|
||||
<div class="posts">
|
||||
{#each data.posts as post}
|
||||
<a href={`/posts/` + post.slug}>
|
||||
<div class="feature-image">
|
||||
{#if post.feature_image}
|
||||
<img
|
||||
src={post.feature_image}
|
||||
alt={post.feature_image_alt ||
|
||||
post.feature_image_caption ||
|
||||
post.title ||
|
||||
'Feature Image'}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
<span class="title">{post.title}</span>
|
||||
<span class="meta">
|
||||
{#each post.authors || [] as author, i}
|
||||
{i > 0 ? ' & ' : ''}<span>{author.name}</span>
|
||||
{/each} ·
|
||||
<span
|
||||
>{DateTime.fromISO(post.published_at || post.created_at || '2001-11-03')
|
||||
.setLocale('de-DE')
|
||||
.toLocaleString(DateTime.DATE_FULL)}</span
|
||||
>
|
||||
·
|
||||
<span
|
||||
>Ca. {post.reading_time || post.reading_time === 0
|
||||
? Math.max(1, post.reading_time) + ' Min.'
|
||||
: 'Unbekannte'} Lesezeit</span
|
||||
>
|
||||
</span>
|
||||
{#if post.custom_excerpt || post.excerpt}
|
||||
<p class="excerpt">
|
||||
{(post.custom_excerpt || post.excerpt)?.replaceAll('\n', ' ')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
@include contentGrid;
|
||||
}
|
||||
|
||||
.posts {
|
||||
> a {
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
padding: var(--gap) 0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
display: block;
|
||||
width: 90%;
|
||||
margin: 0 auto;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
border-bottom: thin solid var(--color-border);
|
||||
}
|
||||
|
||||
> .feature-image {
|
||||
--size: 150px;
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
flex-shrink: 0;
|
||||
|
||||
> img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
> .details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .title {
|
||||
font-size: 1.6em;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
> .meta {
|
||||
opacity: 0.75;
|
||||
margin-top: 5px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
> .excerpt {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
29
src/routes/posts/[slug]/+page.server.ts
Normal file
29
src/routes/posts/[slug]/+page.server.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type { PageServerLoad } from './$types';
|
||||
import GhostContentAPI from '@tryghost/content-api';
|
||||
import Secrets from '$lib/server/secrets';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const api = new GhostContentAPI({
|
||||
url: Secrets.ghost.url,
|
||||
key: Secrets.ghost.key,
|
||||
version: 'v5.0'
|
||||
});
|
||||
|
||||
let post = null;
|
||||
|
||||
try {
|
||||
post = await api.posts.read(
|
||||
{
|
||||
slug: params.slug
|
||||
},
|
||||
{ include: ['tags', 'authors'] }
|
||||
);
|
||||
} catch (e) {
|
||||
throw error(404, 'Artikel Nicht Gefunden');
|
||||
}
|
||||
|
||||
return {
|
||||
post
|
||||
};
|
||||
};
|
216
src/routes/posts/[slug]/+page.svelte
Normal file
216
src/routes/posts/[slug]/+page.svelte
Normal file
|
@ -0,0 +1,216 @@
|
|||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Globe } from 'lucide-svelte';
|
||||
import { siTwitter } from 'simple-icons';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
$: publishTime = DateTime.fromISO(data.post.published_at || data.post.created_at || '2001-11-03');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.post.title}</title>
|
||||
<meta name="title" content={data.post.meta_title || data.post.title} />
|
||||
<meta
|
||||
name="description"
|
||||
content={data.post.meta_description || data.post.custom_excerpt || data.post.excerpt}
|
||||
/>
|
||||
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:site" content="@todo" /> <!-- TODO: Change me. -->
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content={data.post.twitter_title || data.post.meta_title || data.post.title}
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={data.post.twitter_description ||
|
||||
data.post.meta_description ||
|
||||
data.post.custom_excerpt ||
|
||||
data.post.excerpt}
|
||||
/>
|
||||
<meta name="twitter:image" content={data.post.twitter_image || data.post.feature_image} />
|
||||
|
||||
<meta property="og:type" content="article" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content={data.post.og_title || data.post.meta_title || data.post.title}
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content={data.post.og_description ||
|
||||
data.post.meta_description ||
|
||||
data.post.custom_excerpt ||
|
||||
data.post.excerpt}
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content={data.post.og_image || data.post.twitter_image || data.post.feature_image}
|
||||
/>
|
||||
<meta property="article:published_time" content={data.post.published_at} />
|
||||
{#if data.post.updated_at}
|
||||
<meta property="article:modified_time" content={data.post.updated_at} />
|
||||
{/if}
|
||||
{#if data.post.primary_tag}
|
||||
<meta
|
||||
property="article:section"
|
||||
content={data.post.primary_tag.og_title || data.post.primary_tag.name}
|
||||
/>
|
||||
{/if}
|
||||
{@html `<script type="application/ld+json">${JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
headline: data.post.title,
|
||||
image: data.post.feature_image,
|
||||
datePublished: data.post.published_at,
|
||||
dateModified: data.post.updated_at,
|
||||
author: data.post.authors?.map((author) => {
|
||||
return {
|
||||
'@type': 'Person',
|
||||
name: author.name
|
||||
};
|
||||
})
|
||||
})}</script>`}
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>{data.post?.title}</h1>
|
||||
<span class="meta">
|
||||
<span>{publishTime.setLocale('de-DE').toLocaleString(DateTime.DATE_FULL)}</span> ·
|
||||
<span
|
||||
>Ca. {data.post.reading_time || data.post.reading_time === 0
|
||||
? Math.max(1, data.post.reading_time) + ' Min.'
|
||||
: 'Unbekannte'} Lesezeit</span
|
||||
>
|
||||
·
|
||||
<span>
|
||||
{#each data.post.tags || [] as tag, i}
|
||||
{i > 0 ? ', ' : ''}<span>{tag.name}</span><!--href={`/posts/` + tag.slug}-->
|
||||
{/each}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
{#if data.post.feature_image}
|
||||
<div class="feature-image">
|
||||
<figure>
|
||||
<img src={data.post.feature_image} alt={data.post.feature_image_alt || 'Feature Image'} />
|
||||
<figcaption>{@html data.post.feature_image_caption||""}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="article">
|
||||
{@html data.post?.html}
|
||||
|
||||
<!--<pre>{JSON.stringify(data.post, null, 2)}</pre>-->
|
||||
</div>
|
||||
|
||||
<div class="authors">
|
||||
<hr />
|
||||
{#each data.post.authors || [] as author}
|
||||
<div class="author">
|
||||
<div class="pic">
|
||||
{#if author.profile_image}
|
||||
<img src={author.profile_image} alt={'Profilbild von ' + author.name} />
|
||||
{/if}
|
||||
{#if !author.profile_image}
|
||||
<div class="photo-placeholder" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="name">{author.name}</span>
|
||||
<p>{author.bio || ''}</p>
|
||||
<div class="links">
|
||||
{#if author.website}
|
||||
<a href={author.website}><Globe /> {new URL(author.website).host}</a>
|
||||
{/if}
|
||||
{#if author.twitter}
|
||||
<a href={'https://twitter.com/' + author.twitter} class="twitter"
|
||||
>{@html siTwitter.svg} {author.twitter}</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.article {
|
||||
:global {
|
||||
@include ghostContentContainer;
|
||||
}
|
||||
}
|
||||
|
||||
.authors {
|
||||
@include contentGrid;
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
margin-bottom: var(--gap);
|
||||
|
||||
> * {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pic {
|
||||
width: 125px;
|
||||
|
||||
img,
|
||||
.photo-placeholder {
|
||||
width: 125px;
|
||||
height: 125px;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
background-color: var(--color-dark-surface);
|
||||
}
|
||||
}
|
||||
|
||||
.meta {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
padding: var(--padding);
|
||||
.name {
|
||||
font-size: 2em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
gap: var(--gap);
|
||||
a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
:global(svg) {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin-right: var(--padding);
|
||||
}
|
||||
|
||||
&.twitter {
|
||||
:global(svg) {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
@include articleHeader;
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue