Merge Edge with sort and resize #2

Merged
alfred merged 64 commits from edge into master 2022-07-28 01:51:14 +01:00
16 changed files with 885 additions and 75 deletions

8
.gitignore vendored
View File

@ -7,3 +7,11 @@ dist/
# Test data and logs folders # Test data and logs folders
.data/ .data/
.idea/dataSources.xml .idea/dataSources.xml
# CDN link
cdn/
cdn
# Config Link
cnf/
cnf

View File

@ -34,4 +34,8 @@ clean:
deploy: build deploy: build
sudo systemctl stop wappcityuni sudo systemctl stop wappcityuni
sudo cp "${BIN}" /usr/local/bin sudo cp "${BIN}" /usr/local/bin
sudo cp *.go.html cnf
sudo cp *.go.yml cnf
sudo cp *.css cdn
sudo cp *.js cdn
sudo systemctl start wappcityuni sudo systemctl start wappcityuni

241
base.css Normal file
View File

@ -0,0 +1,241 @@
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
main{
padding-top: 90px;
padding-left: 6px;
}
.no-dec{
text-decoration: none;
}
.no-lst-style{
list-style: none;
}
.centered{
text-align: center;
}
.content > p{
word-break: break-word;
-ms-word-wrap: break-word;
word-wrap: break-word;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.header{
position: fixed;
top: 0;
width: 100%;
}
.home-button, .sort-button{
display: inline-block;
width: 64px;
height: 82px;
overflow: hidden;
text-align: center;
}
.sort-button{
cursor: pointer;
}
.home-button > div, .sort-button > div{
display: inline;
font-size: 60px;
padding: 10px;
vertical-align: middle;
}
.nav{
width: 100%;
height: 100%;
overflow: hidden;
max-height: 0;
}
.so-pane{
display: none;
overflow: hidden;
position: fixed;
max-height: 0;
}
.so-pane > *{
vertical-align: middle;
font-size: 16px;
text-align: left;
padding: 2px;
}
.so-pane > label{
background-color: transparent;
}
.nav-menu, .sort-menu, .data-hold{
display: none;
}
.menu a{
display: block;
padding: 32px 18px;
}
.hmb{
cursor: pointer;
float: right;
background-color: transparent;
padding: 40px 20px;
}
.hmb-line{
display: block;
height: 2px;
position: relative;
width: 24px;
}
.hmb-line::before, .hmb-line::after{
content: '';
display: block;
height: 100%;
position: absolute;
transition: all .1s ease-out;
width: 100%;
}
.hmb-line::before{
top: 5px;
}
.hmb-line::after{
top: -5px;
}
.nav-menu:checked ~ nav{
max-height: 100%;
}
.nav-menu:checked ~ .hmb .hmb-line{
background: transparent;
}
.nav-menu:checked ~ .hmb .hmb-line::before{
transform: rotate(-45deg);
top:0;
}
.nav-menu:checked ~ .hmb .hmb-line::after{
transform: rotate(45deg);
top:0;
}
.sort-menu:checked ~ .so-pane{
display: block;
box-sizing: content-box;
position: fixed;
top: 85px;
max-height: 100%;
width: auto;
height: auto;
padding: 4px;
text-align: center;
border-style: solid;
border-width: 2px;
}
.main-box{
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-webkit-box-direction: normal;
-moz-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-align-content: center;
align-content: center;
width: 100%;
}
.main-box > div, footer > p{
margin: 3px 0;
padding: 2px;
box-sizing: content-box;
}
.item-table{
display: table;
width: 100%;
box-sizing: content-box;
background-color: transparent;
}
.item-table > div{
display: table-row;
background-color: transparent;
box-sizing: inherit;
}
.item-table > div > div > div{
padding: 2px;
}
.item-table-caption{
display: table-caption !important;
caption-side: bottom;
}
.item-table-full, .item-table-360, .item-table-caption{
border-style: solid;
border-width: 1px;
background-color: transparent;
box-sizing: inherit;
}
.item-table-full{
display: table-cell;
vertical-align: top;
width: 100%;
}
.item-table-360{
display: none;
}
.image-box{
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-box-orient: horizontal;
-moz-box-orient: horizontal;
-webkit-box-direction: normal;
-moz-box-direction: normal;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-wrap: wrap;
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-justify-content: center;
justify-content: center;
}
.image-box > a{
border-style: solid;
border-width: 1px;
margin: 2px;
}
@media (min-width: 560px){
.main-box > div, footer > p{
margin: 5px;
}
.item-table-360{
display: table-cell;
vertical-align: middle;
width: 360px;
overflow: hidden;
}
.image-box > a{
border-width: 4px;
margin: 10px;
}
.small-only-row{
display: none !important;
}
}
@media (min-width: 640px){
.nav{
max-height: none;
top: 0;
position: relative;
float: right;
width: auto;
}
.menu li{
float: left;
}
.menu a:hover{
background-color: transparent;
}
.hmb{
display: none;
}
}

38
dark.css Normal file
View File

@ -0,0 +1,38 @@
body{
color: #f9f9f9;
background-color: #050506;
border-color: #696969;
}
a{
color: #b0b0f0;
}
.header, .nav, footer, .so-pane{
background-color: #1d1d1e;
}
.home-button > div, .sort-button > div, .menu a, .so-pane > *{
color: #e0e0e0;
}
.home-button:hover, .menu a:hover, .hmb:hover, .sort-button:hover, .sort-menu:checked ~ .sort-button, .so-pane > input, .so-pane > select{
background-color: #606061;
}
.hmb-line, .hmb-line::before, .hmb-line::after{
background: #e0e0e0;
}
.main-box{
background-color: #0f0f0f;
}
.item-table{
background-color: #3f3f3f;
}
.so-pane{
border-color: #3f3f3f;
}
.item-table > div > div, .item-table-caption{
border-color: #f5deb3;
}
.image-box{
background-color: #4f4f4f;
}
.image-box > a{
border-color: #b0b0f0;
}

View File

@ -13,28 +13,25 @@
text-align: center; text-align: center;
background-color: mediumslateblue; background-color: mediumslateblue;
} }
table, th, td { table, th, td {
margin: auto; margin: auto;
text-align: left; text-align: left;
border: black 1px solid; border: black 1px solid;
border-collapse: collapse; border-collapse: collapse;
word-break: break-word; word-break: break-word;
-ms-word-wrap: break-word;
word-wrap: break-word;
} }
table, td { table, td {
background-color: lightgray; background-color: lightgray;
} }
table { table {
width: 80%; width: 80%;
} }
th { th {
background-color: lightsteelblue; background-color: lightsteelblue;
width: 25%; width: 25%;
} }
td { td {
width: 75%; width: 75%;
} }

