Create CCCHH theme

This commit is contained in:
jtbx 2023-10-02 12:08:18 +02:00
parent 8394b08524
commit 2d267ba9fe
23 changed files with 629 additions and 1 deletions

View file

@ -22,4 +22,42 @@ All other information like details on groups, projects and recurring events shou
## Usage ## Usage
TODO ### Build and Deploy
Running the hugo command without and parameters will re-generate the site in the `public` directory.
To deploy the website, just copy the whole folder to a directory which is servered by the webserver of your preference.
Please note, that the website should be re-deployed at least daily to update the "announcement" section on the front page even if there were no changed to the content.
### Add Pages
To run a local version, just install HUGO by following the [instructions](https://gohugo.io/installation/) for your operating system.
To build the website and run a development webserver, execute:
```shell
hugo server
```
To also build posts in the future or in draft state, run this instead:
```shell
hugo server -D
```
#### Add an Event Announcement
There are two basic types of posts: Events and articles.
Events will be shown on the home page from their publishing date until they have happened.
To create a new event blog post, run a command like this:
```shell
hugo new content --king event blog/your-event-title.md
```
#### Add a Blog Entry
As mentioned before, you can also create blog posts for things which aren't events.
They will only be shown in the "blog" section and posted to the RSS feeds.
To create a new general blog post, run a command like this:
```shell
hugo new content --king article blog/your-article-title.md
```

8
content/_index.md Normal file
View file

@ -0,0 +1,8 @@
---
title: "Home"
date: 2022-11-20T09:03:20-08:00
draft: false
---
TODO: Hallo, wir sind der CCC Hamburg...
TOOD: Was wir machen und wer wir sind

7
content/blog/_index.md Normal file
View file

@ -0,0 +1,7 @@
---
title: "Blog"
draft: false
menu: main
---
TODO: Provide filters by type (announcement, post)

4
hugo.toml Normal file
View file

@ -0,0 +1,4 @@
baseURL = 'https://hamburg.ccc.de/'
languageCode = 'de-de'
title = 'CCC Hansestadt Hamburg e.V.'
theme = 'ccchh'

20
themes/ccchh/LICENSE Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2023 YOUR_NAME_HERE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,8 @@
---
kind: article
title: '{{ replace .File.ContentBaseName `-` ` ` | title }}'
date: '{{ .Date }}'
draft: true
---
TODO: Write your blog post

View file

@ -0,0 +1,5 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

View file

@ -0,0 +1,9 @@
---
kind: event
title: '{{ replace .File.ContentBaseName `-` ` ` | title }}'
date: '{{ .Date }}' # date of the event
publishDate: '{{ .Date }}' # when to publish
draft: true
---
TODO: Describe your event here

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 86 KiB

View file

@ -0,0 +1,84 @@
import timeDistance, { deLocale } from 'js/util/timeDistance'
const spaceapiUrl = "https://www.hamburg.ccc.de/dooris/status.json"
const interval_ms = 60000
const classNameOpen = "open"
const classNameClosed = "closed"
function updateRoomState(openState, lastChangeTimestamp) {
console.debug("SpaceAPI: Setting room " + openState + " since " + lastChangeTimestamp)
const roomstate = document.querySelector('#roomstate')
var statusItem = roomstate.querySelector(".state")
var durationItem = roomstate.querySelector(".duration")
if (openState === null || lastChangeTimestamp === null) {
console.warn("SpaceAPI: Room state unknown");
statusItem.textContent = "unbekannt"
roomstate.classList.remove(classNameOpen)
roomstate.classList.remove(classNameClosed)
durationItem.textContent = "(seit unbekannt)"
} else {
if (openState) {
statusItem.textContent = "offen"
roomstate.classList.remove(classNameClosed)
roomstate.classList.add(classNameOpen)
} else {
statusItem.textContent = "geschlossen"
roomstate.classList.remove(classNameOpen)
roomstate.classList.add(classNameClosed)
}
const changeDate = new Date(lastChangeTimestamp * 1000)
const changeText = timeDistance(deLocale).inWords(changeDate)
durationItem.textContent = "(" + changeText + ")"
}
}
function parseResponse(apiJson) {
console.debug('SpaceAPI got:', apiJson)
if (typeof apiJson.api === "undefined") {
console.error("SpaceAPI JSON invalid: 'api' item not present")
return [null, null]
}
if (typeof apiJson.state === "undefined") {
console.error("SpaceAPI JSON invalid: 'state' item not present")
return [null, null]
}
if (typeof apiJson.state.open === "undefined") {
console.error("SpaceAPI JSON invalid: 'state.open' item not present")
return [null, null]
}
console.debug('SpaceAPI state:', apiJson.state)
var openState = (apiJson.state.open == true)
var lastchange = null
if (apiJson.state.lastchange !== "undefined") {
lastchange = apiJson.state.lastchange
}
return [openState, lastchange]
}
function update() {
fetch(spaceapiUrl)
.then(resp => resp.json())
.then(function (data) {
var state = parseResponse(data)
updateRoomState(state[0], state[1])
})
.catch(err => { throw err });
}
window.onload = function () {
update()
window.setTimeout(update, interval_ms)
}

View file

@ -0,0 +1,104 @@
export default ((locale) => {
const self = {}
if (locale === undefined) {
self.locales = {
pastPrefix: '',
pastSufix: 'ago',
futurePrefix: 'in',
futureSufix: '',
seconds: '%p less than a minutei %s',
minute: '%p about a minute %s',
minutes: '%p %d minutes %s',
hour: '%p about an hour %s',
hours: '%p about %d%r hours %s',
day: '%p a day %s',
days: '%p %d%r days %s',
month: '%p about a month %s',
months: '%p %d%r months %s',
year: '%p about a year %s',
years: '%p %d%r years %s'
}
} else {
self.locales = locale
}
self.inWords = (timeDistance, start) => {
if (start === undefined) {
start = new Date()
} else {
start = start instanceof(Date) ? start : parseInt(start)
}
timeDistance = timeDistance instanceof(Date) ? timeDistance : parseInt(timeDistance)
const prefix = timeDistance < new Date() ? self.locales.pastPrefix : self.locales.futurePrefix
const sufix = timeDistance < new Date() ? self.locales.pastSufix : self.locales.futureSufix
let seconds = Math.abs(Math.floor((start - timeDistance) / 1000)),
interval = 0,
rest = 0,
intervals = {
year: seconds / 31536000,
month: seconds / 2592000,
day: seconds / 86400,
hour: seconds / 3600,
minute: seconds / 60
}
let distance = self.locales.seconds
let key
for (key in intervals) {
interval = Math.floor(intervals[key])
rest = intervals[key] - interval
if (interval >= 1) {
distance = self.locales[key + 's']
break
} else if (interval === 1) {
distance = self.locales[key]
break
}
}
if (rest > 0.5) {
distance = distance.replace(/%r/, "½")
} else if (rest > 0.25) {
distance = distance.replace(/%r/, "¼")
} else {
if (interval === 1) {
distance = self.locales[key]
}
distance = distance.replace(/\s*%r/, "")
}
distance = distance.replace(/%d/i, interval)
distance = distance.replace(/%p/i, prefix)
distance = distance.replace(/%s/i, sufix)
return distance.trim()
}
return self
})
export const deLocale = {
pastPrefix: 'seit',
pastSufix: '',
futurePrefix: 'in',
futureSufix: '',
seconds: '%p %d Sekunden',
minute: '%p einer Minute',
minutes: '%p %d Minuten',
hour: '%p einer Stunde',
hours: '%p %d%r Stunden',
day: '%p einem Tag',
days: '%p %d%r Tagen',
month: '%p einem Monat',
months: '%p %d%r Monaten',
year: '%p einem Jahr',
years: '%p %d%r Jahren'
}

View file

@ -0,0 +1,78 @@
$roomstate_color_unknown: #dda218;
$roomstate_color_open: var(--ins-color);
$roomstate_color_closed: var(--del-color);
// Room State in Menu
#roomstate {
font-size: 0.9em;
line-height: 1.0em;
max-width: 8em;
padding: 5px;
color: $roomstate_color_unknown;
&.open {
color: $roomstate_color_open;
}
&.closed {
color: $roomstate_color_closed;
}
span.duration {
font-size: 0.7em;
}
}
// CCCHH Icon in Menu
@media only screen and (prefers-color-scheme: light) {
.invert-on-light {
filter: invert(1);
}
}
// Home page Announcements
.announcement {
border-radius: var(--border-radius);
background: var(--code-background-color);
padding: 10px 15px;
margin-bottom: var(--typography-spacing-vertical);
p:last-child {
margin-bottom: 0;
}
}
@media only screen and (prefers-color-scheme: dark) {
.announcement {
background: var(--code-background-color);
}
}
// Home Friends&Family Gallery
.flex-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
div {
width: 250px;
// 2*250px + 2*var(--spacing) + 1px
@media only screen and (max-width: 533px) {
width: 150px;
}
}
}
.img-link {
a {
img {
display: block;
padding: var(--grid-spacing-horizontal);
margin: 0 auto;
}
}
}

