Compare commits

...

42 Commits

Author SHA1 Message Date
227a8a308a
Make sure fonts are deterministic. 2024-09-22 11:01:14 +01:00
1c0626aaed
Add cdn links for gcloud city design downloads. 2024-09-21 22:58:24 +01:00
0de79f2cf1
Shorten the gc-c- entry.
Some checks are pending
ci/woodpecker/push/build Pipeline is pending
2024-06-15 17:44:43 +01:00
d676d742c8
Mid 2024 Update.
Some checks are pending
ci/woodpecker/push/build Pipeline is pending
2024-06-15 17:39:31 +01:00
759e04dc25
Fallback compatibility.
Some checks are pending
ci/woodpecker/push/build Pipeline is pending
2023-09-19 23:31:01 +01:00
d41851cfbc
Squash and merge edge.
Some checks are pending
ci/woodpecker/push/build Pipeline is pending
September Update.
2023-09-19 23:27:32 +01:00
f60e04ada4
Squash and merge edge.
All checks were successful
ci/woodpecker/push/build Pipeline was successful
2022-12-28 20:36:13 +00:00
de745fad87
September update.
All checks were successful
continuous-integration/drone/push Build is passing
Fix .xyz -> .com
Fix padding of text.
Add image preload for moon in js.
Add CV references in about.
2022-09-07 15:47:02 +01:00
717f47cbef
Squash edge.
All checks were successful
continuous-integration/drone/push Build is passing
Apply auto visibility of items on the nav bar when window resized.
2022-08-17 23:09:13 +01:00
f5a850efd5
Update padding for the hamburger.
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-17 00:23:50 +01:00
42f578d5a3
Fix CSS again!
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-17 00:19:53 +01:00
d6ebbfcb20
Update CSS.
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-16 23:04:17 +01:00
14e8d1e9e9
Update index.go.yml.
All checks were successful
continuous-integration/drone/push Build is passing
Add beta deploy support through "d".
2022-08-15 19:35:20 +01:00
f614f7bfe9
Update css and go html pages.
All checks were successful
continuous-integration/drone/push Build is passing
Update the index data yml.
2022-08-01 01:39:11 +01:00
3ec08ec6d9
Update so-pane position.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-31 00:20:06 +01:00
54df3388e3
Update padding.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-31 00:12:44 +01:00
b1a6dbe4db
Switch back to replace.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 23:59:21 +01:00
489ec3a7df
Hopefully fix history state.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 22:17:33 +01:00
814b43e43d
Fixed the Pop issue and a naming issue.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 21:45:57 +01:00
90d43f46b8
Try fixing popstate?
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 21:34:16 +01:00
aa82a91a29
Switch ReplaceHistory with PushHistory
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 21:21:17 +01:00
a314ffa2b1
Fix sort not executing if order kept the same.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 21:17:02 +01:00
f79020c95d
Fix sorting object target bug.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 21:11:29 +01:00
fd95d53702
Fix sorting URL setter.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 21:07:22 +01:00
4c180c0966
Fix script variable issues.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 21:06:04 +01:00
8f3fc74270
Add sorting Java Script.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 21:03:47 +01:00
18eb65218b
Fix sort order menu display issues.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 18:05:11 +01:00
5bbd9db71d
Add hiding order-sort pane when clicked off of.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 17:59:14 +01:00
2477a7ee20
Fully fix URL splitting code.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 17:03:52 +01:00
38e5bad973
Fix base URL cleaning for IE.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 17:02:12 +01:00
d9e37e4e2e
Switch to ReplaceHistory.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 16:57:18 +01:00
5d7b39ac15
Fully add JS Theme switching.
All checks were successful
continuous-integration/drone/push Build is passing
Remove non-working IE Favicon Link Tags.
2022-07-30 16:49:44 +01:00
7c59cbacea
Make JS Setup get called after the main tag.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 16:23:00 +01:00
fbc6b3ed6a
Add JS based theme toggling.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 16:19:45 +01:00
27ae3e3fed
Add title attribute for video play image.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 15:13:54 +01:00
7951002892
Set video width.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 15:12:43 +01:00
1c1418e78a
Hotfix config.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 15:11:09 +01:00
5a62535bce
Initial script stuff.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 15:07:10 +01:00
26b7893c71
Fix drop down positioning.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 01:35:56 +01:00
2948165acb
Hotfix nav images.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-30 01:23:44 +01:00
dba4022bf4
Update layout for full flex-box with table fallback.
All checks were successful
continuous-integration/drone/push Build is passing
Fix up sort drop down css and html.
Resize nav bar.
Round some objects.
Add copyright messages.
2022-07-29 22:34:12 +01:00
e1e78655bd
Squash Edge branch with data and full pure html / css page support.
All checks were successful
continuous-integration/drone/push Build is passing
2022-07-28 01:56:00 +01:00
25 changed files with 1752 additions and 121 deletions

View File

@ -1,8 +0,0 @@
kind: pipeline
name: default
steps:
- name: build
image: golang
commands:
- make build

12
.gitignore vendored
View File

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

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SwUserDefinedSpecifications">
<option name="specTypeByUrl">
<map />
</option>
</component>
</project>

12
.woodpecker/build.yml Normal file
View File

@ -0,0 +1,12 @@
platform: linux/amd64
pipeline:
format:
image: golang
commands:
- files=$(gofmt -l .) && echo "$files" && [ -z "$files" ]
build:
image: golang
commands:
- make build

View File

@ -1,6 +1,7 @@
SHELL := /bin/bash SHELL := /bin/bash
PRODUCT_NAME := wappcityuni PRODUCT_NAME := wappcityuni
BIN := dist/${PRODUCT_NAME} BIN := dist/${PRODUCT_NAME}
DNAME := ${PRODUCT_NAME}_
ENTRY_POINT := ./cmd/${PRODUCT_NAME} ENTRY_POINT := ./cmd/${PRODUCT_NAME}
HASH := $(shell git rev-parse --short HEAD) HASH := $(shell git rev-parse --short HEAD)
COMMIT_DATE := $(shell git show -s --format=%ci ${HASH}) COMMIT_DATE := $(shell git show -s --format=%ci ${HASH})
@ -11,9 +12,10 @@ COMP_BIN := go
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
BIN := $(BIN).exe BIN := $(BIN).exe
DNAME := $(DNAME).exe
endif endif
.PHONY: build dev test clean deploy .PHONY: build dev test clean deploy d
build: build:
mkdir -p dist/ mkdir -p dist/
@ -34,4 +36,17 @@ 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
d: build
sudo systemctl stop wappcityuni_
sudo cp "${BIN}" "/usr/local/bin/${DNAME}"
sudo cp *.go.html cnf_
sudo cp *.go.yml cnf_
sudo cp *.css cdn_
sudo cp *.js cdn_
sudo systemctl start wappcityuni_

View File