196
index.go.html Normal file
View File

@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>City University Portfolio</title>
<link rel="stylesheet" href="{{ .Data.CSSBaseURL }}"/>
{{ if .Light }}
<link rel="stylesheet" href="{{ .Data.CSSLightURL }}"/>
{{ else }}
<link rel="stylesheet" href="{{ .Data.CSSDarkURL }}"/>
{{ end }}
<script src="{{ .Data.JScriptURL }}"></script>
</head>
<body>
<header class="header">
{{ if .Light }}
<a href="?light" class="home-button no-dec" title="Home" id="logo"><div><img src="{{ .Data.LogoImageLocation }}" width="64px" alt="&#8962;"></div></a>
<a href="?{{ .Parameters }}" class="home-button no-dec" title="Switch to Dark Mode" id="theme"><div><img src="{{ .Data.MoonImageLocation }}" width="64px" alt='{{ "{" }}'></div></a>
{{ else }}
<a href="?" class="home-button no-dec" title="Home" id="logo"><div><img src="{{ .Data.LogoImageLocation }}" width="64px" alt="&#8962;"></div></a>
{{ if eq .Parameters "" }}
<a href="?light" class="home-button no-dec" title="Switch to Light Mode" id="theme"><div><img src="{{ .Data.SunImageLocation }}" width="64px" alt='()'></div></a>
{{ else }}
<a href="?light&{{ .Parameters }}" class="home-button no-dec" title="Switch to Light Mode" id="theme"><div><img src="{{ .Data.SunImageLocation }}" width="64px" alt='()'></div></a>
{{ end }}
{{ end }}
<input class="sort-menu" type="checkbox" id="sort-menu"/>
<label class="sort-button no-dec" for="sort-menu" title="Order and Sort Options"><div><img src="{{ .Data.SortImageLocation }}" width="64px" alt='&#8595;'></div></label>
<form class="so-pane" action="?" method="get" id="so-form">
{{ if .Light }}
<input id="so-theme" type="hidden" name="light" />
{{ end }}
{{ $sort := 0 }}
<label class="no-dec" for="so-order">Order by:</label>
<select name="order" id="so-order">
{{ if eq .OrderStartDate 0 }}
<option value="start">Start Date</option>
{{ else }}
<option value="start" selected>Start Date</option>
{{ $sort = .OrderStartDate }}
{{ end }}
{{ if eq .OrderEndDate 0 }}
<option value="end">End Date</option>
{{ else }}
<option value="end" selected>End Date</option>
{{ $sort = .OrderEndDate }}
{{ end }}
{{ if eq .OrderName 0 }}
<option value="name">Name</option>
{{ else }}
<option value="name" selected>Name</option>
{{ $sort = .OrderName }}
{{ end }}
{{ if eq .OrderDuration 0 }}
<option value="duration">Duration</option>
{{ else }}
<option value="duration" selected>Duration</option>
{{ $sort = .OrderDuration }}
{{ end }}
</select>
<label class="no-dec" for="so-sort">Sort:</label>
<select name="sort" id="so-sort">
{{ if gt $sort 0 }}
<option value="asc" selected>Ascending</option>
{{ else }}
<option value="asc">Ascending</option>
{{ end }}
{{ if lt $sort 0 }}
<option value="desc" selected>Descending</option>
{{ else }}
<option value="desc">Descending</option>
{{ end }}
</select>
<input id="so-submit" type="submit" value="Commit">
</form>
<input class="nav-menu" type="checkbox" id="nav-menu"/>
<label class="hmb" for="nav-menu" title="Navigation Links"><span class="hmb-line"></span></label>
<nav class="nav" id="nav">
<ul class="menu no-lst-style">
{{ range .Data.GetHeaderLabels }}
<li><b><a href="{{ $.Data.GetHeaderLink . }}" class="no-dec" title="{{ . }}">{{ . }}</a></b></li>
{{ end }}
</ul>
</nav>
</header>
<main class="main-box">
<div>
<div class="item-table">
<div>
<div class="item-table-full">
<div class="centered"><h1>{{ .Data.About.Title }}</h1></div>
</div>
<div class="item-table-360">
<div class="centered"><h4>Email: <a href="mailto:{{ .Data.About.ContactEmail }}">{{ .Data.About.ContactEmail }}</a></h4></div>
</div>
</div>
<div class="small-only-row">
<div class="item-table-full">
<div class="centered"><h4>Email: <a href="mailto:{{ .Data.About.ContactEmail }}">{{ .Data.About.ContactEmail }}</a></h4></div>
</div>
</div>
<div>
<div class="item-table-full">
<div class="content">{{ .Data.About.GetContent }}</div>
</div>
<div class="item-table-360">
<div><a href="{{ .Data.About.ImageLocation }}"><img src="{{ .Data.About.ThumbnailLocation }}" alt="{{ .Data.About.ImageAltText }}" title="{{ .Data.About.ImageAltText }}" width="360px"></a></div>
</div>
</div>
<div class="small-only-row">
<div class="item-table-full">
<div class="centered"><a href="{{ .Data.About.ImageLocation }}"><img src="{{ .Data.About.ThumbnailLocation }}" alt="{{ .Data.About.ImageAltText }}" title="{{ .Data.About.ImageAltText }}" width="360px"></a></div>
</div>
</div>
</div>
</div>
{{ range .GetEntries }}
<div>
<div class="item-table">
<div>
<div class="item-table-full">
<div class="centered"><h1>{{ .Name }}</h1></div>
</div>
<div class="item-table-360">
{{ if eq .GetStartDate .GetEndDate }}
<div class="centered"><h4>{{ .GetStartDate }}</h4></div>
{{ else }}
<div class="centered"><h4>{{ .GetStartDate }} - {{ .GetEndDate }}</h4></div>
{{ end }}
</div>
</div>
<div class="small-only-row">
<div class="item-table-full">
{{ if eq .GetStartDate .GetEndDate }}
<div class="centered"><h4>{{ .GetStartDate }}</h4></div>
{{ else }}
<div class="centered"><h4>{{ .GetStartDate }} - {{ .GetEndDate }}</h4></div>
{{ end }}
</div>
</div>
<div>
<div class="item-table-full">
<div class="content">{{ .GetContent }}</div>
</div>
<div class="item-table-360">
<div>
{{ if eq .VideoLocation "" }}
<img src="{{ $.Data.NoVideoImageLocation }}" alt="No Video" width="360px">
{{ else }}
<video controls width="360px">
<source src="{{ .VideoLocation }}" type="{{ .VideoContentType }}">
<a href="{{ .VideoLocation }}">The Video</a>
</video>
{{ end }}
</div>
</div>
</div>
<div class="small-only-row">
<div class="item-table-full">
<div class="centered">
{{ if eq .VideoLocation "" }}
<img src="{{ $.Data.NoVideoImageLocation }}" alt="No Video" width="360px">
{{ else }}
<video controls width="360px">
<source src="{{ .VideoLocation }}" type="{{ .VideoContentType }}">
<a href="{{ .VideoLocation }}">The Video</a>
</video>
{{ end }}
</div>
</div>
</div>
{{ if not (eq .GetImageCount 0) }}
<div class="item-table-caption">
<div class="image-box">
{{ range .GetImages }}
<a href="{{ .ImageLocation }}"><img src="{{ .ThumbnailLocation }}" alt="{{ .ImageAltText }}" title="{{ .ImageAltText }}" width="240px"></a>
{{ end }}
</div>
</div>
{{ end }}
</div>
<div class="data-hold">{{ .GetInt64Duration }}</div>
<div class="data-hold">{{ .GetStartDateHTML }}</div>
<div class="data-hold">{{ .GetEndDateHTML }}</div>
</div>
{{ end }}
</main>
<footer>
<p>
Looking for the old static HTML page, here's the <a href="index.html">link</a>.
</p>
</footer>
</body>
</html>