1
themes/ccchh/hugo.toml Normal file
View file

@ -0,0 +1 @@
# Theme config.

View file

@ -0,0 +1,8 @@
{{ define "main" }}
<main class="container" aria-role="main">
<div>
<h1 id="title">404 Page not Found</h1>
<a href="{{ "" | relURL }}">Back Home</a>
</div>
</main>
{{ end }}

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
{{- partial "head.html" . }}
</head>
<body>
{{- partial "header.html" . }}
{{- block "main" . }}{{- end }}
{{- partial "footer.html" . }}
</body>
</html>

View file

@ -0,0 +1,13 @@
{{ define "main" }}
<main class="container" aria-role="main">
<header class="header">
<h1>{{ .Title }}</h1>
{{ with .Params.subtitle }}
<span class="subtitle">{{ . }}</span>
{{ end }}
</header>
<div class="content">
{{ .Content }}
</div>
</main>
{{ end }}

View file

@ -0,0 +1,33 @@
{{ define "main" }}
<main class="container" aria-role="main">
<header class="header">
<h1>{{ .Title }}</h1>
{{ with .Params.subtitle }}
<span class="subtitle">{{ . }}</span>
{{ end }}
</header>
<div class="content">
{{ .Content }}
</div>
<ul>
{{ range .Pages }}
<li>
<a href="{{ .Permalink }}">{{ .Date.Format "2006-01-02" }} | {{ .Title }}</a>
</li>
{{ end }}
</ul>
{{ range .Pages.GroupByParam "kind" }}
<h3>{{ .Key }}</h3>
<ul>
{{ range .Pages }}
<li>
<a href="{{ .Permalink }}">{{ .Date.Format "2006-01-02" }} | {{ .Title }}</a>
</li>
{{ end }}
</ul>
{{ end }}
<!-- TODO: Pagination: see https://gohugo.io/templates/pagination/ -->
</main>
{{ end }}

