diff --git a/.pugrc.js b/.pugrc.js index 7183759..7581da0 100644 --- a/.pugrc.js +++ b/.pugrc.js @@ -5,21 +5,21 @@ module.exports = { app_name: "Fappen", manifest: require('./manifest.json'), navigation: { - Frontpage: "/pages/index.pug", - Stregsystem: "/pages/stregsystem.pug", - Songbook: "/pages/songbook/index.pug", - Events: "/pages/events.pug", - Links: "/pages/links.pug", - Offline: "/pages/offline.pug" + Frontpage: ["/pages/index.pug", "🏠", "Frontpage"], + Stregsystem: ["/pages/stregsystem.pug", "💵", "Browse our collection of wares"], + Songbook: ["/pages/songbook/index.pug", "🎼", "Browser our collection of songs"], + Events: ["/pages/events.pug", "📅", "List upcoming events"], + Links: ["/pages/links.pug", "🌐", "Look at nice links"], + Offline: ["/pages/offline.pug", "✈", "Enter offline-mode"], + TenFoot: ["/pages/tenfoot/toggle.pug", "➜]", "Toggle 10-foot display"], }, links: { - Slack: "https://fklubben.slack.com", - Discord: "https://discord.gg/6DBvANjs3g", - Facebook: "https://www.facebook.com/fklub", - Github: "https://github.com/f-klubben", - Fiki: "https://fklub.dk", - Stregsystem: "https://stregsystem.fklub.dk" - + Slack: ["https://fklubben.slack.com"], + Discord: ["https://discord.gg/6DBvANjs3g"], + Facebook: ["https://www.facebook.com/fklub"], + Github: ["https://github.com/f-klubben"], + Fiki: ["https://fklub.dk"], + Stregsystem: ["https://stregsystem.fklub.dk"] }, disable_worker: process.env.disable_worker === "true" || true, } diff --git a/components/base_layout.pug b/components/page_layout.pug similarity index 73% rename from components/base_layout.pug rename to components/page_layout.pug index d03056d..5adae03 100644 --- a/components/base_layout.pug +++ b/components/page_layout.pug @@ -17,7 +17,7 @@ html(lang="en") ul.border-outer each key in Object.keys(navigation) .border-inner - a(href=navigation[key]) + a(href=navigation[key][0]) li= key button.darkmode-button darkmode main @@ -28,4 +28,8 @@ html(lang="en") if hide_frit_fit !== true p.frit-fit #fritfit (for real) +scripts + script. + // Determine mode from localStorage (or default) + const tenFoot = localStorage.getItem('tenFoot') === '1'; + if (tenFoot) document.body.classList.add('ten-foot'); block scripts diff --git a/components/service_menu.pug b/components/service_menu.pug index fc5232a..59cde16 100644 --- a/components/service_menu.pug +++ b/components/service_menu.pug @@ -1,7 +1,12 @@ mixin menu(links, target) link(rel="stylesheet" href="/styles/service-menu.scss") - section.service-links.border-outer + link(rel="stylesheet" href="/styles/service-menu_10foot.scss") + section.service-menu.border-outer each key in Object.keys(links) - a(href=links[key] target=target) + a.service-link(href=links[key][0] target=target) + if links[key].length > 1 + .service-icon=links[key][1] .border-inner h3=key + if links[key].length > 2 + .service-description=links[key][2] diff --git a/components/tenfoot_layout.pug b/components/tenfoot_layout.pug new file mode 100644 index 0000000..48ed550 --- /dev/null +++ b/components/tenfoot_layout.pug @@ -0,0 +1,40 @@ +include core + +block pug_meta + +- const title = typeof page_title === 'string' ? `${page_title} - ${app_name}` : app_name; + +doctype html +html(lang="en") + +head(title) + link(rel="stylesheet" href="/styles/tenfoot.scss") + body + header.header-with-logo + .logo-title-container + a.logo-wrapper(href="/tenfoot/") + img.logo(src="/media/flogo_w.svg" alt="Logo") + .title-wrapper + h1= app_name + block title + + main + if show_load_indicator + #loading-indicator.spinner + img(width="200" src="/media/fstjerne.svg") + + .nav-hint + span Navigate with + kbd ↑ + kbd ↓ + kbd ← + kbd → + kbd Enter + block content + + footer + if hide_frit_fit !== true + p.frit-fit #fritfit (for real) + + +scripts + script(type="module" src="/scripts/tenfoot/tenfoot-navigation.ts") + block scripts \ No newline at end of file diff --git a/media/flogo_w.svg b/media/flogo_w.svg new file mode 100644 index 0000000..4d54b7d --- /dev/null +++ b/media/flogo_w.svg @@ -0,0 +1,77 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pages/events.pug b/pages/events.pug index a20f80d..7484deb 100644 --- a/pages/events.pug +++ b/pages/events.pug @@ -1,4 +1,4 @@ -extends ../components/base_layout +extends ../components/page_layout block pug_meta - const page_title = "Events"; diff --git a/pages/index.pug b/pages/index.pug index b3000b3..82781b6 100644 --- a/pages/index.pug +++ b/pages/index.pug @@ -1,4 +1,4 @@ -extends ../components/base_layout +extends ../components/page_layout include ../components/service_menu block content diff --git a/pages/links.pug b/pages/links.pug index 60cba4b..1b9f0af 100644 --- a/pages/links.pug +++ b/pages/links.pug @@ -1,4 +1,4 @@ -extends ../components/base_layout +extends ../components/page_layout include ../components/service_menu block content diff --git a/pages/offline.pug b/pages/offline.pug index 940a200..8a27199 100644 --- a/pages/offline.pug +++ b/pages/offline.pug @@ -1,4 +1,4 @@ -extends ../components/base_layout +extends ../components/page_layout block head link(rel="stylesheet", href="/styles/offline.scss") diff --git a/pages/songbook/index.pug b/pages/songbook/index.pug index 5dd0143..d0fd213 100644 --- a/pages/songbook/index.pug +++ b/pages/songbook/index.pug @@ -1,4 +1,4 @@ -extends ../../components/base_layout.pug +extends ../../components/page_layout block head link(rel="stylesheet" href="/styles/songbook.scss") script(type="module", src="/scripts/songbook.ts") diff --git a/pages/stregsystem.pug b/pages/stregsystem.pug index 35a6c3c..941c9c8 100644 --- a/pages/stregsystem.pug +++ b/pages/stregsystem.pug @@ -1,4 +1,4 @@ -extends ../components/base_layout +extends ../components/page_layout block pug_meta - const page_title = "Stregsystemet"; diff --git a/pages/tenfoot/index.pug b/pages/tenfoot/index.pug new file mode 100644 index 0000000..3615c74 --- /dev/null +++ b/pages/tenfoot/index.pug @@ -0,0 +1,9 @@ +extends ../../components/tenfoot_layout + +block title + h2 Welcome + .subtitle Select a service to continue + +block content + link(rel="stylesheet" href="/styles/menu10foot.scss") + section.menu10foot diff --git a/pages/tenfoot/toggle.pug b/pages/tenfoot/toggle.pug new file mode 100644 index 0000000..f053a2b --- /dev/null +++ b/pages/tenfoot/toggle.pug @@ -0,0 +1,13 @@ +include ../../components/core + +doctype html +html(lang="en") + body + script. + // Read current mode from localStorage + const tenFoot = localStorage.getItem('tenFoot') === '1'; + + localStorage.setItem('tenFoot', tenFoot ? '0' : '1'); + + // Redirect after flipping + window.location.href = '/'; diff --git a/scripts/tenfoot/menu.ts b/scripts/tenfoot/menu.ts new file mode 100644 index 0000000..22ac6be --- /dev/null +++ b/scripts/tenfoot/menu.ts @@ -0,0 +1,84 @@ +// Keyboard navigation for 10-foot interface +interface ServiceSelected { + service: string; +} + +export interface GotoPage { + navigateTo(page: string): Promise; +} + +export class TenFootMenu { + private cards: NodeListOf; + private nav: GotoPage; + private currentIndex: number; + + constructor(navigator: GotoPage, cardContainer: Element) { + this.nav = navigator; + this.cards = cardContainer.querySelectorAll('.menu10foot-card'); + this.currentIndex = 0; + this.init(); + } + + private init(): void { + console.log("Initialized Tenfoot Menu"); + this.setupKeyboardNavigation(); + this.setupClickHandlers(); + + // Focus first card on load + if (this.cards.length > 0) { + this.focusCard(0); + } + } + + private focusCard(index: number): void { + if (index >= 0 && index < this.cards.length) { + this.cards[index].focus(); + this.currentIndex = index; + } + } + + private setupKeyboardNavigation(): void { + document.addEventListener('keydown', (e: KeyboardEvent) => { + switch(e.key) { + case 'ArrowRight': + e.preventDefault(); + this.focusCard((this.currentIndex + 1) % this.cards.length); + break; + + case 'ArrowLeft': + e.preventDefault(); + this.focusCard((this.currentIndex - 1 + this.cards.length) % this.cards.length); + break; + + case 'Enter': + e.preventDefault(); + this.cards[this.currentIndex].click(); + break; + } + }); + } + + private setupClickHandlers(): void { + this.cards.forEach((card: HTMLElement, index: number) => { + card.addEventListener('click', async () => { + const link = card.dataset.link || ''; + const titleElement = card.querySelector('.service-title'); + const title = titleElement?.textContent || ''; + + console.log(`Selected: ${title} (${link})`); + + await this.nav.navigateTo(link); + }); + + card.addEventListener('focus', () => { + this.currentIndex = index; + + card.scrollIntoView({ + behavior: 'smooth', + inline: 'center', + block: 'nearest' + }); + }); + }); + } +} diff --git a/scripts/tenfoot/tenfoot-navigation.ts b/scripts/tenfoot/tenfoot-navigation.ts new file mode 100644 index 0000000..189110d --- /dev/null +++ b/scripts/tenfoot/tenfoot-navigation.ts @@ -0,0 +1,122 @@ +// Keyboard navigation for 10-foot interface + +import {TenFootMenu, GotoPage} from "./menu"; + +class TenFootNavigator implements GotoPage { + private currentPage: string; + + constructor() { + this.init(); + } + + private init(): void { + console.log("Loaded Tenfoot-navigation"); + + this.inputOverride(); + this.handleLoading(); + this.TryInstantiateMenu(); + } + + private inputOverride(): void { + document.addEventListener('keydown', (e: KeyboardEvent) => { + switch(e.key) { + case 'Enter': + break; + case 'Escape': + case 'BrowserBack': + e.preventDefault(); + break; + case 'MediaPlayPause': + break; + case 'MediaPlay': + break; + case 'MediaPause': + break; + case 'MediaStop': + break; + case 'MediaTrackNext': + break; + case 'MediaTrackPrevious': + break; + } + }); + } + + private handleLoading(): void { + const loadingIndicator = document.getElementById('loading-indicator'); + + if (loadingIndicator) { + window.addEventListener('load', () => { + setTimeout(() => { + loadingIndicator.style.display = 'none'; + }, 1500); + }); + } + } + + async navigateTo(page: string): Promise { + await this.LoadPage(page); + } + + async LoadPage(tenfoot_page: string) { + // Special case of navigation + if (tenfoot_page == "//"){ + window.location.href = "/"; + }else if (tenfoot_page == "/"){ + tenfoot_page = "index"; + } + + const app = document.getElementsByTagName('section')[0] + + try { + await fetch(`/${tenfoot_page}.html`) + .then(response => response.text()) + .then(html => { + // Parse the HTML string into a document + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + // Extract the
element + const mainContent = doc.querySelector('main'); + console.log(mainContent); + + if (mainContent) { + // Inject only the inner content of
+ app.innerHTML = mainContent.innerHTML; + } + } + ); + + this.TryInstantiateMenu(); + } catch (error) { + app.innerHTML = '

Page not found

'; + } + } + + TryInstantiateMenu(){ + // Try load 10foot-menu handler + const initMenu = () => { + const menuContainer = document.getElementsByClassName('menu10foot')[0]; + let nav = new TenFootMenu(this, menuContainer); + }; + + // TODO: mby doesn't work for loading subpages + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initMenu); + } else { + initMenu(); + } + } +} + +const initNavigation = () => { + let nav = new TenFootNavigator(); +}; + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initNavigation); +} else { + initNavigation(); +} + +export default TenFootNavigator; \ No newline at end of file diff --git a/styles/base_10foot.scss b/styles/base_10foot.scss new file mode 100644 index 0000000..2797db9 --- /dev/null +++ b/styles/base_10foot.scss @@ -0,0 +1,67 @@ +body.ten-foot { + // Variables + $primary-gradient-start: #1a1a2e; + $primary-gradient-end: #16213e; + $text-color: #ffffff; + $focus-color: #00d4ff; + + body { + background: linear-gradient(135deg, $primary-gradient-start 0%, $primary-gradient-end 100%); + color: $text-color; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + overflow-x: hidden; + max-height: 100vh; + display: flex; + flex-direction: column; + } + + main { + flex: 1; + padding-left: 100vw; // scuffed + display: flex; + flex-direction: column; + overflow: hidden; + //padding: 4rem; + } + + .center { + text-align: center; + } + + .flex-center { + align-content: center; + justify-content: center; + } + + /* + Header & navigation + */ + + header { + display: none; + } + + .spinner { + animation: rotation 5s infinite linear; + } + + @keyframes rotation { + from { + transform: rotate(0deg); + } + to { + transform: rotate(359deg); + } + } + + /* Border style simulating html tables */ + .border-outer { + border: inherit !important; + } + + .border-inner { + margin: inherit !important; + border: inherit !important; + } +} + diff --git a/styles/menu10foot.scss b/styles/menu10foot.scss new file mode 100644 index 0000000..e07a824 --- /dev/null +++ b/styles/menu10foot.scss @@ -0,0 +1,92 @@ +// ten-foot.scss +// Variables +$primary-gradient-start: #1a1a2e; +$primary-gradient-end: #16213e; +$text-color: #ffffff; +$card-bg: rgba(255, 255, 255, 0.1); +$card-bg-hover: rgba(255, 255, 255, 0.15); +$card-border: rgba(255, 255, 255, 0.2); +$card-border-hover: rgba(255, 255, 255, 0.4); +$focus-color: #00d4ff; + +// Mixins +@mixin card-shadow { + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); +} + +@mixin text-shadow { + text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); +} + +@mixin smooth-transition($properties: all, $duration: 0.3s) { + transition: $properties $duration ease; +} + +// Service Grid +.menu10foot { + flex: 1; + display: flex; + align-items: center; + gap: 3rem; + + overflow-x: auto; + overflow-y: hidden; + + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + padding-inline: 3rem; + + /* IE / Edge Legacy */ + -ms-overflow-style: none; +} + +.menu10foot::-webkit-scrollbar { + display: none; /* Chrome, Safari, Edge */ +} + +.menu10foot-card { + flex: 0 0 auto; // prevents shrinking + margin: 0; + + display: flex; + flex-direction: column; + align-items: center; + gap: 2em; + + text-align: center; + background: $card-bg; + backdrop-filter: blur(10px); + border-radius: 2rem; + padding: 3em; + @include smooth-transition; + border: 2px solid $card-border; + cursor: pointer; + font-size: clamp(0.1rem, 1vw, 6rem); + height: 25em; + width: 25em; + scroll-margin-inline: 30rem; + + &:hover, + &:focus { + transform: scale(1.05); + background: $card-bg-hover; + border-color: $card-border-hover; + @include card-shadow; + } + + .menu10foot-icon { + font-size: 6em; + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3)); + } + + .menu10foot-title { + font-size: 3em; + font-weight: 600; + } + + .menu10foot-description { + font-size: 1.8em; + opacity: 0.9; + line-height: 1.6; + } +} diff --git a/styles/service-menu.scss b/styles/service-menu.scss index 33e4124..15e1f26 100644 --- a/styles/service-menu.scss +++ b/styles/service-menu.scss @@ -1,4 +1,4 @@ -.service-links { +.service-menu { width: 50%; box-sizing: border-box; text-align: center; @@ -7,4 +7,8 @@ .service-body { padding: 1em; +} + +.service-icon, .service-description { + display: none; } \ No newline at end of file diff --git a/styles/service-menu_10foot.scss b/styles/service-menu_10foot.scss new file mode 100644 index 0000000..f9fb24c --- /dev/null +++ b/styles/service-menu_10foot.scss @@ -0,0 +1,105 @@ +// ten-foot.scss +// Variables +$primary-gradient-start: #1a1a2e; +$primary-gradient-end: #16213e; +$text-color: #ffffff; +$card-bg: rgba(255, 255, 255, 0.1); +$card-bg-hover: rgba(255, 255, 255, 0.15); +$card-border: rgba(255, 255, 255, 0.2); +$card-border-hover: rgba(255, 255, 255, 0.4); +$focus-color: #00d4ff; + +// Mixins +@mixin card-shadow { + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); +} + +@mixin text-shadow { + text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); +} + +@mixin smooth-transition($properties: all, $duration: 0.3s) { + transition: $properties $duration ease; +} + +// Only apply rules on 10-foot mode +body.ten-foot { + // Service Grid + .service-menu { + flex: 1; + display: flex; + align-items: center; + gap: 3rem; + + overflow-x: auto; + overflow-y: hidden; + + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + padding-inline: 3rem; + + /* IE / Edge Legacy */ + -ms-overflow-style: none; + } + + .service-menu::-webkit-scrollbar { + display: none; /* Chrome, Safari, Edge */ + } + + .service-menu > a { + // Un-linkify + display: block; + text-decoration: none; + color: inherit; + } + + .service-menu > .service-link { + flex: 0 0 auto; // prevents shrinking + margin: 0; + + display: flex; + flex-direction: column; + align-items: center; + gap: 2em; + + text-align: center; + background: $card-bg; + backdrop-filter: blur(10px); + border-radius: 2rem; + padding: 3em; + @include smooth-transition; + border: 2px solid $card-border; + cursor: pointer; + font-size: clamp(0.1rem, 1vw, 6rem); + height: 25em; + width: 25em; + scroll-margin-inline: 30rem; + + &:hover, + &:focus { + transform: scale(1.05); + background: $card-bg-hover; + border-color: $card-border-hover; + @include card-shadow; + } + + div > h3 { + h3 { + font-size: 3em; + font-weight: 600; + } + } + + .service-icon { + font-size: 6em; + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3)); + } + + .service-description { + font-size: 1.8em; + opacity: 0.9; + line-height: 1.6; + } + + } +} diff --git a/styles/tenfoot.scss b/styles/tenfoot.scss new file mode 100644 index 0000000..16e1667 --- /dev/null +++ b/styles/tenfoot.scss @@ -0,0 +1,174 @@ +// ten-foot.scss +// Variables +$primary-gradient-start: #1a1a2e; +$primary-gradient-end: #16213e; +$text-color: #ffffff; +$focus-color: #00d4ff; + +// Mixins +@mixin card-shadow { + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4); +} + +@mixin text-shadow { + text-shadow: 0 4px 8px rgba(0, 0, 0, 0.5); +} + +@mixin smooth-transition($properties: all, $duration: 0.3s) { + transition: $properties $duration ease; +} + +// Reset +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +// Body +html { + background: linear-gradient(135deg, $primary-gradient-start 0%, $primary-gradient-end 100%); + color: $text-color; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + overflow-x: hidden; + max-height: 100vh; + display: flex; + flex-direction: column; +} + +main { + flex: 1; + padding-left: 100vw; // scuffed + display: flex; + flex-direction: column; + overflow: hidden; + //padding: 4rem; +} + +// Header with logo +.header-with-logo { + padding: 1.5em; + + .logo-title-container { + display: flex; + align-items: center; + gap: 2em; + } + + .logo-wrapper { + flex-shrink: 0; + + .logo { + width: 8em; + height: 8em; + filter: drop-shadow(0 4px 12px rgba(255, 255, 255, 0.3)); + @include smooth-transition(transform); + + &:hover { + transform: scale(1.05); + } + } + } + + .title-wrapper { + flex: 1; + + h1 { + font-size: 3rem; + font-weight: 700; + @include text-shadow; + line-height: 1.2; + } + + .subtitle { + font-size: 2rem; + opacity: 0.8; + font-weight: 300; + } + } +} + +// Header +header { + height: auto !important; + font-size: clamp(0.1rem, 2vh, 6rem); + + h1 { + font-size: 5em; + font-weight: 700; + margin-bottom: 0.2em; + @include text-shadow; + } + + .subtitle { + font-size: 2.5em; + opacity: 0.8; + font-weight: 300; + } +} + +// Footer +footer { + padding: 2rem; + margin-top: auto; +} +.frit-fit { + text-align: left; + font-size: 1.5em; + opacity: 0.6; +} + +// Focus indicators for accessibility +*:focus { + outline: 4px solid $focus-color; + outline-offset: 4px; +} + +// Navigation hint +.nav-hint { + position: fixed; + bottom: 2rem; + right: 2rem; + font-size: 1.5rem; + opacity: 0.5; + display: flex; + gap: 1rem; + align-items: center; + + kbd { + background: rgba(255, 255, 255, 0.2); + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-family: monospace; + font-size: 1.3rem; + } +} + + +// Loading Indicator +#loading-indicator { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + + &.spinner { + animation: spin 2s linear infinite; + } + + img { + width: 200px; + height: 200px; + filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.5)); + } +} + +@keyframes spin { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } +} \ No newline at end of file diff --git a/util/template/song.pug b/util/template/song.pug index 05c2a6c..ce89c4b 100644 --- a/util/template/song.pug +++ b/util/template/song.pug @@ -1,4 +1,4 @@ -extends ../../../components/base_layout +extends ../../../components/page_layout block head link(rel="stylesheet" href="/styles/songbook.scss") link(rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css")