160
index.go.yml Normal file
View File

@ -0,0 +1,160 @@
#This file is (C) Captain ALM
#Under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
cssBaseURL: "https://cityuni.captainalm.com/resources/assets/base.css"
cssDarkURL: "https://cityuni.captainalm.com/resources/assets/dark.css"
cssLightURL: "https://cityuni.captainalm.com/resources/assets/light.css"
jScriptURL: "https://cityuni.captainalm.com/resources/assets/index.js"
noVideoImageLocation: "https://cityuni.captainalm.com/resources/assets/novideo.png"
logoImageLocation: "https://cityuni.captainalm.com/resources/assets/logo.png"
moonImageLocation: "https://cityuni.captainalm.com/resources/assets/moon.png"
sunImageLocation: "https://cityuni.captainalm.com/resources/assets/sun.png"
sortImageLocation: "https://cityuni.captainalm.com/resources/assets/sort.png"
headerLinks:
Main Portfolio: "https://portfolio.captainalm.com/"
Root Site Home: "https://www.captainalm.com/"
Github: "https://github.com/Captain-ALM/"
about:
title: "Alfred Manville (Captain ALM)"
content: >
<p>
Hello, I'm Alfred Manville (#age# Years Old).
I'm a free and open-source developer who enjoys networking my laptops together,
writes network software to communicate between them and then tries to break said software.
I also have a <a href="https://youtube.com/c/CaptainALM">Youtube Channel</a> which is in the process of being resumed from a hiatus.
</p>
<p>
On the programming side, I know Visual Basic .net, C# .net, C, Java, Go, Javascript (Circa. 2000), Processing and Microsoft Smallbasic
(I have also dabbled in C++, Python and Bash/Batch).
I am currently in the progress of writing infrastructure software in Go, in the past, I wrote a command console in VB .net for my own
pluggable libraries (I created a CMD emulator to get past the school disabling interactive CMD) and some network communication applications
(Including a peer-to-peer VOIP client using NAudio as the audio library and my own network wrapper library).
</p>
<p>
On the cracking / hacking side, I've used virtual machines a lot (Mainly infrastructure testing
but I did at one point try creating and breaking into a test windows domain network I had setup).
I've also used VMs to pull perform a PXE boot off a network and analyse the partially deployed image
(And this is why the deployment servers should be switched off when unneeded, especially since PXE
auto-domain-join would store its credentials in the image).
I also played around with accessing the RDP servers when I was in secondary school (Turns out remote apps
is just a glorified application auto-launcher with window size detection).
</p>
<p>
I also <a href="https://subsection.captainalm.com/">bake bread</a> although this sub-site is still under construction (Mostly learnt from my grandma).
I also play video-games and am an expert at using lower-end hardware.
</p>
<p>
I used to do Karate (Kyokushin Brown Belt) and I wish I could still fit my bike.
My <a href="https://cdn.captainalm.com/download/keys/alfred@captainalm.com.asc">GPG Key</a> for my email address.
</p>
thumbnailLocation: "https://cityuni.captainalm.com/resources/assets/imageofyou_t.jpg"
imageLocation: "https://cityuni.captainalm.com/resources/assets/imageofyou.jpg"
imageAltText: "Image of me."
birthYear: 2002
contactEmail: "alfred@captainalm.com"
entries:
- name: "Bootcamp 2021: Ninjaformer GUI"
content: >
<p>
My first programming task at City, concluding the 2 week 2021 Programming Bootcamp,
although I have only spent 3 days programming this and was a tad bit too ambitious.
(I could have started earlier though)
</p>
<p>
This Processing project show that I can use arrays, loops, mouse and keyboard interaction and geometric transforms.
The project contains a GUI library that I made to create the menu system for what could have been the Ninjaformer game.
</p>
<p>
Unfortunately, while the code for loading tile, sprite and level information exists (JSON, sprite sheet support);
I ran out of time before the submission to actually even start on the game. But you can play around with the main code and build your own GUIs too so...
</p>
<p>
Here is the repo: <a href="https://github.com/Captain-ALM/Ninjaformer-Processing">https://github.com/Captain-ALM/Ninjaformer-Processing</a>
</p>
startDate: "01/10/2021"
endDate: "31/10/2021"
videoLocation: "https://cityuni.captainalm.com/resources/stream/vid1.mp4"
videoContentType: "video/mp4"
thumbnailLocations:
- "https://cityuni.captainalm.com/resources/assets/pic1_t.png"
- "https://cityuni.captainalm.com/resources/assets/pic2_t.png"
- "https://cityuni.captainalm.com/resources/assets/pic3_t.png"
imageLocations:
- "https://cityuni.captainalm.com/resources/assets/pic1.png"
- "https://cityuni.captainalm.com/resources/assets/pic2.png"
- "https://cityuni.captainalm.com/resources/assets/pic3.png"
imageAltTexts:
- "Level select screen."
- "Empty content interface (Gameplay)."
- "Level editor screen."
- name: "City Game Project 2022: Ninjaformer (Alpha, Beta)"
content: >
<p>
My first major project at City (A Java Game), concluding 2.2 Months of programming.
This game uses the University provided game library (Which is just JBox2D extended).
</p>
<p>
Looking back on this, I wish I started earlier (Like January) that way I could have implemented all the features I wanted.
This project allows for levels to be designed within the program and allows them to be edited as XML outside the program.
</p>
<p>
The code is extensible and it is relatively straight forward to implement new features. There are a few bugs that can crop up
(Such as sticking to surfaces due to ground body updates) but I already know ways to fix them.
This project relies on part of a GUI library I built in it and I had to modify the CityGame library by extending it.
The audio and assets were also created by me, although they're a bit amateurish as I'm a computer scientist not an artist!
</p>
<p>
This game is designed to be a story based game... The tutorial level at the beginning of the game is the dream in which the main
character dreams of being a ninja, this allows for the player to learn the controls and basic mechanics of the game.
The next level is the training level in which the ninja trains within a monastery.
The final level allows the ninja to "complete" the game while exploring a set of caves.
</p>
<p>
Here is the repo: <a href="https://github.com/cityteaching/citygame2122-Captain-ALM"><strike><del>Not public due to university anti-plagiarism policy.</del></strike></a>
</p>
startDate: "25/02/2022"
endDate: "08/05/2022"
videoLocation: "https://cityuni.captainalm.com/resources/stream/vid2.mp4"
videoContentType: "video/mp4"
thumbnailLocations:
- "https://cityuni.captainalm.com/resources/assets/pic4_t.png"
- "https://cityuni.captainalm.com/resources/assets/pic5_t.png"
- "https://cityuni.captainalm.com/resources/assets/pic6_t.png"
imageLocations:
- "https://cityuni.captainalm.com/resources/assets/pic4.png"
- "https://cityuni.captainalm.com/resources/assets/pic5.png"
- "https://cityuni.captainalm.com/resources/assets/pic6.png"
imageAltTexts:
- "Cave level."
- "Tutorial level."
- "Training level."
- name: "Global Game Jam January 2022 : Shadow work"
content: >
<p>
I may have not done any programming for this (Even though I know C#) but I helped write a good chunk of the background story
(I also helped with the level design although it turns out 48 hours is hard to get polished stuff done in, so some stuff had to be axed).
</p>
<p>
Read about and get the game files from: <a href="https://globalgamejam.org/2022/games/shadow-work-8">https://globalgamejam.org/2022/games/shadow-work-8</a>
</p>
<p>
Download the windows executable from: <a href="https://cdn.captainalm.com/download/ShadowWorkExecutable.zip">https://cdn.captainalm.com/download/ShadowWorkExecutable.zip</a>
</p>
startDate: "20/01/2022"
endDate: "30/01/2022"
#videoLocation: "https://cityuni.captainalm.com/resources/stream/vid3.mp4"
#videoContentType: "video/mp4"
- name: "City-University Portfolio"
content: >
<p>
This project is what's outputting this page at the moment! The backend is written in Go and there is both custom front-end Javascript and CSS!
The pages support theming and the entries can be sorted through Javascript or on the backend through GET parameters.
</p>
<p>
This project is under the BSD-3-Clause License, so if reusing, you must scrub references to me, the yml file this is written in is under
Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International <a href="https://creativecommons.org/licenses/by-nc-nd/4.0/">
<img src="https://mirrors.creativecommons.org/presskit/buttons/88x31/png/by-nc-nd.png" alt="License" height="16"></a>.
</p>
<p>
Find the source code here: <a href="https://code.mrmelon54.xyz/alfred/cityuni-webserver">https://code.mrmelon54.xyz/alfred/cityuni-webserver</a>
</p>
startDate: "13/07/2022"

0
index.js Normal file
View File

38
light.css Normal file
View File

@ -0,0 +1,38 @@
body{
color: #060606;
background-color: #fafaf9;
border-color: #969696;
}
a{
color: #4f4fff;
}
.header, .nav, footer, .so-pane{
background-color: #e2e2e1;
}
.home-button > div, .sort-button > div, .menu a, .so-pane > *{
color: #1f1f1f;
}
.home-button:hover, .menu a:hover, .hmb:hover, .sort-button:hover, .sort-menu:checked ~ .sort-button, .so-pane > input, .so-pane > select{
background-color: #9f9f9e;
}
.hmb-line, .hmb-line::before, .hmb-line::after{
background: #1f1f1f;
}
.main-box{
background-color: #f0f0f0;
}
.item-table{
background-color: #c0c0c0;
}
.so-pane{
border-color: #c0c0c0;
}
.item-table > div > div, .item-table-caption{
border-color: #0a214c;
}
.image-box{
background-color: #b0b0b0;
}
.image-box > a{
border-color: #4f4f0f;
}

View File

@ -7,13 +7,14 @@ import (
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"net/textproto" "net/textproto"
"net/url"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
) )
const indexName = "index.go"
type PageHandler struct { type PageHandler struct {
PageContentsCache map[string]*CachedPage PageContentsCache map[string]*CachedPage
PageProviders map[string]PageProvider PageProviders map[string]PageProvider
@ -50,7 +51,16 @@ func NewPageHandler(config conf.ServeYaml) *PageHandler {
} }
func (ph *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { func (ph *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
actualPagePath := strings.TrimRight(request.URL.Path, "/") actualPagePath := ""
if strings.HasSuffix(request.URL.Path, "/") {
if strings.HasSuffix(request.URL.Path, ".go/") {
actualPagePath = strings.TrimRight(request.URL.Path, "/")
} else {
actualPagePath = request.URL.Path + indexName
}
} else {
actualPagePath = request.URL.Path
}
var currentProvider PageProvider var currentProvider PageProvider
canCache := false canCache := false
@ -64,7 +74,7 @@ func (ph *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
actualQueries = currentProvider.GetCacheIDExtension(queryValues) actualQueries = currentProvider.GetCacheIDExtension(queryValues)
if ph.CacheSettings.EnableContentsCaching { if ph.CacheSettings.EnableContentsCaching {
cached := ph.getPageFromCache(request.URL, actualQueries) cached := ph.getPageFromCache(request.URL.Path, actualQueries)
if cached != nil { if cached != nil {
pageContent = cached.Content pageContent = cached.Content
pageContentType = cached.ContentType pageContentType = cached.ContentType
@ -76,7 +86,7 @@ func (ph *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
pageContentType, pageContent, canCache = currentProvider.GetContents(queryValues) pageContentType, pageContent, canCache = currentProvider.GetContents(queryValues)
lastMod = currentProvider.GetLastModified() lastMod = currentProvider.GetLastModified()
if pageContentType != "" && canCache && ph.CacheSettings.EnableContentsCaching { if pageContentType != "" && canCache && ph.CacheSettings.EnableContentsCaching {
ph.setPageToCache(request.URL, actualQueries, &CachedPage{ ph.setPageToCache(request.URL.Path, actualQueries, &CachedPage{
Content: pageContent, Content: pageContent,
ContentType: pageContentType, ContentType: pageContentType,
LastMod: lastMod, LastMod: lastMod,
@ -143,7 +153,7 @@ func (ph *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
} }
} }
case http.MethodDelete: case http.MethodDelete:
ph.PurgeTemplateCache(actualPagePath) ph.PurgeTemplateCache(actualPagePath, request.URL.Path == "/")
ph.PurgeContentsCache(request.URL.Path, actualQueries) ph.PurgeContentsCache(request.URL.Path, actualQueries)
utils.SetNeverCacheHeader(writer.Header()) utils.SetNeverCacheHeader(writer.Header())
utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "") utils.WriteResponseHeaderCanWriteBody(request.Method, writer, http.StatusOK, "")
@ -167,12 +177,12 @@ func (ph *PageHandler) ServeHTTP(writer http.ResponseWriter, request *http.Reque
func (ph *PageHandler) PurgeContentsCache(path string, query string) { func (ph *PageHandler) PurgeContentsCache(path string, query string) {
if ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge { if ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge {
if path == "" { if path == "/" {
ph.pageContentsCacheRWMutex.Lock() ph.pageContentsCacheRWMutex.Lock()
ph.PageContentsCache = make(map[string]*CachedPage) ph.PageContentsCache = make(map[string]*CachedPage)
ph.pageContentsCacheRWMutex.Unlock() ph.pageContentsCacheRWMutex.Unlock()
} else { } else {
if strings.HasSuffix(path, "/") { if strings.HasSuffix(path, ".go/") {
ph.pageContentsCacheRWMutex.RLock() ph.pageContentsCacheRWMutex.RLock()
toDelete := make([]string, len(ph.PageContentsCache)) toDelete := make([]string, len(ph.PageContentsCache))
theSize := 0 theSize := 0
@ -189,7 +199,10 @@ func (ph *PageHandler) PurgeContentsCache(path string, query string) {
delete(ph.PageContentsCache, toDelete[i]) delete(ph.PageContentsCache, toDelete[i])
} }
ph.pageContentsCacheRWMutex.Unlock() ph.pageContentsCacheRWMutex.Unlock()
} else { return
} else if strings.HasSuffix(path, "/") {
path += indexName
}
ph.pageContentsCacheRWMutex.Lock() ph.pageContentsCacheRWMutex.Lock()
if query == "" { if query == "" {
delete(ph.PageContentsCache, path) delete(ph.PageContentsCache, path)
@ -199,12 +212,11 @@ func (ph *PageHandler) PurgeContentsCache(path string, query string) {
ph.pageContentsCacheRWMutex.Unlock() ph.pageContentsCacheRWMutex.Unlock()
} }
} }
}
} }
func (ph *PageHandler) PurgeTemplateCache(path string) { func (ph *PageHandler) PurgeTemplateCache(path string, all bool) {
if ph.CacheSettings.EnableTemplateCaching && ph.CacheSettings.EnableTemplateCachePurge { if ph.CacheSettings.EnableTemplateCaching && ph.CacheSettings.EnableTemplateCachePurge {
if path == "" { if all {
for _, pageProvider := range ph.PageProviders { for _, pageProvider := range ph.PageProviders {
pageProvider.PurgeTemplate() pageProvider.PurgeTemplate()
} }
@ -215,36 +227,39 @@ func (ph *PageHandler) PurgeTemplateCache(path string) {
} }
} }
} }
func (ph *PageHandler) getPageFromCache(urlIn *url.URL, cleanedQueries string) *CachedPage { func (ph *PageHandler) getPageFromCache(pathIn string, cleanedQueries string) *CachedPage {
ph.pageContentsCacheRWMutex.RLock() ph.pageContentsCacheRWMutex.RLock()
defer ph.pageContentsCacheRWMutex.RUnlock() defer ph.pageContentsCacheRWMutex.RUnlock()
if strings.HasSuffix(urlIn.Path, "/") { if strings.HasSuffix(pathIn, ".go/") {
return ph.PageContentsCache[strings.TrimRight(urlIn.Path, "/")] return ph.PageContentsCache[strings.TrimRight(pathIn, "/")]
} else { } else if strings.HasSuffix(pathIn, "/") {
if cleanedQueries == "" { pathIn += indexName
return ph.PageContentsCache[urlIn.Path]
} else {
return ph.PageContentsCache[urlIn.Path+"?"+cleanedQueries]
} }
if cleanedQueries == "" {
return ph.PageContentsCache[pathIn]
} else {
return ph.PageContentsCache[pathIn+"?"+cleanedQueries]
} }
} }
func (ph *PageHandler) setPageToCache(urlIn *url.URL, cleanedQueries string, newPage *CachedPage) { func (ph *PageHandler) setPageToCache(pathIn string, cleanedQueries string, newPage *CachedPage) {
ph.pageContentsCacheRWMutex.Lock() ph.pageContentsCacheRWMutex.Lock()
defer ph.pageContentsCacheRWMutex.Unlock() defer ph.pageContentsCacheRWMutex.Unlock()
if strings.HasSuffix(urlIn.Path, "/") { if strings.HasSuffix(pathIn, ".go/") {
ph.PageContentsCache[strings.TrimRight(urlIn.Path, "/")] = newPage ph.PageContentsCache[strings.TrimRight(pathIn, "/")] = newPage
} else { return
if cleanedQueries == "" { } else if strings.HasSuffix(pathIn, "/") {
ph.PageContentsCache[urlIn.Path] = newPage pathIn += indexName
} else {
ph.PageContentsCache[urlIn.Path+"?"+cleanedQueries] = newPage
} }
if cleanedQueries == "" {
ph.PageContentsCache[pathIn] = newPage
} else {
ph.PageContentsCache[pathIn+"?"+cleanedQueries] = newPage
} }
} }
func (ph *PageHandler) getAllowedMethodsForPath(pathIn string) []string { func (ph *PageHandler) getAllowedMethodsForPath(pathIn string) []string {
if strings.HasSuffix(pathIn, "/") { if pathIn == "/" || strings.HasSuffix(pathIn, ".go/") {
if (ph.CacheSettings.EnableTemplateCaching && ph.CacheSettings.EnableTemplateCachePurge) || if (ph.CacheSettings.EnableTemplateCaching && ph.CacheSettings.EnableTemplateCachePurge) ||
(ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge) { (ph.CacheSettings.EnableContentsCaching && ph.CacheSettings.EnableContentsCachePurge) {
return []string{http.MethodHead, http.MethodGet, http.MethodOptions, http.MethodDelete} return []string{http.MethodHead, http.MethodGet, http.MethodOptions, http.MethodDelete}

View File

@ -2,20 +2,23 @@ package index
import ( import (
"html/template" "html/template"
"strconv"
"strings"
"time" "time"
) )
type AboutYaml struct { type AboutYaml struct {
Title string `yaml:"title"` Title string `yaml:"title"`
Content string `yaml:"content"` Content string `yaml:"content"`
ThumbnailLocation string `yaml:"thumbnailLocation"` ThumbnailLocation template.URL `yaml:"thumbnailLocation"`
ImageLocation string `yaml:"imageLocation"` ImageLocation template.URL `yaml:"imageLocation"`
ImageAltText string `yaml:"imageAltText"`
BirthYear int `yaml:"birthYear"` BirthYear int `yaml:"birthYear"`
ContactEmail string `yaml:"contactEmail"` ContactEmail string `yaml:"contactEmail"`
} }
func (ay AboutYaml) GetContent() template.HTML { func (ay AboutYaml) GetContent() template.HTML {
return template.HTML(ay.Content) return template.HTML(strings.ReplaceAll(strings.ReplaceAll(ay.Content, "#age#", strconv.Itoa(ay.GetAge())), "#birth#", strconv.Itoa(ay.BirthYear)))
} }
func (ay AboutYaml) GetAge() int { func (ay AboutYaml) GetAge() int {

View File

@ -1,12 +1,38 @@
package index package index
import "html/template"
type DataYaml struct { type DataYaml struct {
HomeLink string `yaml:"homeLink"` HeaderLinks map[string]template.URL `yaml:"headerLinks"`
PortfolioLink string `yaml:"portfolioLink"` CSSBaseURL template.URL `yaml:"cssBaseURL"`
CSSBaseURL string `yaml:"cssBaseURL"` CSSLightURL template.URL `yaml:"cssLightURL"`
CSSLightURL string `yaml:"cssLightURL"` CSSDarkURL template.URL `yaml:"cssDarkURL"`
CSSDarkURL string `yaml:"cssDarkURL"` JScriptURL template.URL `yaml:"jScriptURL"`
JScriptURL string `yaml:"jScriptURL"` NoVideoImageLocation template.URL `yaml:"noVideoImageLocation"`
LogoImageLocation template.URL `yaml:"logoImageLocation"`
SunImageLocation template.URL `yaml:"sunImageLocation"`
MoonImageLocation template.URL `yaml:"moonImageLocation"`
SortImageLocation template.URL `yaml:"sortImageLocation"`
About AboutYaml `yaml:"about"` About AboutYaml `yaml:"about"`
Entries []EntryYaml `yaml:"entries"` Entries []EntryYaml `yaml:"entries"`
} }
func (dy DataYaml) GetHeaderLabels() []string {
if dy.HeaderLinks == nil {
return []string{}
}
toReturn := make([]string, len(dy.HeaderLinks))
i := 0
for key := range dy.HeaderLinks {
toReturn[i] = key
i++
}
return toReturn
}
func (dy DataYaml) GetHeaderLink(headerLabel string) template.URL {
if dy.HeaderLinks == nil {
return ""
}
return dy.HeaderLinks[headerLabel]
}

View File

@ -1,27 +1,41 @@
package index package index
import ( import (
"golang.captainalm.com/cityuni-webserver/utils/yaml"
"html/template" "html/template"
"math"
"net/http"
"time" "time"
) )
const dateFormat = "2006-01-02" const dateFormat = "01/2006"
type EntryYaml struct { type EntryYaml struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Content string `yaml:"content"` Content string `yaml:"content"`
StartDate time.Time `yaml:"startDate"` StartDate yaml.DateType `yaml:"startDate"`
EndDate time.Time `yaml:"endDate"` EndDate yaml.DateType `yaml:"endDate"`
VideoLocation string `yaml:"videoLocation"` VideoLocation template.URL `yaml:"videoLocation"`
VideoContentType string `yaml:"videoContentType"` VideoContentType string `yaml:"videoContentType"`
ThumbnailLocations []string `yaml:"thumbnailLocations"` ThumbnailLocations []template.URL `yaml:"thumbnailLocations"`
ImageLocations []string `yaml:"imageLocations"` ImageLocations []template.URL `yaml:"imageLocations"`
ImageAltTexts []string `yaml:"imageAltTexts"`
}
type ImageReference struct {
ThumbnailLocation template.URL
ImageLocation template.URL
ImageAltText string
} }
func (ey EntryYaml) GetStartDate() string { func (ey EntryYaml) GetStartDate() string {
return ey.StartDate.Format(dateFormat) return ey.StartDate.Format(dateFormat)
} }
func (ey EntryYaml) GetStartDateHTML() string {
return ey.StartDate.Format(http.TimeFormat)
}
func (ey EntryYaml) GetEndDate() string { func (ey EntryYaml) GetEndDate() string {
if ey.EndDate.IsZero() { if ey.EndDate.IsZero() {
return "" return ""
@ -30,11 +44,15 @@ func (ey EntryYaml) GetEndDate() string {
} }
} }
func (ey EntryYaml) GetEndDateHTML() string {
return ey.GetEndTime().Format(http.TimeFormat)
}
func (ey EntryYaml) GetEndTime() time.Time { func (ey EntryYaml) GetEndTime() time.Time {
if ey.EndDate.IsZero() { if ey.EndDate.IsZero() {
return time.Now() return time.Now()
} else { } else {
return ey.EndDate return ey.EndDate.Time
} }
} }
@ -43,5 +61,25 @@ func (ey EntryYaml) GetContent() template.HTML {
} }
func (ey EntryYaml) GetDuration() time.Duration { func (ey EntryYaml) GetDuration() time.Duration {
return ey.GetEndTime().Sub(ey.StartDate).Truncate(time.Second) return ey.GetEndTime().Sub(ey.StartDate.Time).Truncate(time.Second)
}
func (ey EntryYaml) GetInt64Duration() int64 {
return int64(ey.GetDuration())
}
func (ey EntryYaml) GetImageCount() int {
return int(math.Min(math.Min(float64(len(ey.ThumbnailLocations)), float64(len(ey.ImageLocations))), float64(len(ey.ImageAltTexts))))
}
func (ey EntryYaml) GetImages() []ImageReference {
toReturn := make([]ImageReference, ey.GetImageCount())
for i := 0; i < len(ey.ThumbnailLocations) && i < len(ey.ImageLocations) && i < len(ey.ImageAltTexts); i++ {
toReturn[i] = ImageReference{
ThumbnailLocation: ey.ThumbnailLocations[i],
ImageLocation: ey.ImageLocations[i],
ImageAltText: ey.ImageAltTexts[i],
}
}
return toReturn
} }

View File

@ -17,12 +17,14 @@ const yamlName = "index.go.yml"
func NewPage(dataStore string, cacheTemplates bool) *Page { func NewPage(dataStore string, cacheTemplates bool) *Page {
var ptm *sync.Mutex var ptm *sync.Mutex
var sdm *sync.Mutex
if cacheTemplates { if cacheTemplates {
ptm = &sync.Mutex{} ptm = &sync.Mutex{}
sdm = &sync.Mutex{}
} }
pageToReturn := &Page{ pageToReturn := &Page{
DataStore: dataStore, DataStore: dataStore,
StoredDataMutex: &sync.Mutex{}, StoredDataMutex: sdm,
PageTemplateMutex: ptm, PageTemplateMutex: ptm,
} }
return pageToReturn return pageToReturn
@ -51,6 +53,17 @@ func (p *Page) GetLastModified() time.Time {
} }
func (p *Page) GetCacheIDExtension(urlParameters url.Values) string { func (p *Page) GetCacheIDExtension(urlParameters url.Values) string {
toReturn := p.getNonThemedCleanQuery(urlParameters)
if toReturn != "" {
toReturn += "&"
}
if urlParameters.Has("light") {
toReturn += "light"
}
return strings.TrimRight(toReturn, "&")
}
func (p *Page) getNonThemedCleanQuery(urlParameters url.Values) string {
toReturn := "" toReturn := ""
if urlParameters.Has("order") { if urlParameters.Has("order") {
if theParameter := strings.ToLower(urlParameters.Get("order")); theParameter == "start" || theParameter == "end" || theParameter == "name" || theParameter == "duration" { if theParameter := strings.ToLower(urlParameters.Get("order")); theParameter == "start" || theParameter == "end" || theParameter == "name" || theParameter == "duration" {
@ -59,12 +72,9 @@ func (p *Page) GetCacheIDExtension(urlParameters url.Values) string {
} }
if urlParameters.Has("sort") { if urlParameters.Has("sort") {
if theParameter := strings.ToLower(urlParameters.Get("sort")); theParameter == "asc" || theParameter == "ascending" || theParameter == "desc" || theParameter == "descending" { if theParameter := strings.ToLower(urlParameters.Get("sort")); theParameter == "asc" || theParameter == "ascending" || theParameter == "desc" || theParameter == "descending" {
toReturn += "sort=" + theParameter + "&" toReturn += "sort=" + theParameter
} }
} }
if urlParameters.Has("light") {
toReturn += "light"
}
return strings.TrimRight(toReturn, "&") return strings.TrimRight(toReturn, "&")
} }
@ -80,6 +90,7 @@ func (p *Page) GetContents(urlParameters url.Values) (contentType string, conten
theMarshal := &Marshal{ theMarshal := &Marshal{
Data: *theData, Data: *theData,
Light: urlParameters.Has("light"), Light: urlParameters.Has("light"),
Parameters: template.URL(p.getNonThemedCleanQuery(urlParameters)),
} }
switch strings.ToLower(urlParameters.Get("order")) { switch strings.ToLower(urlParameters.Get("order")) {
case "end": case "end":

View File

@ -1,9 +1,13 @@
package index package index
import "sort" import (
"html/template"
"sort"
)
type Marshal struct { type Marshal struct {
Data DataYaml Data DataYaml
Parameters template.URL
OrderStartDate int8 OrderStartDate int8
OrderEndDate int8 OrderEndDate int8
OrderName int8 OrderName int8
@ -15,12 +19,12 @@ func (m Marshal) GetEntries() (toReturn []EntryYaml) {
toReturn = m.Data.Entries toReturn = m.Data.Entries
if m.OrderStartDate > 0 { if m.OrderStartDate > 0 {
sort.Slice(toReturn, func(i, j int) bool { sort.Slice(toReturn, func(i, j int) bool {
return toReturn[i].StartDate.Before(toReturn[j].StartDate) return toReturn[i].StartDate.Before(toReturn[j].StartDate.Time)
}) })
} }
if m.OrderStartDate < 0 { if m.OrderStartDate < 0 {
sort.Slice(toReturn, func(i, j int) bool { sort.Slice(toReturn, func(i, j int) bool {
return toReturn[i].StartDate.After(toReturn[j].StartDate) return toReturn[i].StartDate.After(toReturn[j].StartDate.Time)
}) })
} }
if m.OrderEndDate > 0 { if m.OrderEndDate > 0 {

31
utils/yaml/date-type.go Normal file
View File

@ -0,0 +1,31 @@
package yaml
import (
"gopkg.in/yaml.v3"
"strings"
"time"
)
const dateFormat = "02/01/2006"
type DateType struct {
time.Time
}
func (dt *DateType) MarshalYAML() (interface{}, error) {
return dt.Time.Format(dateFormat), nil
}
func (dt *DateType) UnmarshalYAML(value *yaml.Node) error {
var stringIn string
err := value.Decode(&stringIn)
if err != nil {
return nil
}
pt, err := time.Parse(dateFormat, strings.TrimSpace(stringIn))
if err != nil {
return err
}
dt.Time = pt
return nil
}