View file

@ -0,0 +1,50 @@
{{ define "main" }}
<main class="container" aria-role="main">
<!-- <header class="homepage-header">
<h1>{{ .Title }}</h1>
{{ with .Params.subtitle }}
<span class="subtitle">{{ . }}</span>
{{ end }}
</header> -->
<div class="homepage-content">
<!-- Note that the content for index.html, as a sort of list page, will pull from content/_index.md -->
{{ .Content }}
</div>
{{- $events := where (.Site.GetPage "blog").Pages ".Params.kind" "event" }}
{{- $upcoming := where $events ".Params.date" "ge" now }}
{{ if $upcoming}}
<div class="announcements">
<h3>Neuigkeiten</h3>
{{- range $upcoming.ByDate }}
<div class="announcement">
<p><a href="{{ .Permalink }}">{{ .Date.Format "2006-01-02" }}: {{ .Title }}</a>
{{ .Summary }}
{{- if .Truncated }}
<a href="{{ .RelPermalink }}">Read More…</a>
{{- end }}
</p>
</div>
{{- end }}
</div>
{{- end }}
{{- $home_sections := .Site.GetPage "/home" }}
{{- range sort ($home_sections.Resources.Match "*.md") ".Name" }}
<div class="section">
<h3>{{ .Title }}</h3>
{{ .Content -}}
{{- with .Params.Resources }}
<div class="flex-grid">
{{- range . }}
{{- $img := $home_sections.Resources.GetMatch (printf "%s" .src) }}
<div class="img-link"><a href="{{ .params.url }}"><img src="{{ $img.Permalink }}" class="box-image" alt=""></a></div>
{{- end }}
</div>
{{- end }}
</div>
{{ end }}
</main>
{{ end }}