@ -1,11 +1,13 @@
# Captain ALM Cityuni subdomain WebServer # Captain ALM Cityuni subdomain WebServer
[![Build Status](https://ci.mrmelon54.xyz/api/badges/alfred/cityuni-webserver/status.svg)](https://ci.mrmelon54.xyz/alfred/cityuni-webserver) [![Build Status](https://ci.mrmelon54.com/api/badges/alfred/cityuni-webserver/status.svg)](https://ci.mrmelon54.com/alfred/cityuni-webserver)
This provides my template and cache supporting web / application server for my city university portfolio subdomain. This provides my template and cache supporting web / application server for my city university portfolio subdomain.
[Production Server](https://cityuni.captainalm.com/)
Maintainer: Maintainer:
[Captain ALM](https://code.mrmelon54.xyz/alfred) [Captain ALM](https://code.mrmelon54.com/alfred)
License: License:
[BSD 3-Clause](https://code.mrmelon54.xyz/alfred/GOPackageHeaderServer/src/branch/master/LICENSE) [BSD 3-Clause](https://code.mrmelon54.com/alfred/cityuni-webserver/src/branch/master/LICENSE)

358
base.css Normal file
View File

@ -0,0 +1,358 @@
/*
This file is (C) Captain ALM
Under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
*/
*{
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Times New Roman", Times, serif;
}
#st{
position: absolute;
left: -1080px;
top: -1080px;
font-size: 1em;
visibility: hidden;
white-space: nowrap;
}
main{
display: block;
padding-top: 70px;
padding-left: 6px;
}
.no-dec{
text-decoration: none;
}
.no-lst-style{
list-style: none;
}
.centered{
text-align: center;
}
.content, .content > *{
word-break: break-word;
-ms-word-wrap: break-word;
word-wrap: break-word;
}
.content > p, .content > h1, .content > h2, .content > h3, .content > h4, .content > h5, .content > h6{
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.header{
position: fixed;
top: 0;
width: 100%;
z-index: 100;
border-style: solid;
border-width: 0 0 2px;
}
.home-button, .sort-button{
display: inline-block;
width: 84px;
height: 66px;
overflow: hidden;
text-align: center;
}
.sort-button{
cursor: pointer;
}
.home-button > div, .sort-button > div{
display: inline;
font-size: 48px;
padding: 10px;
vertical-align: middle;
}
.nav{
width: 100%;
height: 100%;
overflow: hidden;
max-height: 0;
}
.vnav{
max-height: none;
top: 0;
position: relative;
float: right;
width: auto;
height: 100%;
overflow: hidden;
}
.so-pane{
display: none;
overflow: hidden;
position: fixed;
max-height: 0;
}
.so-pane > form > div{
display: table-row;
background-color: transparent;
box-sizing: inherit;
max-width: 100%;
width: 100%;
}
.so-pane > form > div > div, .so-pane > form > div > span{
background-color: transparent;
box-sizing: inherit;
padding: 2px;
text-align: center;
vertical-align: middle;
}
.so-pane > form > div > div{
display: table-cell;
max-width: 50%;
width: 50%;
}
.so-pane > form > div > span{
display: block;
max-width: 100%;
width: 100%;
}
.so-pane-full{
display: table-caption !important;
caption-side: bottom;
}
.so-pane > form > div > * > *{
max-width: 100%;
width: 100%;
vertical-align: middle;
font-size: 16px;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.so-pane > form > div > div > *{
text-align: left;
}
.so-pane > form > div > div > label{
background-color: transparent;
text-align: center;
}
.nav-menu, .sort-menu{
display: none;
}
.menu a, .vmenu a{
display: block;
}
.menu a, .vmenu a, #st{
padding: 24px 16px;
font-weight: bold;
}
.vmenu li{
float: left;
}
.vmenu a:hover{
background-color: transparent;
}
.hmb{
cursor: pointer;
float: right;
background-color: transparent;
padding: 32px 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, .nav-open{
max-height: 100%;
}
.nav-menu:checked ~ .hmb .hmb-line, .nav-open-hmb .hmb-line{
background: transparent;
}
.nav-menu:checked ~ .hmb .hmb-line::before, .nav-open-hmb .hmb-line::before{
transform: rotate(-45deg);
top:0;
}
.nav-menu:checked ~ .hmb .hmb-line::after, .nav-open-hmb .hmb-line::after{
transform: rotate(45deg);
top:0;
}
.sort-menu:checked ~ .so-pane, .so-pane-open{
display: block;
z-index: 101;
box-sizing: content-box;
position: fixed;
text-align: center;
top: 70px;
left: 20px;
max-height: 100%;
width: 230px;
height: auto;
-webkit-border-bottom-left-radius: 8px;
-webkit-border-bottom-right-radius: 8px;
-moz-border-radius-bottomleft: 8px;
-moz-border-radius-bottomright: 8px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
padding: 4px;
border-style: solid;
border-width: 0 2px 2px;
overflow: hidden;
}
.sort-menu:checked ~ .so-pane > form, .so-pane-open > form{
display: table;
box-sizing: inherit;
}
.main-box > div, footer{
max-width: 100%;
margin: 3px 0;
padding: 2px;
box-sizing: content-box;
}
.item-table{
display: table;
box-sizing: content-box;
background-color: transparent;
-webkit-border-radius: 32px;
-moz-border-radius: 32px;
border-radius: 32px;
border-width: 1px;
border-style: solid;
}
.flex-col{
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;
max-width: 100%;
width: 100%;
overflow: hidden;
}
.item-table > div{
display: table-row;
box-sizing: inherit;
max-width: 100%;
width: 100%;
}
.flex-row, .item-table > div{
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;
max-width: 100%;
width: 100%;
overflow: hidden;
}
.item-table > div > div > div{
padding: 4px;
}
.item-table-caption, .so-pane-caption{
display: table-caption !important;
caption-side: bottom;
}
.item-table-full, .item-table-360, .item-table-caption, .so-pane-caption{
background-color: transparent;
box-sizing: inherit;
}
.item-table-full{
display: table-cell;
vertical-align: top;
width: 100%;
}
.item-table-360{
display: table-cell;
vertical-align: middle;
max-width: 360px;
width: 360px;
overflow: hidden;
text-align: center;
}
.image-box > a{
border-style: solid;
border-width: 1px;
margin: 2px;
}
@media (min-width: 600px){
.main-box > div{
padding: 12px;
}
footer{
padding: 4px;
}
.item-table > div{
-ms-flex-wrap: nowrap;
-webkit-flex-wrap: nowrap;
flex-wrap: nowrap;
}
.item-table-full{
max-width: -webkit-calc(100% - 361px);
max-width: -moz-calc(100% - 361px);
max-width: calc(100% - 361px);
width: -webkit-calc(100% - 361px);
width: -moz-calc(100% - 361px);
width: calc(100% - 361px);
}
.image-box > a{
border-width: 4px;
margin: 10px;
}
}
@media (min-width: 680px){
.nav{
max-height: none;
top: 0;
position: relative;
float: right;
width: auto;
}
.vnav{
display: none;
}
.menu li{
float: left;
}
.menu a:hover{
background-color: transparent;
}
.hmb{
display: none;
}
}
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
.item-table-full, .item-table-360 {
display: block !important;
}
.item-table-caption{
display: block !important;
}
}

19
conf/page.go Normal file
View File

@ -0,0 +1,19 @@
package conf
import "strings"
type PageYaml struct {
PageName string `yaml:"pageName"`
PagePath string `yaml:"pagePath"`
}
func (py PageYaml) GetPagePath() string {
toReturn := py.PagePath
if !strings.HasSuffix(toReturn, ".go") {
toReturn += ".go"
}
if !strings.HasPrefix(toReturn, "/") {
toReturn = "/" + toReturn
}
return toReturn
}

View File

@ -9,10 +9,13 @@ import (
type ServeYaml struct { type ServeYaml struct {
DataStorage string `yaml:"dataStorage"` DataStorage string `yaml:"dataStorage"`
TemplateStorage string `yaml:"templateStorage"`
Domains []string `yaml:"domains"` Domains []string `yaml:"domains"`
RangeSupported bool `yaml:"rangeSupported"` RangeSupported bool `yaml:"rangeSupported"`
EnableGoInfoPage bool `yaml:"enableGoInfoPage"` EnableGoInfoPage bool `yaml:"enableGoInfoPage"`
CacheSettings CacheSettingsYaml `yaml:"cacheSettings"` CacheSettings CacheSettingsYaml `yaml:"cacheSettings"`
PageSettings []PageYaml `yaml:"pageSettings"`
YmlDataFallback bool `yaml:"ymlDataFallback"`
} }
func (sy ServeYaml) GetDomainString() string { func (sy ServeYaml) GetDomainString() string {
@ -39,3 +42,20 @@ func (sy ServeYaml) GetDataStoragePath() string {
return sy.DataStorage return sy.DataStorage
} }
} }
func (sy ServeYaml) GetTemplateStoragePath() string {
if sy.TemplateStorage == "" || !filepath.IsAbs(sy.TemplateStorage) {
wd, err := os.Getwd()
if err != nil {
return ""
} else {
if sy.TemplateStorage == "" {
return wd
} else {
return path.Join(wd, sy.TemplateStorage)
}
}
} else {
return sy.TemplateStorage
}
}

View File

@ -4,9 +4,13 @@ listen:
webMethod: "http" webMethod: "http"
identify: true identify: true
serve: serve:
dataStorage: ""
domains: []
rangeSupported: true rangeSupported: true
enableGoInfoPage: true enableGoInfoPage: true
cacheSettings: cacheSettings:
enableTemplateCaching: false
enableTemplateCachePurge: false
enableContentsCaching: true enableContentsCaching: true
enableContentsCachePurge: true enableContentsCachePurge: true
maxAge: 3600 maxAge: 3600

40
dark.css Normal file
View File

@ -0,0 +1,40 @@
/*
This file is (C) Captain ALM
Under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
*/
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, .vmenu a, .so-pane > form > div > * > *{
color: #e0e0e0;
}
.home-button:hover, .menu a:hover, .vmenu a:hover, .hmb:hover, .sort-button:hover, .sort-menu:checked ~ .sort-button, .sort-button-active, .so-pane > form > div > span > input, .so-pane > form > div > div > select{
background-color: #606061;
}
.hmb-line, .hmb-line::before, .hmb-line::after{
background: #e0e0e0;
}
.main-box{
background-color: #0f0f0f;
}
.item-table{
background-color: #3f3f3f;
border-color: #f5deb3;
}
.so-pane, .header{
border-color: #6f6f6f;
}
.image-box, .item-heading{
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%;
} }
@ -70,6 +67,10 @@
<tr> <tr>
<th>Product Description</th> <th>Product Description</th>
<td>{{ .ProductDescription }}</td> <td>{{ .ProductDescription }}</td>
</tr>
<tr>
<th>Product License</th>
<td>BSD 3-Clause License</td>
</tr> </tr>
<tr> <tr>
<th>Product Location</th> <th>Product Location</th>
@ -149,6 +150,10 @@
<th>Memory Page Size</th> <th>Memory Page Size</th>
<td>{{ .PageSize }}</td> <td>{{ .PageSize }}</td>
</tr> </tr>
<tr>
<th>System Time</th>
<td>{{ .CurrentTime }}</td>
</tr>
</table> </table>
</p> </p>
<p> <p>

206
index.go.html Normal file
View File

@ -0,0 +1,206 @@
<!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"/>
<meta name="description" content="Captain ALM's City University Portfolio">
<meta name="keywords" content="CaptainALM Captain_ALM Captain ALM portfolio Alfred Manville projects programming hacking cracking city uni cityuni cuol City University of London mycityuni">
<title>City University Portfolio</title>
<link rel="stylesheet" href="{{ .Data.CSSBaseURL }}"/>
{{ if .Light }}
<link id="style-theme" rel="stylesheet" href="{{ .Data.CSSLightURL }}"/>
{{ else }}
<link id="style-theme" rel="stylesheet" href="{{ .Data.CSSDarkURL }}"/>
{{ end }}
<script type="application/javascript">
var TheParameters = "{{ .Parameters }}"
var CssLightURL = "{{ .Data.CSSLightURL }}"
var CssDarkURL = "{{ .Data.CSSDarkURL }}"
var SunImageURL = "{{ .Data.SunImageLocation }}"
var MoonImageURL = "{{ .Data.MoonImageLocation }}"
var PlayImageURL = "{{ .Data.PlayVideoImageLocation }}"
</script>
<script type="application/javascript" 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 id="theme-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 id="theme-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 id="theme-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" id="sort-menu-button" title="Order and Sort Options"><div><img src="{{ .Data.SortImageLocation }}" width="64px" alt='&#8595;'></div></label>
<div class="so-pane" id="so-pane">
<form action="?" method="get" id="so-form">
{{ if .Light }}
<input id="so-theme" type="hidden" name="light" />
{{ end }}
<div>
{{ $sort := 0 }}
<div><label class="no-dec" for="so-order">Order by:</label></div>
<div><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></div>
</div>
<div>
<div><label class="no-dec" for="so-sort">Sort:</label></div>
<div><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></div>
</div>
<div class="so-pane-full">
<span><input id="so-submit" type="submit" value="Commit"></span>
</div>
</form>
</div>
<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="vnav" id="vnav">
<ul class="vmenu no-lst-style" id="vmenu">
</ul>
</nav>
<nav class="nav" id="nav">
<ul class="menu no-lst-style" id="menu">
{{ range .Data.GetHeaderLabels }}
<li><a href="{{ $.Data.GetHeaderLink . }}" class="no-dec" title="{{ . }}">{{ . }}</a></li>
{{ end }}
</ul>
</nav>
</header>
<main class="main-box flex-col">
<div id="about">
<div class="item-table flex-col">
<div class="item-heading">
<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>
<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>
</div>
{{ $c := 0 }}
{{ range .GetEntries }}
{{ $c = $.CounterPlusPlus }}
<div id="entry-{{ $c }}">
<script type="application/javascript">
CreateEntry({{ $c }}, "{{ .Name }}", "{{ .VideoLocation }}", "{{ .VideoContentType }}", "{{ .GetStartDateHTML }}", "{{ .GetEndDateHTML }}", {{ .GetInt64Duration }})
</script>
<div class="item-table flex-col">
<div class="item-heading">
<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>
<div class="item-table-full">
<div class="content">{{ .GetContent }}</div>
</div>
<div class="item-table-360">
<div id="video-{{ $c }}">
{{ if eq .VideoLocation "" }}
<img src="{{ .GetVideoThumbnail $.Data.NoVideoImageLocation }}" alt="No Video" width="360px">
{{ else }}
{{ if .IsVideoLink }}
<a href="{{ .VideoLocation }}">
<img src="{{ .GetVideoThumbnail $.Data.PlayVideoImageLocation }}" alt="Play Video" title="Play" width="360px">
</a>
{{ else }}
<script type="application/javascript">
CreateVideoPlaceholder({{ $c }}, "{{ .GetVideoThumbnail $.Data.PlayVideoImageLocation }}")
</script>
<noscript>
<video controls width="360px">
<source src="{{ .VideoLocation }}" type="{{ .VideoContentType }}">
<a href="{{ .VideoLocation }}">The Video</a>
</video>
</noscript>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ if not (eq .GetImageCount 0) }}
<div class="item-table-caption">
<div class="image-box flex-row">
{{ range .GetImages }}
<a href="{{ .ImageLocation }}"><img src="{{ .ThumbnailLocation }}" alt="{{ .ImageAltText }}" title="{{ .ImageAltText }}" width="240px"></a>
{{ end }}
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}
</main>
<script type="application/javascript">
SetupJS()
</script>
<footer>
<p>
Looking for the old static HTML page, here's the <a href="index.html">link</a>.
</p>
<p>
This page's content is licensed 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/80x15/png/by-nc-nd.png" alt="License" title="License" height="16"></a>.
</p>
</footer>
<div id="st"></div>
</body>
</html>

370
index.go.yml Normal file
View File

@ -0,0 +1,370 @@
#This file is (C) Captain ALM
#Under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
cssBaseURL: "resources/assets/base.css"
cssDarkURL: "resources/assets/dark.css"
cssLightURL: "resources/assets/light.css"
jScriptURL: "resources/assets/index.js"
noVideoImageLocation: "resources/assets/novideo.png"
playVideoImageLocation: "resources/assets/video.png"
logoImageLocation: "resources/assets/logo.png"
moonImageLocation: "resources/assets/moon.png"
sunImageLocation: "resources/assets/sun.png"
sortImageLocation: "resources/assets/sort.png"
headerLinks:
Main Portfolio: "https://portfolio.captainalm.com/"
Root Site Home: "https://www.captainalm.com/"
LinkedIn: "https://www.linkedin.com/in/alfred-manville/"
about:
title: "Alfred Manville (Captain ALM)"
content: >
<p>
Hello, I'm Alfred Manville (#age# Years Old) and a third year student at City, University of London.
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, C++, Python, Bash,
Haskell, Processing and Microsoft Smallbasic (I have also dabbled in Batch).
I am currently in the progress of writing infrastructure software in Go, in the past, I wrote a <a href="https://github.com/Captain-ALM/CALM-Console">command console</a>
in VB .net for my own pluggable libraries (I created a CMD emulator to get past the school disabling interactive CMD) and some <a href="https://github.com/Captain-ALM/CALMNetLibSamples">
network communication applications</a> (Including a peer-to-peer <a href="https://github.com/Captain-ALM/C-ALM-VOIP">VOIP client</a>
using NAudio as the audio library and my own network wrapper library, however, it is in need of bug-fixing at the moment).
</p>
<p>
My github username is <a href="https://github.com/Captain-ALM">Captain-ALM</a> and has half my public programming projects,
the other half is located at: <a href="https://code.mrmelon54.com/alfred">https://code.mrmelon54.com/alfred</a>
</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 <a href="https://github.com/Captain-ALM/rdpccgs-ccgsrdp-">accessing the RDP servers</a>
when I was in secondary school (Turns out remote apps is just a glorified application auto-launcher with window size detection).
Here is <a href="https://github.com/Captain-ALM/op_ctrl">Operation Control</a> (Stylized: op_ctrl) used for same-computer exploiting
by wrapping target applications run by other users (Such as via RDP Remote Apps) and then starting a master server to send arbitrary
.net DLLs to execute on the target slaved client.
</p>
<p>
I also <a href="https://subsection.captainalm.com/">bake bread</a> (Mostly learnt from my grandma); although this sub-site is still under construction.
I also play video-games (Check my Youtube Channel or my <a href="https://www.gog.com/u/Captain_ALM">GOG Profile</a>) 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.
Here is my <a href="https://cdn.captainalm.com/download/keys/alfred@captainalm.com.asc">GPG Key</a> for my email address.
</p>
<p>
My CV is available in the following formats: ( <a href="https://cdn.captainalm.com/download/cvs/AlfredManvilleCV-2024.docx">DOCX</a> | <a href="https://cdn.captainalm.com/download/cvs/AlfredManvilleCV-2024.pdf">PDF</a> )
</p>
thumbnailLocation: "resources/assets/imageofyou_t.jpg"
imageLocation: "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>
<p>
The original video is available here: <a href="resources/stream/vid1.mp4">resources/stream/vid1.mp4</a>
</p>
startDate: "01/10/2021"
endDate: "31/10/2021"
videoLocation: "resources/stream/vid-bootcamp.mp4"
videoContentType: "video/mp4"
videoThumbnailLocation: "resources/assets/bootcamp-vid.png"
thumbnailLocations:
- "resources/assets/pic1_t.jpg"
- "resources/assets/pic2_t.jpg"
- "resources/assets/pic3_t.jpg"
- "resources/assets/bootcamp-1_t.jpg"
- "resources/assets/bootcamp-2_t.jpg"
- "resources/assets/bootcamp-3_t.jpg"
imageLocations:
- "resources/assets/pic1.jpg"
- "resources/assets/pic2.jpg"
- "resources/assets/pic3.jpg"
- "resources/assets/bootcamp-1.jpg"
- "resources/assets/bootcamp-2.jpg"
- "resources/assets/bootcamp-3.jpg"
imageAltTexts:
- "Level select screen."
- "Empty content interface (Gameplay)."
- "Level editor screen."
- "Ninjagame source code."
- "Button image source code."
- "Sprite world source code."
- 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>
The game contains a mechanism system allowing for the unlocking of portals (Doors) for the character to progress within and between levels.
The character contains a set of different stamina types, core stamina which if depleted causes a death, leg stamina which is a multiplier for
doing any action that uses legs and arm stamina which is a multiplier for doing any action that uses arms. The game contains a weapon system of
throwing stars, swords and throwable liquids.
</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>
<p>
The original video is available here: <a href="resources/stream/vid2.mp4">resources/stream/vid2.mp4</a>
</p>
startDate: "25/02/2022"
endDate: "08/05/2022"
videoLocation: "resources/stream/vid-ninjaformer-2022.mp4"
videoContentType: "video/mp4"
videoThumbnailLocation: "resources/assets/ninjaformer-vid.png"
thumbnailLocations:
- "resources/assets/pic4_t.jpg"
- "resources/assets/pic5_t.jpg"
- "resources/assets/pic6_t.jpg"
- "resources/assets/ninjaformer-1_t.jpg"
- "resources/assets/ninjaformer-2_t.jpg"
- "resources/assets/ninjaformer-3_t.jpg"
imageLocations:
- "resources/assets/pic4.jpg"
- "resources/assets/pic5.jpg"
- "resources/assets/pic6.jpg"
- "resources/assets/ninjaformer-1.jpg"
- "resources/assets/ninjaformer-2.jpg"
- "resources/assets/ninjaformer-3.jpg"
imageAltTexts:
- "Cave level."
- "Tutorial level."
- "Training level (Editor mode)."
- "Main Menu screen."
- "Pause Menu screen."
- "Audio Settings screen."
- 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/game/dist/ShadowWorkExecutable.zip">https://cdn.captainalm.com/download/game/dist/ShadowWorkExecutable.zip</a>
</p>
<p>
The Design Document is available here: <a href="https://cdn.captainalm.com/download/game/design/Shadow-WIP-Story-Info.pdf">https://cdn.captainalm.com/download/game/design/Shadow-WIP-Story-Info.pdf</a>
</p>
startDate: "20/01/2022"
endDate: "30/01/2022"
videoLocation: "resources/stream/vid-shadowwork.mp4"
videoContentType: "video/mp4"
videoThumbnailLocation: "resources/assets/shadowwork-vid.png"
thumbnailLocations:
- "resources/assets/shadowwork-2_t.jpg"
- "resources/assets/shadowwork-3_t.jpg"
- "resources/assets/shadowwork-4_t.jpg"
- "resources/assets/shadowwork-5_t.jpg"
- "resources/assets/shadowwork-6_t.jpg"
- "resources/assets/shadowwork-1_t.jpg"
imageLocations:
- "resources/assets/shadowwork-2.jpg"
- "resources/assets/shadowwork-3.jpg"
- "resources/assets/shadowwork-4.jpg"
- "resources/assets/shadowwork-5.jpg"
- "resources/assets/shadowwork-6.jpg"
- "resources/assets/shadowwork-1.jpg"
imageAltTexts:
- "Main Menu screen."
- "Controls screen."
- "Bedroom (Beginning game area)."
- "First Puzzle, duality keys."
- "Visualisation of in-game character split."
- "End of the game area."
- 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 License.
</p>
<p>
Find the source code here: <a href="https://code.mrmelon54.com/alfred/cityuni-webserver">https://code.mrmelon54.com/alfred/cityuni-webserver</a>
</p>
startDate: "13/07/2022"
- name: "Python Communicator"
content: >
<p>
After learning python for my portfolio development option, I wanted to showcase what I knew in python.
I usually make a network application in each of the new programming languages that I learn.
For the repo showing me learning python please go to: <a href="https://code.mrmelon54.com/alfred/LearningPy">https://code.mrmelon54.com/alfred/LearningPy</a>
</p>
<p>
This project is a message based network communicator written in python and has a module for networking.
The project allows the sending of text messages and files over a network.
It also comes with a nice twist, in that if you select the pickle protocol, you can inject code into another client by sending a specially crafted message!
</p>
<p>
Find the source code here: <a href="https://github.com/Captain-ALM/CALMPyNetworker">https://github.com/Captain-ALM/CALMPyNetworker</a>
</p>
startDate: "10/12/2022"
endDate: "10/12/2022"
videoLocation: "resources/stream/vid-pycom.mp4"
videoContentType: "video/mp4"
videoThumbnailLocation: "resources/assets/pycom-vid.png"
thumbnailLocations:
- "resources/assets/pycom-1_t.jpg"
- "resources/assets/pycom-2_t.jpg"
- "resources/assets/pycom-3_t.jpg"
- "resources/assets/pycom-4_t.jpg"
imageLocations:
- "resources/assets/pycom-1.jpg"
- "resources/assets/pycom-2.jpg"
- "resources/assets/pycom-3.jpg"
- "resources/assets/pycom-4.jpg"
imageAltTexts:
- "Text Messaging."
- "File Messaging."
- "Exploit Testing."
- "Failed Exploit Testing."
- name: "Group Project - AirVia ATS (AirTicket Sales)"
content: >
<p>
This group project was creating a Ticket Sales system for the fictional company AirVia LTD, for this the group had to both design the implementation and then write the code for it.
Unfortunately, the project was not finished to a state where all the required features were added in and while all of the backend functionality except for the reports existed,
the GUIs to view and control those backends was not available.
Examples of this include: Sales, Transactions, Discounts and Reports where no GUIs were created or finished for any of these components.
The design of the program was followed with a lot of adaptations (Rather than not being followed at all) and it architecturally made sense with the use of facade implementations;
The use of facade allowed for other people to code against an interface while waiting for a controller to be finished which extended the interface.
</p>
<p>
The database system was handled via an abstraction layer I designed and wrote myself which supports 'locking' a record for keeping consistency when multiple instances of the program are running.
This system uses an auxiliary table that has only the primary key column, a record is locked if it does not exist in the auxiliary table (Cannot delete) and is not locked if it does (Cannot insert);
This allows for atomic locking and unlocking of the record.
The implementation requires the record locked for safe access (Loading, Storing).
The abstraction layer makes use of two base classes, one for a single record and one for a table; with tha table one allowing the creation and deletion of the extending table via a schema and name being provided (As seen in the source code).
I also developed a backup system for the database that supports any table with the specifically supported data types used in the tables defined in the schema - removing the need to use third-party programs like SQLDump.
</p>
<p>
In the end, the following features were implemented: Login, Help / Error / Status Bar, Account System + GUI, Blank Types + GUI, Blanks + GUI, Customers + GUI, Discounts, Flexible Discounts, Sales, Transactions, Refunds, Dashboard + Notifications, Database Interfacing + Backup and Rates + GUI.
</p>
<p>
Find the source code here: <a href="https://github.com/karansambee/IN2018-Team-Project/tree/master">https://github.com/karansambee/IN2018-Team-Project/tree/master</a>
</p>
startDate: "01/02/2023"
endDate: "30/04/2023"
videoLocation: "resources/stream/vid-groupproject-2023.mp4"
videoContentType: "video/mp4"
videoThumbnailLocation: "resources/assets/groupproject-vid.png"
thumbnailLocations:
- "resources/assets/groupproject-1_t.jpg"
- "resources/assets/groupproject-2_t.jpg"
- "resources/assets/groupproject-3_t.jpg"
- "resources/assets/groupproject-4_t.jpg"
- "resources/assets/groupproject-5_t.jpg"
- "resources/assets/groupproject-6_t.jpg"
- "resources/assets/groupproject-7_t.jpg"
- "resources/assets/groupproject-8_t.jpg"
- "resources/assets/groupproject-9_t.jpg"
- "resources/assets/groupproject-10_t.jpg"
imageLocations:
- "resources/assets/groupproject-1.png"
- "resources/assets/groupproject-2.png"
- "resources/assets/groupproject-3.png"
- "resources/assets/groupproject-4.png"
- "resources/assets/groupproject-5.png"
- "resources/assets/groupproject-6.png"
- "resources/assets/groupproject-7.png"
- "resources/assets/groupproject-8.png"
- "resources/assets/groupproject-9.png"
- "resources/assets/groupproject-10.png"
imageAltTexts:
- "Logon Interface."
- "Administrator Dashboard Interface."
- "Blank Modifier."
- "Customer Creator."
- "Disabling an Account."
- "Rate Creator."
- "Blank Type Editor."
- "Help on Force Unlocking (Database Manager Interface)."
- "Account Editor on a Manager within The Dashboard Interface."
- "Part of the Main.java source code Screenshot."
- name: "City-University Promotional Video"
content: >
<p>
Here, I star in an interview for City, University of London's Promotional Marketing Campaign. Join <a href="https://www.city.ac.uk/">City</a>!
</p>
<p>
Find the video here: <a href="https://www.youtube.com/watch?v=tOccImgskec">https://www.youtube.com/watch?v=tOccImgskec</a>
</p>
<p>
Find the general School of Science and Technology video here: <a href="https://www.youtube.com/watch?v=pkTCf4CWFSY">https://www.youtube.com/watch?v=pkTCf4CWFSY</a>
</p>
startDate: "05/06/2023"
endDate: "05/06/2023"
videoLocation: "https://www.youtube.com/watch?v=tOccImgskec"
videoContentType: "text/uri-list"
videoThumbnailLocation: "resources/assets/citypromo-vid.png"
- name: "Decide Quiz - GCloud City"
content: >
<p>
This project was <a href="https://cdn.captainalm.com/download/gcloudcity/CloudApplicationSpecification-AlfredManville-MohammadMasood.docx">designed</a> <a href="https://cdn.captainalm.com/download/gcloudcity/CloudPresentation-AlfredManville-MohammadMasood.pptx">to</a> operate as a Kahoot clone although the front-end never got completed by the other member of the pair (This was written in React).
</p>
<p>
This was created in a pair as part of the Cloud Computing module at City, University of London. It was built for use using google cloud and therefore
uses many Google Cloud Platform Technologies which are listed and shown off in the video.
</p>
<p>
The communication protocol uses JSON packets passed either through a WebSocket connection or a REST based protocol that polls the server for data while sending the queued packets.
The REST connection is used as a fallback system where WebSockets do not work; in addition, a REST connection is made to the master server to first work out which app server has
the least load, once found, this is sent back to the client where the client will then attempt a WebSocket connection and, on failure, a REST session is created (A key is returned
which is then used as a parameter in subsequent communications). In the event there is no more capacity, a service unavailable error is sent and a new VM will be spun up, in the case
that any are left. The source code for this library can be found at <a href="https://github.com/Captain-ALM/gc-c-com">https://github.com/Captain-ALM/gc-c-com</a> and the master server source
code can be found at <a href="https://github.com/Captain-ALM/gc-c-master-srv">https://github.com/Captain-ALM/gc-c-master-srv</a>.
</p>
<p>
The main game loop was supposed to allow user generated quizzes once logged in along-side allowing for public quizzes that could be used and copied by other users.
Only logged in users could start games where any user - both logged in or not - could join and answer questions within a set amount of time, with the person answering
the fastest getting more points than people who answer later. The system would also have a leaderboard shown at the end of each question; it was also designed to recover
from crashes and resume from the last question executed. The source code for the app server can be found at <a href="https://github.com/Captain-ALM/gc-c-app-srv">
https://github.com/Captain-ALM/gc-c-app-srv</a> where all this functionality does exist in the backend; the database source code can be found at <a href="https://github.com/Captain-ALM/gc-c-db">https://github.com/Captain-ALM/gc-c-db</a>.
</p>
startDate: "27/11/2023"
endDate: "14/01/2024"
videoLocation: "resources/stream/vid-gc-c-v2.mp4"
videoContentType: "video/mp4"
videoThumbnailLocation: "resources/assets/vid-gc-c-v2.png"

369
index.js Normal file
View File

@ -0,0 +1,369 @@
/*
This file is (C) Captain ALM
Under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
*/
var EntryData = []
var EntryIndices = []
var SortOrderStateI = true
var SortOrderBStateI = true
var SortOrderEnabled = false
var SortValue = ""
var OrderValue = ""
function SetupJS() {
SetupIndexArray()
SetupJSTheme()
SetupJSHSO()
SetupJSSOI()
SetupJSRSN()
}
function CreateEntry(id, name, videourl, videotype, start, end, duration) {
EntryData[id] = {
name: name,
videourl: videourl,
videotype: videotype,
start: Date.parse(start),
end: Date.parse(end),
duration : parseInt(duration, 10)
};
}
function CreateVideoPlaceholder(id,phImageURL) {
var imgPH = document.createElement("img")
imgPH.src = phImageURL
imgPH.id = "play-"+id
imgPH.alt = "Play Video"
imgPH.title = "Play"
imgPH.width = 360
imgPH.style.cursor = "pointer"
if (document.addEventListener) {
imgPH.addEventListener("click", function () {ActivateVideo(id);})
} else {
imgPH.setAttribute("onclick", "ActivateVideo("+id+");")
imgPH.onclick = function () {ActivateVideo(id);}
}
document.getElementById("video-" + id).appendChild(imgPH)
}
function ActivateVideo(id) {
var holder = document.getElementById("video-" + id)
holder.removeChild(document.getElementById("play-"+id))
var vid = document.createElement("video")
vid.controls = true
vid.width = 360
var vids = document.createElement("source")
vids.src = EntryData[id].videourl
vids.type = EntryData[id].videotype
var vida = document.createElement("a")
vida.href = EntryData[id].videourl
vida.innerText = "The Video"
vid.appendChild(vids)
vid.appendChild(vida)
holder.appendChild(vid)
if (vid.play) {vid.play();}
}
function SetupIndexArray() {
for (var i = 0; i < EntryData.length; i++) {
EntryIndices[i] = i
}
}
function SetupJSTheme() {
var th = document.getElementById("theme")
th.href = "#"
new Image().src = MoonImageURL //Preload I hope
if (document.addEventListener) {
th.addEventListener("click", ToggleTheme)
} else {
th.setAttribute("onclick", "ToggleTheme();")
th.onclick = ToggleTheme
}
}
function ReplaceHistory(url) {
var s = true
if (window.history) {
if (window.history.replaceState) {
window.history.replaceState({}, "", url)
s = false
}
}
if (s) {
document.location.href = url
}
}
function ToggleTheme() {
var th = document.getElementById("theme")
var thimg = document.getElementById("theme-img")
var thsty = document.getElementById("style-theme")
var logo = document.getElementById("logo")
var url = document.location.href
url = url.split("#", 1)[0].split('?', 1)[0]
if (document.getElementById("so-theme")) {
thimg.src = SunImageURL
thimg.alt = "()"
th.title = "Switch to Light Mode"
document.getElementById("so-form").removeChild(document.getElementById("so-theme"))
logo.href = "?"
ReplaceHistory(url+"?"+TheParameters+"#")
thsty.href = CssDarkURL
} else {
thimg.src = MoonImageURL
thimg.alt = "{"
th.title = "Switch to Dark Mode"
var thi = document.createElement("input")
thi.name = "light"
thi.type = "hidden"
thi.id = "so-theme"
document.getElementById("so-form").appendChild(thi)
logo.href = "?light"
if (TheParameters === "") {
ReplaceHistory(url+"?light#")
} else {
ReplaceHistory(url+"?light&"+TheParameters+"#")
}
thsty.href = CssLightURL
}
}
function SetupJSHSO() {
var pb = document.getElementById("sort-menu-button")
var pane = document.getElementById("so-pane")
if (document.addEventListener) {
document.addEventListener("click", HandleGlobalClick)
pb.addEventListener("mouseover", HandleSortOrderBEnter)
pb.addEventListener("mouseout", HandleSortOrderBLeave)
pane.addEventListener("mouseover", HandleSortOrderEnter)
pane.addEventListener("mouseout", HandleSortOrderLeave)
} else {
document.parentElement.setAttribute("onclick", "HandleGlobalClick();")
pb.setAttribute("onmouseover", "HandleSortOrderBEnter();")
pb.setAttribute("onmouseout", "HandleSortOrderBLeave();")
pane.setAttribute("onmouseover", "HandleSortOrderEnter();")
pane.setAttribute("onmouseout", "HandleSortOrderLeave();")
document.parentElement.onclick = HandleGlobalClick
pb.onmouseover = HandleSortOrderBEnter
pb.onmouseout = HandleSortOrderBLeave
pane.onmouseover = HandleSortOrderEnter
pane.onmouseout = HandleSortOrderLeave
}
}
function HandleGlobalClick() {
if (SortOrderStateI && SortOrderBStateI) {document.getElementById("sort-menu").checked = false;}
}
function HandleSortOrderBEnter() {
SortOrderBStateI = false
}
function HandleSortOrderBLeave(){
SortOrderBStateI = true
}
function HandleSortOrderEnter() {
SortOrderStateI = false
}
function HandleSortOrderLeave(){
SortOrderStateI = true
}
function SetupJSSOI() {
var submit = document.getElementById("so-submit")
if (submit.parentNode) {submit.parentNode.removeChild(submit);}
var oc = document.getElementById("so-order")
OrderValue = oc.value
var sc = document.getElementById("so-sort")
SortValue = sc.value
if (document.addEventListener) {
oc.addEventListener("change", HandleSortOrderChange)
sc.addEventListener("change", HandleSortOrderChange)
} else {
oc.setAttribute("onchange", "HandleSortOrderChange();")
sc.setAttribute("onchange", "HandleSortOrderChange();")
oc.onchange = HandleSortOrderChange
sc.onchange = HandleSortOrderChange
}
SortOrderEnabled = true
}
function HandleSortOrderChange() {
if (SortOrderEnabled) {EntrySort(document.getElementById("so-order").value, document.getElementById("so-sort").value);}
}
function EntrySort(o, s) {
var ts = s.toString().toLowerCase()
var chg = false
if (SortValue !== s) {
chg = true
SortValue = s
}
if (chg || OrderValue !== o) {
if (ts === "asc" || ts === "ascending") {
ts = 1
} else {
ts = -1
}
var to = o.toString().toLowerCase()
if (to === "start") {
if (ts < 0) {
EntryIndices = EntryIndices.sort(SortStartD)
} else {
EntryIndices = EntryIndices.sort(SortStartA)
}
} else if (to === "end") {
if (ts < 0) {
EntryIndices = EntryIndices.sort(SortEndD)
} else {
EntryIndices = EntryIndices.sort(SortEndA)
}
} else if (to === "name") {
if (ts < 0) {
EntryIndices = EntryIndices.sort(SortNameD)
} else {
EntryIndices = EntryIndices.sort(SortNameA)
}
} else if (to === "duration") {
if (ts < 0) {
EntryIndices = EntryIndices.sort(SortDurationD)
} else {
EntryIndices = EntryIndices.sort(SortDurationA)
}
}
chg = true
OrderValue = o
}
if (chg) {
TheParameters = "order="+OrderValue+"&sort="+SortValue
var url = document.location.href
url = url.split("#", 1)[0].split('?', 1)[0]
if (document.getElementById("so-theme")) {
ReplaceHistory(url+"?light&"+TheParameters)
} else {
ReplaceHistory(url+"?"+TheParameters)
}
for (var i = 0; i < EntryIndices.length; i++) {
var tNode = document.getElementById("entry-"+EntryIndices[i])
var pNode = tNode.parentNode
tNode = pNode.removeChild(tNode)
pNode.appendChild(tNode)
}
}
}
function SortStartA(a, b) {
if (EntryData[a].start < EntryData[b].start) {
return -1
} else {
return 1
}
}
function SortStartD(a, b) {
if (EntryData[a].start > EntryData[b].start) {
return -1
} else {
return 1
}
}
function SortEndA(a, b) {
if (EntryData[a].end < EntryData[b].end) {
return -1
} else {
return 1
}
}
function SortEndD(a, b) {
if (EntryData[a].end > EntryData[b].end) {
return -1
} else {
return 1
}
}
function SortNameA(a, b) {
if (EntryData[a].name < EntryData[b].name) {
return -1
} else {
return 1
}
}
function SortNameD(a, b) {
if (EntryData[a].name > EntryData[b].name) {
return -1
} else {
return 1
}
}
function SortDurationA(a, b) {
if (EntryData[a].duration < EntryData[b].duration) {
return -1
} else {
return 1
}
}
function SortDurationD(a, b) {
if (EntryData[a].duration > EntryData[b].duration) {
return -1
} else {
return 1
}
}
function SetupJSRSN() {
if (window.addEventListener) {
window.addEventListener("resize", PerformNavResize)
} else {
window.setAttribute("onresize", "PerformNavResize();")
window.onresize = PerformNavResize
}
PerformNavResize()
}
function PerformNavResize() {
var ww = 0
if (window.innerWidth && window.innerWidth !== 10) {
ww = window.innerWidth
} else {
var ht = document.getElementsByTagName("html")
if (ht && ht.length > 0) {ww = ht[0].clientWidth;}
}
if (ww > 0) {
var maxbarsz = ww - 342;
var men = document.getElementById("menu")
var vmen = document.getElementById("vmenu")
if (men && vmen) {
if (ww > 679) {
while (vmen.childNodes.length > 0) {InsertBefore(men, vmen.removeChild(vmen.childNodes[vmen.childNodes.length - 1]));}
} else {
var vmeni
var mensz = 0
var menc = []
var imenc = 0
for (vmeni = 0; vmeni < vmen.childNodes.length; vmeni++) {
if (vmen.childNodes[vmeni].nodeType === Node.ELEMENT_NODE) {
if (mensz+vmen.childNodes[vmeni].clientWidth > maxbarsz) {menc[imenc] = vmen.childNodes[vmeni]; imenc++;}
mensz += vmen.childNodes[vmeni].clientWidth
}
}
if (menc.length > 0) {
for (vmeni = 0; vmeni < menc.length; vmeni++) {vmen.removeChild(menc[vmeni]);}
for (vmeni = menc.length - 1; vmeni >= 0; vmeni--) {InsertBefore(men, menc[vmeni]);}
} else {
for (vmeni = 0; vmeni < men.childNodes.length; vmeni++) {
if (men.childNodes[vmeni].nodeType === Node.ELEMENT_NODE) {
var mena = GetFirstSubElement(men.childNodes[vmeni], 0)
var menaw = GetNavTextWidth(mena.textContent)
if (mensz+menaw <= maxbarsz) {menc[imenc] = men.childNodes[vmeni]; imenc++;}
mensz += menaw
}
}
for (vmeni = 0; vmeni < menc.length; vmeni++) {vmen.appendChild(men.removeChild(menc[vmeni]));}
}
}
}
}
}
function GetFirstSubElement(t,r) {
for (var gfsei = 0; gfsei < t.childNodes.length; gfsei++) {
if (t.childNodes[gfsei].nodeType === Node.ELEMENT_NODE) {
if (r < 1) {return t.childNodes[gfsei];} else {return GetFirstSubElement(t.childNodes[gfsei], r - 1);}
}
}
return t
}
function InsertBefore(p,c) {
if (p.childNodes.length > 0) {p.insertBefore(c, p.childNodes[0]);} else {p.appendChild(c);}
}
function GetNavTextWidth(s) {
var st = document.getElementById("st")
if (st) {
st.textContent = s
var trw = st.clientWidth
st.textContent = ""
return trw
}
return 8 * s.length + 32
}

40
light.css Normal file
View File

@ -0,0 +1,40 @@
/*
This file is (C) Captain ALM
Under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License
*/
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, .vmenu a, .so-pane > form > div > * > *{
color: #1f1f1f;
}
.home-button:hover, .menu a:hover, .vmenu a:hover, .hmb:hover, .sort-button:hover, .sort-menu:checked ~ .sort-button, .sort-button-active, .so-pane > form > div > span > input, .so-pane > form > div > div > select{
background-color: #9f9f9e;
}
.hmb-line, .hmb-line::before, .hmb-line::after{
background: #1f1f1f;
}
.main-box{
background-color: #f0f0f0;
}
.item-table{
background-color: #c0c0c0;
border-color: #0a214c;
}
.so-pane, .header{
border-color: #909090;
}
.image-box, .item-heading{
background-color: #b0b0b0;
}
.image-box > a{
border-color: #4f4f0f;
}

View File

@ -45,6 +45,7 @@ func (gipg *goInfoPage) GetCacheIDExtension(urlParameters url.Values) string {
type goInfoTemplateMarshal struct { type goInfoTemplateMarshal struct {
FullOutput bool FullOutput bool
CurrentTime time.Time
RegisteredPages []string RegisteredPages []string
CachedPages []string CachedPages []string
ProcessID int ProcessID int
@ -98,6 +99,7 @@ func (gipg *goInfoPage) GetContents(urlParameters url.Values) (contentType strin
theBuffer := &io.BufferedWriter{} theBuffer := &io.BufferedWriter{}
err = theTemplate.ExecuteTemplate(theBuffer, templateName, &goInfoTemplateMarshal{ err = theTemplate.ExecuteTemplate(theBuffer, templateName, &goInfoTemplateMarshal{
FullOutput: urlParameters.Has("full"), FullOutput: urlParameters.Has("full"),
CurrentTime: time.Now(),
RegisteredPages: regPages, RegisteredPages: regPages,
CachedPages: cacPages, CachedPages: cacPages,
ProcessID: os.Getpid(), ProcessID: os.Getpid(),

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
@ -42,15 +43,24 @@ func NewPageHandler(config conf.ServeYaml) *PageHandler {
CacheSettings: config.CacheSettings, CacheSettings: config.CacheSettings,
} }
if config.EnableGoInfoPage { if config.EnableGoInfoPage {
toReturn.PageProviders = GetProviders(config.CacheSettings.EnableTemplateCaching, config.DataStorage, toReturn) toReturn.PageProviders = GetProviders(config.CacheSettings.EnableTemplateCaching, config.GetDataStoragePath(), toReturn, config.GetTemplateStoragePath(), config.PageSettings, config.YmlDataFallback)
} else { } else {
toReturn.PageProviders = GetProviders(config.CacheSettings.EnableTemplateCaching, config.DataStorage, nil) toReturn.PageProviders = GetProviders(config.CacheSettings.EnableTemplateCaching, config.GetDataStoragePath(), nil, config.GetTemplateStoragePath(), config.PageSettings, config.YmlDataFallback)
} }
return toReturn return toReturn
} }
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}
@ -271,6 +286,9 @@ func (ph *PageHandler) GetRegisteredPages() []string {
} }
func (ph *PageHandler) GetCachedPages() []string { func (ph *PageHandler) GetCachedPages() []string {
if ph.pageContentsCacheRWMutex == nil {
return make([]string, 0)
}
ph.pageContentsCacheRWMutex.RLock() ph.pageContentsCacheRWMutex.RLock()
defer ph.pageContentsCacheRWMutex.RUnlock() defer ph.pageContentsCacheRWMutex.RUnlock()
pages := make([]string, len(ph.PageContentsCache)) pages := make([]string, len(ph.PageContentsCache))
@ -283,6 +301,9 @@ func (ph *PageHandler) GetCachedPages() []string {
} }
func (ph *PageHandler) GetNumberOfCachedPages() int { func (ph *PageHandler) GetNumberOfCachedPages() int {
if ph.pageContentsCacheRWMutex == nil {
return 0
}
ph.pageContentsCacheRWMutex.RLock() ph.pageContentsCacheRWMutex.RLock()
defer ph.pageContentsCacheRWMutex.RUnlock() defer ph.pageContentsCacheRWMutex.RUnlock()
return len(ph.PageContentsCache) return len(ph.PageContentsCache)

View File

@ -1,18 +1,26 @@
package pageHandler package pageHandler
import "golang.captainalm.com/cityuni-webserver/pageHandler/pages/index" import (
"golang.captainalm.com/cityuni-webserver/conf"
"golang.captainalm.com/cityuni-webserver/pageHandler/pages/index"
"strings"
)
var providers map[string]PageProvider var providers map[string]PageProvider
func GetProviders(cacheTemplates bool, dataStorage string, pageHandler *PageHandler) map[string]PageProvider { func GetProviders(cacheTemplates bool, dataStorage string, pageHandler *PageHandler, templateStorage string, pageSettings []conf.PageYaml, ymlDataFallback bool) map[string]PageProvider {
if providers == nil { if providers == nil {
providers = make(map[string]PageProvider) providers = make(map[string]PageProvider)
if pageHandler != nil { if pageHandler != nil {
infoPage := newGoInfoPage(pageHandler, dataStorage, cacheTemplates) infoPage := newGoInfoPage(pageHandler, dataStorage, cacheTemplates)
providers[infoPage.GetPath()] = infoPage //Go Information Page providers[infoPage.GetPath()] = infoPage //Go Information Page
} }
indexPage := index.NewPage(dataStorage, cacheTemplates) for _, cpg := range pageSettings { //Register pages
if strings.EqualFold(cpg.PageName, index.PageName) {
indexPage := index.NewPage(dataStorage, cacheTemplates, templateStorage, cpg.GetPagePath(), ymlDataFallback)
providers[indexPage.GetPath()] = indexPage providers[indexPage.GetPath()] = indexPage
} }
}
}
return providers return providers
} }

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,39 @@
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"` PlayVideoImageLocation template.URL `yaml:"playVideoImageLocation"`
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,55 @@
package index package index
import ( import (
"golang.captainalm.com/cityuni-webserver/utils/yaml"
"html/template" "html/template"
"math"
"net/http"
"strings"
"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"`
VideoThumbnailLocation template.URL `yaml:"videoThumbnailLocation"`
}
type ImageReference struct {
ThumbnailLocation template.URL
ImageLocation template.URL
ImageAltText string
}
func (ey EntryYaml) GetVideoThumbnail(usual template.URL) template.URL {
if ey.VideoThumbnailLocation == "" {
return usual
} else {
return ey.VideoThumbnailLocation
}
}
func (ey EntryYaml) IsVideoLink() bool {
return strings.EqualFold(ey.VideoContentType, "text/uri-list")
} }
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 +58,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 +75,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

@ -1,6 +1,7 @@
package index package index
import ( import (
"errors"
"golang.captainalm.com/cityuni-webserver/utils/io" "golang.captainalm.com/cityuni-webserver/utils/io"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"html/template" "html/template"
@ -12,24 +13,32 @@ import (
"time" "time"
) )
const PageName = "index"
const templateName = "index.go.html" const templateName = "index.go.html"
const yamlName = "index.go.yml"
func NewPage(dataStore string, cacheTemplates bool) *Page { func NewPage(dataStore string, cacheTemplates bool, templateStore string, pagePath string, ymlDataFallback 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, YMLDataFallback: ymlDataFallback,
StoredDataMutex: &sync.Mutex{}, PagePath: pagePath,
DataPath: path.Join(dataStore, pagePath),
TemplatePath: path.Join(templateStore, templateName),
StoredDataMutex: sdm,
PageTemplateMutex: ptm, PageTemplateMutex: ptm,
} }
return pageToReturn return pageToReturn
} }
type Page struct { type Page struct {
DataStore string YMLDataFallback bool
PagePath string
DataPath string
TemplatePath string
StoredDataMutex *sync.Mutex StoredDataMutex *sync.Mutex
StoredData *DataYaml StoredData *DataYaml
LastModifiedData time.Time LastModifiedData time.Time
@ -39,7 +48,7 @@ type Page struct {
} }
func (p *Page) GetPath() string { func (p *Page) GetPath() string {
return "/index.go" return p.PagePath
} }
func (p *Page) GetLastModified() time.Time { func (p *Page) GetLastModified() time.Time {
@ -51,6 +60,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 +79,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 +97,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":
@ -118,16 +136,12 @@ func (p *Page) getPageTemplate() (*template.Template, error) {
defer p.PageTemplateMutex.Unlock() defer p.PageTemplateMutex.Unlock()
} }
if p.PageTemplate == nil { if p.PageTemplate == nil {
thePath := templateName stat, err := os.Stat(p.TemplatePath)
if p.DataStore != "" {
thePath = path.Join(p.DataStore, thePath)
}
stat, err := os.Stat(thePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
p.LastModifiedTemplate = stat.ModTime() p.LastModifiedTemplate = stat.ModTime()
loadedData, err := os.ReadFile(thePath) loadedData, err := os.ReadFile(p.TemplatePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -150,14 +164,19 @@ func (p *Page) getPageData() (*DataYaml, error) {
defer p.StoredDataMutex.Unlock() defer p.StoredDataMutex.Unlock()
} }
if p.StoredData == nil { if p.StoredData == nil {
thePath := yamlName thePath := p.DataPath
if p.DataStore != "" {
thePath = path.Join(p.DataStore, thePath)
}
stat, err := os.Stat(thePath) stat, err := os.Stat(thePath)
if err != nil {
if p.YMLDataFallback && errors.Is(err, os.ErrNotExist) {
thePath += ".yml"
stat, err = os.Stat(thePath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} else {
return nil, err
}
}
p.LastModifiedData = stat.ModTime() p.LastModifiedData = stat.ModTime()
fileHandle, err := os.Open(thePath) fileHandle, err := os.Open(thePath)
if err != nil { if err != nil {
@ -178,15 +197,16 @@ func (p *Page) getPageData() (*DataYaml, error) {
p.StoredData = dataYaml p.StoredData = dataYaml
} }
return dataYaml, nil return dataYaml, nil
} else { } else {
return p.StoredData, nil return p.StoredData, nil
} }
} }
func getSortValue(toCheckIn string) int8 { func getSortValue(toCheckIn string) int8 {
if toCheckIn == "desc" || toCheckIn == "descending" { if toCheckIn == "asc" || toCheckIn == "ascending" {
return -1
} else {
return 1 return 1
} else {
return -1
} }
} }

View File

@ -1,26 +1,31 @@
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
OrderDuration int8 OrderDuration int8
Light bool Light bool
Counter int
} }
func (m Marshal) GetEntries() (toReturn []EntryYaml) { 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 {
@ -55,3 +60,9 @@ func (m Marshal) GetEntries() (toReturn []EntryYaml) {
} }
return toReturn return toReturn
} }
func (m *Marshal) CounterPlusPlus() int {
toret := m.Counter
m.Counter++
return toret
}

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
}