View file

@ -0,0 +1,6 @@
<footer class="container">
<nav class="menu">
{{- partial "menu.html" (dict "menuID" "footer" "page" . ) -}}
</nav>
</footer>

View file

@ -0,0 +1,22 @@
{{- $options := dict "transpiler" "libsass" "targetPath" "css/style.css" -}}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
{{- with resources.GetRemote "https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css" }}
<link rel="stylesheet" href="{{ .RelPermalink }}" crossorigin="anonymous">
{{ end }}
{{ with resources.Get "sass/main.scss" | toCSS $options | minify | fingerprint -}}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end }}
<link rel="stylesheet" href="{{ .Site.BaseURL }}thirdparty/fontawesome6/css/all.min.css" crossorigin="anonymous">
{{ with resources.Get "js/roomstate.js" | js.Build | minify | fingerprint -}}
<script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
{{ range .AlternativeOutputFormats -}}
{{ printf `<link rel="%s" type="%s" href="%s" title="%s" />` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
{{ end -}}
<title>{{ .Title }}</title>
<!-- TODO: Favicon -->

View file

@ -0,0 +1,6 @@
<div class="container header">
<nav class="menu">
{{- partial "menu.html" (dict "menuID" "main" "page" . ) -}}
</nav>
</div>

View file

@ -0,0 +1,32 @@
{{- $page := .page -}}
{{- $menuID := .menuID -}}
{{- $menuContent := index site.Menus .menuID -}}
<ul>
{{- if compare.Eq $menuID "main" -}}
{{ $image := resources.Get "images/logo.svg" }}
<li><a href="/{{ .Site.Home.URL }}"><img src="{{ $image.RelPermalink }}" class="invert-on-light" width="150px" alt="CCCHH Icon"></a></li>
{{- end }}
{{- range $menuContent }}
<li>
{{- $attrs := dict "href" .URL }}
{{- if $page.IsMenuCurrent $menuID . -}}
{{- $attrs = merge $attrs (dict "class" "active" "aria-current" "page") }}
{{- end -}}
<a
{{- range $k, $v := $attrs }}
{{- with $v }}
{{- printf " %s=%q" $k $v | safeHTMLAttr }}
{{- end }}
{{- end -}}
>{{ or (T .Identifier) .Name | safeHTML }}</a></li>
{{- end }}
</ul>
{{- if compare.Eq $menuID "main" -}}
<ul>
<li class="roomstate" id="roomstate">
<span class="state">unbekannt</span><br><span class="duration">(seit&nbsp;)</span>
</li>
</ul>
{{- end }}

21
themes/ccchh/theme.toml Normal file
View file

@ -0,0 +1,21 @@
# theme.toml template for a Hugo theme
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
name = "CCCHH"
license = "MIT"
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE"
description = ""
# homepage = "http://example.com/"
tags = []
features = []
min_version = "0.114.1"
[author]
name = ""
homepage = ""
# If porting an existing theme
[original]
name = ""
homepage = ""
repo = ""