forked from hswaw/hscloud
hswaw/site: wip new layout
Change-Id: I4da3a668429dee42c7292accb9e24b93703f1538
This commit is contained in:
parent
c35d52b19e
commit
717aad4ac6
14 changed files with 767 additions and 128 deletions
|
@ -5,6 +5,7 @@ go_library(
|
|||
name = "go_default_library",
|
||||
srcs = [
|
||||
"at.go",
|
||||
"events.go",
|
||||
"feeds.go",
|
||||
"main.go",
|
||||
"spaceapi.go",
|
||||
|
@ -41,5 +42,5 @@ container_push(
|
|||
format = "Docker",
|
||||
registry = "registry.k0.hswaw.net",
|
||||
repository = "q3k/hswaw-site",
|
||||
tag = "1622585979-{STABLE_GIT_COMMIT}",
|
||||
tag = "1626124964-{STABLE_GIT_COMMIT}",
|
||||
)
|
||||
|
|
|
@ -19,6 +19,8 @@ type UpcomingEvent struct {
|
|||
UID string
|
||||
// Summary is the 'title' of the event, usually a short one-liner.
|
||||
Summary string
|
||||
// Full description of event. Might contain multiple lines of test.
|
||||
Description string
|
||||
// Start and End of the events, potentially whole-day dates. See EventTime
|
||||
// for more information.
|
||||
// If Start is WholeDay then so is End, and vice-versa.
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
|
@ -57,6 +58,13 @@ func parseUpcomingEvents(now time.Time, data io.Reader) ([]*UpcomingEvent, error
|
|||
}
|
||||
summary := summaryProp.Value
|
||||
|
||||
var description string
|
||||
descriptionProp := event.GetProperty(ics.ComponentPropertyDescription)
|
||||
if descriptionProp != nil && descriptionProp.Value != "" {
|
||||
// The ICS/iCal description has escaped newlines. Undo that.
|
||||
description = strings.ReplaceAll(descriptionProp.Value, `\n`, "\n")
|
||||
}
|
||||
|
||||
status := event.GetProperty(ics.ComponentPropertyStatus)
|
||||
tentative := false
|
||||
if status != nil {
|
||||
|
@ -87,11 +95,12 @@ func parseUpcomingEvents(now time.Time, data io.Reader) ([]*UpcomingEvent, error
|
|||
}
|
||||
|
||||
u := &UpcomingEvent{
|
||||
UID: uid,
|
||||
Summary: summary,
|
||||
Start: start,
|
||||
End: end,
|
||||
Tentative: tentative,
|
||||
UID: uid,
|
||||
Summary: summary,
|
||||
Description: description,
|
||||
Start: start,
|
||||
End: end,
|
||||
Tentative: tentative,
|
||||
}
|
||||
if u.Elapsed(now) {
|
||||
continue
|
||||
|
|
|
@ -22,8 +22,9 @@ func TestUpcomingEvents(t *testing.T) {
|
|||
|
||||
want := []*UpcomingEvent{
|
||||
{
|
||||
UID: "65cd51ba-2fd7-475e-a274-61d19c186b66",
|
||||
Summary: "test event please ignore",
|
||||
UID: "65cd51ba-2fd7-475e-a274-61d19c186b66",
|
||||
Summary: "test event please ignore",
|
||||
Description: "I am a description",
|
||||
Start: &EventTime{
|
||||
Time: time.Unix(1626091200, 0),
|
||||
},
|
||||
|
@ -32,8 +33,9 @@ func TestUpcomingEvents(t *testing.T) {
|
|||
},
|
||||
},
|
||||
{
|
||||
UID: "2f874784-1e09-4cdc-8ae6-185c9ee36be0",
|
||||
Summary: "many days",
|
||||
UID: "2f874784-1e09-4cdc-8ae6-185c9ee36be0",
|
||||
Summary: "many days",
|
||||
Description: "I am a multiline\n\ndescription\n\nwith a link: https://example.com/foo\n\nbarfoo",
|
||||
Start: &EventTime{
|
||||
Time: time.Unix(1626134400, 0),
|
||||
WholeDay: true,
|
||||
|
|
45
hswaw/site/events.go
Normal file
45
hswaw/site/events.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"code.hackerspace.pl/hscloud/hswaw/site/calendar"
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
func (s *service) eventsWorker(ctx context.Context) {
|
||||
get := func() {
|
||||
events, err := calendar.GetUpcomingEvents(ctx, time.Now())
|
||||
if err != nil {
|
||||
glog.Errorf("Geting events failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
s.eventsMu.Lock()
|
||||
s.events = events
|
||||
s.eventsMu.Unlock()
|
||||
}
|
||||
// Perform initial fetch.
|
||||
get()
|
||||
|
||||
// .. and update very minute.
|
||||
t := time.NewTicker(time.Minute)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) getEvents() []*calendar.UpcomingEvent {
|
||||
s.eventsMu.RLock()
|
||||
events := s.events
|
||||
s.eventsMu.RUnlock()
|
||||
return events
|
||||
}
|
|
@ -3,15 +3,18 @@ package main
|
|||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.hackerspace.pl/hscloud/go/mirko"
|
||||
"github.com/golang/glog"
|
||||
|
||||
"code.hackerspace.pl/hscloud/hswaw/site/calendar"
|
||||
"code.hackerspace.pl/hscloud/hswaw/site/static"
|
||||
)
|
||||
|
||||
|
@ -25,12 +28,20 @@ type service struct {
|
|||
feeds map[string]*atomFeed
|
||||
// feedsMu locks the feeds field.
|
||||
feedsMu sync.RWMutex
|
||||
|
||||
// events is a list of upcoming events, sorted so the first event is the
|
||||
// one that will happen the soonests.
|
||||
events []*calendar.UpcomingEvent
|
||||
// eventsMu locks the events field.
|
||||
eventsMu sync.RWMutex
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&flagSitePublic, "site_public", "0.0.0.0:8080", "Address at which to serve public HTTP requests")
|
||||
flag.Parse()
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
mi := mirko.New()
|
||||
if err := mi.Listen(); err != nil {
|
||||
glog.Exitf("Listen failed: %v", err)
|
||||
|
@ -38,6 +49,7 @@ func main() {
|
|||
|
||||
s := &service{}
|
||||
go s.feedWorker(mi.Context())
|
||||
go s.eventsWorker(mi.Context())
|
||||
|
||||
mux := http.NewServeMux()
|
||||
s.registerHTTP(mux)
|
||||
|
@ -103,5 +115,7 @@ func (s *service) handleHTTPStatic(w http.ResponseWriter, r *http.Request) {
|
|||
func (s *service) registerHTTP(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/static/", s.handleHTTPStatic)
|
||||
mux.HandleFunc("/spaceapi", s.handleSpaceAPI)
|
||||
mux.HandleFunc("/events.json", s.handleJSONEvents)
|
||||
mux.HandleFunc("/event/", s.handleEvent)
|
||||
mux.HandleFunc("/", s.handleIndex)
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@ load("@io_bazel_rules_go//extras:embed_data.bzl", "go_embed_data")
|
|||
go_embed_data(
|
||||
name = "static",
|
||||
srcs = [
|
||||
"animations.js",
|
||||
"landing.css",
|
||||
"syrenka.png",
|
||||
"led.js",
|
||||
"neon-syrenka.svg",
|
||||
"@com_npmjs_leaflet//:distfiles",
|
||||
"space.jpg",
|
||||
],
|
||||
package = "static",
|
||||
)
|
||||
|
|
273
hswaw/site/static/animations.js
Normal file
273
hswaw/site/static/animations.js
Normal file
|
@ -0,0 +1,273 @@
|
|||
// To add your own animation, extend 'Animation' and implement draw(), then add
|
||||
// your animation's class name to the list at the bottom of the script.
|
||||
|
||||
class Animation {
|
||||
// The constructor for Animation is called by the site rendering code when
|
||||
// the site loads, so it should be fairly fast. Any delay causes the LED
|
||||
// panel to take longer to load.
|
||||
constructor(nx, ny) {
|
||||
// LED array, indexed by x then y.
|
||||
let leds = new Array(nx);
|
||||
for (let x = 0; x < nx; x++) {
|
||||
leds[x] = new Array(ny);
|
||||
for (let y = 0; y < ny; y++) {
|
||||
leds[x][y] = [0.0, 0.0, 0.0];
|
||||
}
|
||||
}
|
||||
this.leds = leds;
|
||||
|
||||
// Number of LEDs, X and Y.
|
||||
this.nx = nx;
|
||||
this.ny = ny;
|
||||
}
|
||||
|
||||
// Helper function that converts from HSV to RGB, can be used by your draw
|
||||
// code.
|
||||
// H, S and V values must be [0..1].
|
||||
hsv2rgb(h, s, v) {
|
||||
const i = Math.floor(h * 6);
|
||||
const f = h * 6 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const t = v * (1 - (1 - f) * s);
|
||||
|
||||
let r, g, b;
|
||||
switch (i % 6) {
|
||||
case 0: r = v, g = t, b = p; break;
|
||||
case 1: r = q, g = v, b = p; break;
|
||||
case 2: r = p, g = v, b = t; break;
|
||||
case 3: r = p, g = q, b = v; break;
|
||||
case 4: r = t, g = p, b = v; break;
|
||||
case 5: r = v, g = p, b = q; break;
|
||||
}
|
||||
return [r, g, b];
|
||||
}
|
||||
|
||||
draw(ts) {
|
||||
// Implement your animation here.
|
||||
// The 'ts' argument is a timestamp in seconds, floating point, of the
|
||||
// frame being drawn.
|
||||
//
|
||||
// Your implementation should write to this.leds, which is two
|
||||
// dimensional array containing [r,g,b] values. Colour values are [0..1].
|
||||
//
|
||||
// X coordinates are [0 .. this.nx), Y coordinates are [0 .. this.ny).
|
||||
// The coordinate system is with X==Y==0 in the top-left part of the
|
||||
// display.
|
||||
//
|
||||
// For example, for a 3x3 LED display the coordinates are as follors:
|
||||
//
|
||||
// (x:0 y:0) (x:1 y:0) (x:2 y:0)
|
||||
// (x:0 y:1) (x:1 y:1) (x:2 y:1)
|
||||
// (x:0 y:2) (x:1 y:2) (x:2 y:2)
|
||||
//
|
||||
// The LED array (this.leds) is indexed by X first and Y second.
|
||||
//
|
||||
// For example, to set the LED red at coordinates x:1 y:2:
|
||||
//
|
||||
// this.leds[1][2] = [1.0, 0.0, 0.0];
|
||||
}
|
||||
}
|
||||
|
||||
// 'Snake' chase animation, a simple RGB chase that goes around in a zigzag.
|
||||
// By q3k.
|
||||
class SnakeChase extends Animation {
|
||||
draw(ts) {
|
||||
const nx = this.nx;
|
||||
const ny = this.ny;
|
||||
// Iterate over all pixels column-wise.
|
||||
for (let i = 0; i < (nx*ny); i++) {
|
||||
let x = Math.floor(i / ny);
|
||||
let y = i % ny;
|
||||
|
||||
// Flip every second row to get the 'snaking'/'zigzag' effect
|
||||
// during iteration.
|
||||
if (x % 2 == 0) {
|
||||
y = ny - (y + 1);
|
||||
}
|
||||
|
||||
// Pick a hue for every pixel.
|
||||
let h = (i / (nx*ny) * 10) + (ts/2);
|
||||
h = h % 1;
|
||||
|
||||
// Convert to RGB.
|
||||
let c = this.hsv2rgb(h, 1, 1);
|
||||
|
||||
// Poke.
|
||||
this.leds[x][y] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Game of life on a torus, with random state. If cycles or stalls are
|
||||
// detected, the simulation is restarted.
|
||||
// By q3k.
|
||||
class Life extends Animation {
|
||||
draw(ts) {
|
||||
// Generate state if needed.
|
||||
if (this.state === undefined) {
|
||||
this.generateState();
|
||||
}
|
||||
|
||||
// Step simulation every so often.
|
||||
if (this.nextStep === undefined || this.nextStep < ts) {
|
||||
if (this.nextStep !== undefined) {
|
||||
this.step();
|
||||
this.recordState();
|
||||
}
|
||||
// 10 steps per second.
|
||||
this.nextStep = ts + 1.0/10;
|
||||
}
|
||||
|
||||
if (this.shouldRestart(ts)) {
|
||||
this.generateState();
|
||||
}
|
||||
|
||||
// Render state into LED matrix.
|
||||
for (let x = 0; x < this.nx; x++) {
|
||||
for (let y = 0; y < this.ny; y++) {
|
||||
// Turn on and decay smoothly.
|
||||
let [r, g, b] = this.leds[x][y];
|
||||
if (this.state[x][y]) {
|
||||
r += 0.5;
|
||||
g += 0.5;
|
||||
b += 0.5;
|
||||
} else {
|
||||
r -= 0.05;
|
||||
g -= 0.05;
|
||||
b -= 0.05;
|
||||
}
|
||||
r = Math.min(Math.max(r, 0.0), 1.0);
|
||||
g = Math.min(Math.max(g, 0.0), 1.0);
|
||||
b = Math.min(Math.max(b, 0.0), 1.0);
|
||||
this.leds[x][y] = [r, g, b];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recordState records the current state of the simulation within a
|
||||
// 3-element FIFO. This data is used to detect 'stuck' simulations. Any
|
||||
// time there is something repeating within the 3-element FIFO, it means
|
||||
// we're in some boring loop or terminating step, and shouldRestart will
|
||||
// then schedule a simulation restart.
|
||||
recordState() {
|
||||
if (this.recorded === undefined) {
|
||||
this.recorded = [];
|
||||
}
|
||||
// Serialize state into string of 1 and 0.
|
||||
const serialized = this.state.map((column) => {
|
||||
return column.map((value) => value ? "1" : "0").join("");
|
||||
}).join("");
|
||||
this.recorded.push(serialized);
|
||||
|
||||
// Ensure there's not more then 3 recorded state;
|
||||
while (this.recorded.length > 3) {
|
||||
this.recorded.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// shouldRestart looks at the recorded state of simulation frames, and
|
||||
// ensures that there isn't anything repeated within the recorded data. If
|
||||
// so, it schedules a restart of the simulation in 5 seconds.
|
||||
shouldRestart(ts) {
|
||||
// Nothing to do if we have no recorded data.
|
||||
if (this.recorded === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we have a deadline for restarting set already, just obey that and
|
||||
// return true when it expires.
|
||||
if (this.restartDeadline !== undefined) {
|
||||
if (this.restartDeadline < ts) {
|
||||
this.restartDeadline = undefined;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, look for repeat data in the recorded history. If anything
|
||||
// is recorded, schedule a restart deadline in 5 seconds.
|
||||
let s = new Set();
|
||||
|
||||
let restart = false;
|
||||
for (let key of this.recorded) {
|
||||
if (s.has(key)) {
|
||||
restart = true;
|
||||
break;
|
||||
}
|
||||
s.add(key);
|
||||
}
|
||||
if (restart) {
|
||||
console.log("shouldRestart detected restart condition, scheduling restart...");
|
||||
this.restartDeadline = ts + 2;
|
||||
}
|
||||
}
|
||||
|
||||
// generateState builds the initial randomized state of the simulation.
|
||||
generateState() {
|
||||
this.state = new Array();
|
||||
for (let x = 0; x < this.nx; x++) {
|
||||
this.state.push(new Array());
|
||||
for (let y = 0; y < this.ny; y++) {
|
||||
this.state[x][y] = Math.random() > 0.5;
|
||||
}
|
||||
}
|
||||
this.recorded = [];
|
||||
}
|
||||
|
||||
// step runs a simulation step for the game of life board.
|
||||
step() {
|
||||
let next = new Array();
|
||||
for (let x = 0; x < this.nx; x++) {
|
||||
next.push(new Array());
|
||||
for (let y = 0; y < this.ny; y++) {
|
||||
next[x][y] = this.nextFor(x, y);
|
||||
}
|
||||
}
|
||||
this.state = next;
|
||||
}
|
||||
|
||||
// nextFor runs a simulation step for a game of life cell at given
|
||||
// coordinates.
|
||||
nextFor(x, y) {
|
||||
let current = this.state[x][y];
|
||||
// Build coordinates of neighbors, wrapped around (effectively a
|
||||
// torus).
|
||||
let neighbors = [
|
||||
[x-1, y-1], [x, y-1], [x+1, y-1],
|
||||
[x-1, y ], [x+1, y ],
|
||||
[x-1, y+1], [x, y+1], [x+1, y+1],
|
||||
].map(([x, y]) => {
|
||||
x = x % this.nx;
|
||||
y = y % this.ny;
|
||||
if (x < 0) {
|
||||
x += this.nx;
|
||||
}
|
||||
if (y < 0) {
|
||||
y += this.ny;
|
||||
}
|
||||
return [x, y];
|
||||
});
|
||||
// Count number of live and dead neighbours.
|
||||
const live = neighbors.filter(([x, y]) => { return this.state[x][y]; }).length;
|
||||
|
||||
if (current) {
|
||||
if (live < 2 || live > 3) {
|
||||
current = false;
|
||||
}
|
||||
} else {
|
||||
if (live == 3) {
|
||||
current = true;
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
}
|
||||
|
||||
// Add your animations here:
|
||||
export const animations = [
|
||||
Life,
|
||||
SnakeChase,
|
||||
];
|
||||
|
|
@ -1,100 +1,249 @@
|
|||
:root {
|
||||
--primary: #7347d9ff;
|
||||
--primary100: #cfbff1;
|
||||
--secondary: #d947adff;
|
||||
--secondary50: #fae2f0;
|
||||
--darkbgaccent: #1a1622ff;
|
||||
--darkbg: #121212ff;
|
||||
--darkbgalpha: #121212f8;
|
||||
}
|
||||
|
||||
html {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #444;
|
||||
color: #fffdf3;
|
||||
font-weight: 100;
|
||||
font-family: 'Lato', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
font-family: 'Courier Prime', monospace;
|
||||
font-size: 20px;
|
||||
line-height: 150%;
|
||||
|
||||
background-color: var(--darkbgaccent);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
body {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
#ledsFloater {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
overflow-x: hidden;
|
||||
z-index: -11;
|
||||
}
|
||||
|
||||
#ledsWrapper {
|
||||
float: left; /* oh god */
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
#leds {
|
||||
width: max(60vw, 600px);
|
||||
height: max(60vw, 600px);
|
||||
transform: rotate(-15deg);
|
||||
position: relative;
|
||||
top: min(-10vw, -100px);
|
||||
left: min(-10vw, -100px);
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
#page {
|
||||
max-width: 75rem;
|
||||
margin: auto;
|
||||
padding-top: 2rem;
|
||||
padding: 1rem;
|
||||
max-width: 60rem;
|
||||
margin: 6em auto 2em auto;
|
||||
background-color: var(--darkbgalpha);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
#page {
|
||||
background-color: #121212f0;
|
||||
}
|
||||
}
|
||||
|
||||
.about img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 1rem;
|
||||
flex-flow: row wrap;
|
||||
margin: 2em 0 1em 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.top .logo {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
justify-content: right;
|
||||
@media screen and (max-width: 1000px) {
|
||||
.top {
|
||||
margin: 1em 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.top .logo img {
|
||||
margin-top: 2rem;
|
||||
max-height: 25rem
|
||||
max-height: 15rem;
|
||||
}
|
||||
|
||||
.top .mapcontainer {
|
||||
flex-grow: 1;
|
||||
min-width: 35%;
|
||||
@media screen and (max-width: 1000px) {
|
||||
.top .logo img {
|
||||
max-height: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.top .type {
|
||||
max-width: 13em;
|
||||
font-size: min(35px, 4vw);
|
||||
line-height: 0.9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.top .type h1 {
|
||||
padding: 0 0 0.5em 0.2em;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
color: #fff;
|
||||
text-shadow: 0.05em 0.05em var(--secondary);
|
||||
}
|
||||
|
||||
|
||||
#map {
|
||||
margin-bottom: 1rem;
|
||||
height: 16rem;
|
||||
height: 28em;
|
||||
}
|
||||
|
||||
|
||||
.logo h1 {
|
||||
display: block;
|
||||
max-width: 20rem;
|
||||
text-align: center;
|
||||
font-size: 52px;
|
||||
margin-right: 8rem;
|
||||
padding-top: 5rem;
|
||||
.quicklinks {
|
||||
font-size: 16px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.quicklinks {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.quicklinks ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-flow: row wrap;
|
||||
justify-content: center;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
.quicklinks ul li {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.quicklinks ul li:not(.left) {
|
||||
}
|
||||
|
||||
.quicklinks ul li.left {
|
||||
flex-grow: 1;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.quicklinks a {
|
||||
text-decoration: none;
|
||||
padding: 0.8rem 1rem;
|
||||
color: #fff;
|
||||
}
|
||||
.quicklinks a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.quicklinks li:not(.left) a:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.covid {
|
||||
padding: 1rem 2rem;
|
||||
background-color: #9f0000;
|
||||
}
|
||||
|
||||
.covid span {
|
||||
font-size: 20px;
|
||||
background-color: rgba(150, 0, 0, 0.8);
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
padding: 1em 1em 0 1em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
.bottom {
|
||||
padding: 2em 1em 0 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom .about {
|
||||
padding: 1rem 1rem 1rem 2rem;
|
||||
padding: 1rem 2em 3rem 2em;
|
||||
}
|
||||
|
||||
.bottom .blog {
|
||||
padding: 1rem 2rem 1rem 1rem;
|
||||
flex-grow: 1;
|
||||
min-width: 40%;
|
||||
@media screen and (max-width: 1000px) {
|
||||
.bottom .about {
|
||||
padding: 0rem 0em 1rem 0em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bottom .about li + li {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 150%;
|
||||
text-align: justify;
|
||||
text-align: left;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 30px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-family: 'Allerta', sans-serif;
|
||||
@media screen and (max-width: 1000px) {
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
* + h2 {
|
||||
margin: 2rem 0 0 0;
|
||||
}
|
||||
|
||||
h2 + * {
|
||||
margin: 1rem 0 0 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 26px;
|
||||
display: inline-block;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
h2:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
background-color: var(--secondary);
|
||||
height: 0.15em;
|
||||
width: 100%;
|
||||
margin-top: 0.1em;
|
||||
margin-left: 0.3em
|
||||
}
|
||||
h3 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
@ -103,7 +252,13 @@ pre {
|
|||
}
|
||||
|
||||
a {
|
||||
color: #fffdf3;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: var(--primary100);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary100);
|
||||
}
|
||||
|
||||
b {
|
||||
|
@ -114,29 +269,22 @@ ul {
|
|||
padding: 0 0 0 1em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
ul {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
li i {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
#background-logo {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -10;
|
||||
}
|
||||
|
||||
#background-logo img {
|
||||
opacity: 3%;
|
||||
margin-top: 2%;
|
||||
margin-left: 5%;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
#footer {
|
||||
margin-top: 2rem;
|
||||
margin: 1rem 0 0 0;
|
||||
font-size: 0.8rem;
|
||||
opacity: 60%;
|
||||
}
|
||||
|
|
72
hswaw/site/static/led.js
Normal file
72
hswaw/site/static/led.js
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { animations } from "./animations.js";
|
||||
|
||||
class CanvasRenderer {
|
||||
static WIDTH = 1024;
|
||||
static HEIGHT = 1024;
|
||||
|
||||
constructor() {
|
||||
const ledDiv = document.querySelector("#leds");
|
||||
let canvas = document.createElement("canvas");
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
canvas.width = CanvasRenderer.WIDTH;
|
||||
canvas.height = CanvasRenderer.HEIGHT;
|
||||
ledDiv.appendChild(canvas);
|
||||
ledDiv.style.backgroundColor = "#00000000";
|
||||
let context = canvas.getContext('2d');
|
||||
|
||||
this.canvas = canvas;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
render(animation) {
|
||||
const canvas = this.canvas;
|
||||
const context = this.context;
|
||||
const leds = animation.leds;
|
||||
const nx = animation.nx;
|
||||
const ny = animation.ny;
|
||||
|
||||
const xoff = CanvasRenderer.WIDTH / (nx + 1);
|
||||
const yoff = CanvasRenderer.HEIGHT / (ny + 1);
|
||||
const d = xoff * 0.7;
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for (let x = 0; x < nx; x++) {
|
||||
for (let y = 0; y < ny; y++) {
|
||||
const cx = (x + 1) * xoff
|
||||
const cy = (y + 1) * yoff
|
||||
|
||||
const rgb = leds[x][y];
|
||||
const r = Math.max(rgb[0] * 256, 0x1a);
|
||||
const g = Math.max(rgb[1] * 256, 0x16);
|
||||
const b = Math.max(rgb[2] * 256, 0x22);
|
||||
const color = `rgba(${r}, ${g}, ${b})`;
|
||||
|
||||
context.beginPath();
|
||||
context.arc(cx, cy, d/2, 0, 2 * Math.PI, false);
|
||||
context.fillStyle = color;
|
||||
context.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
const animationClass = animations[Math.floor(Math.random() * animations.length)];
|
||||
console.log(`Picked LED animation: ${animationClass.name}`);
|
||||
|
||||
let renderer = new CanvasRenderer();
|
||||
let animation = new animationClass(16, 16);
|
||||
|
||||
let step = (hrts) => {
|
||||
// Run animation logic.
|
||||
animation.draw(hrts / 1000);
|
||||
|
||||
// Draw LEDs.
|
||||
renderer.render(animation);
|
||||
|
||||
// Schedule next frame.
|
||||
window.requestAnimationFrame(step);
|
||||
}
|
||||
window.requestAnimationFrame(step);
|
||||
});
|
38
hswaw/site/static/neon-syrenka.svg
Normal file
38
hswaw/site/static/neon-syrenka.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
BIN
hswaw/site/static/space.jpg
Normal file
BIN
hswaw/site/static/space.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 452 KiB |
|
@ -2,80 +2,73 @@
|
|||
<meta charset="utf-8">
|
||||
<!-- https://html.spec.whatwg.org/multipage/syntax.html#syntax-tag-omission -->
|
||||
<title>Warszawski Hackerspace</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="/static/site/landing.css"/>
|
||||
<link rel="stylesheet" href="/static/leaflet/leaflet.css"/>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Allerta&family=Lato&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime:wght@400;700&family=Noto+Sans&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
</style>
|
||||
<div id="ledsFloater">
|
||||
<div id="ledsWrapper">
|
||||
<div id="leds">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="page">
|
||||
<div class="top">
|
||||
<div class="logo">
|
||||
<img src="/static/site/syrenka.png" />
|
||||
<img src="/static/site/neon-syrenka.svg" />
|
||||
</div>
|
||||
<div class="type">
|
||||
<h1>Warszawski Hackerspace</h1>
|
||||
</div>
|
||||
<div class="mapcontainer">
|
||||
<h2>Gdzie jesteśmy?</h2>
|
||||
<div id="map"></div>
|
||||
<pre>Warszawski Hackerspace
|
||||
ul. Wolność 2A
|
||||
01-018 Warszawa
|
||||
52°14'29.8"N 20°59'5.5"E</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="covid">
|
||||
<span>Na okres pandemii Hackerspace jest <b>zamknięty</b>, więcej informacji: <a href="">projekt covid-19</a></span>
|
||||
<div class="quicklinks">
|
||||
<ul>
|
||||
<li><a href="https://wiki.hackerspace.pl/">Wiki</a></li>
|
||||
<li><a href="https://profile.hackerspace.pl/">Konto</a></li>
|
||||
<li><a href="https://wiki.hackerspace.pl/partners">Partnerzy</a></li>
|
||||
<li><a href="https://wiki.hackerspace.pl/kontakt">Kontakt</a></li>
|
||||
{{ if eq .AtError nil }}
|
||||
{{ $count := len .AtStatus.Users }}
|
||||
<li>
|
||||
<a href="https://at.hackerspace.pl">Osób w spejsie: <b>{{ $count }}</b></a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="about">
|
||||
<h2>Czym jest Hackerspace?</h2>
|
||||
<h2>Czym jest Warszawski Hackerspace?</h2>
|
||||
<p>
|
||||
Przestrzeń stworzona i utrzymywana przez grupę kreatywnych osób, które łączy fascynacja ogólno pojętym tworzeniem w duchu <a href="https://pl.wikipedia.org/wiki/Spo%C5%82eczno%C5%9B%C4%87_haker%C3%B3w">kultury hackerskiej</a>.
|
||||
Przestrzeń stworzona i utrzymywana przez grupę kreatywnych osób, które łączy fascynacja ogólno pojętym tworzeniem w duchu <a href="https://pl.wikipedia.org/wiki/Spo%C5%82eczno%C5%9B%C4%87_haker%C3%B3w">kultury hackerskiej</a>. Razem utrzymujemy przestrzeń na ul. Wolność 2A, gdzie mamy między innymi:
|
||||
<ul>
|
||||
<li><b>Warsztat ciężki</b>, ze sprzętem takim jak ploter laserowy, frezarka kolumnowa CNC, tokarka, spawarki i ramię robotyczne KUKA,</li>
|
||||
<li><b>Warsztat elektroniczny</b>, z oscyloskopami, stacjami lutowniczymi i masą części elektronicznych,</li>
|
||||
<li><b>Przestrzeń socjalną</b>, pełną stołów i kanap do hakowania nad projektami software'owymi,</li>
|
||||
<li><b>Serwerownię</b>, utrzymująca infrastrukturę spejsu i naszego mikro-ISP <a href="https://bgp.wtf">bgp.wtf</a>.</li>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
<b>Hackerspace nie zna barier.</b> Jeśli masz ciekawy pomysł i szukasz ludzi chętnych do współpracy lub po prostu potrzebujesz miejsca i sprzętu - <a href="">zapraszamy</a>!
|
||||
<b>Hackerspace nie zna barier.</b> Jeśli masz ciekawy pomysł i szukasz ludzi chętnych do współpracy, lub po prostu potrzebujesz miejsca i sprzętu - <a href="https://wiki.hackerspace.pl/jak-dolaczyc">zapraszamy</a>! Utrzymujemy się w całosci z wolontariatu naszych członków, <a href="https://wiki.hackerspace.pl/finanse">darowizn i składek</a> oraz drobnej aktywności komercyjnej.
|
||||
</p>
|
||||
<h3>Kto jest teraz w spejsie?</h3>
|
||||
<img src="/static/site/space.jpg" />
|
||||
<h2>Czy mogę odwiedzić spejs? Jak do was dołączyć?</h2>
|
||||
<p>
|
||||
{{ if ne .AtError nil }}
|
||||
<i>Ups, nie udało się załadować stanu checkinatora.</i>
|
||||
{{ else }}
|
||||
{{ $count := len .AtStatus.Users }}
|
||||
{{ if gt $count 4 }}
|
||||
Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajduje się {{ $count }} osób:
|
||||
{{ else if gt $count 1 }}
|
||||
Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajdują się {{ $count }} osoby:
|
||||
{{ else if gt $count 0 }}
|
||||
Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie znajduje się jedna osoba:
|
||||
{{ else }}
|
||||
Według <a href="https://at.hackerspace.pl">naszych instrumentów</a> w spejsie obecnie nie ma nikogo.
|
||||
{{ end }}
|
||||
<ul class="atlist">
|
||||
{{ range .AtStatus.Users }}
|
||||
<li>{{ .Login }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
Nasze cotygodniowe otwarte spotkania są w tej chwili zawieszone z powodu pandemii. Mimo tego, <b>dalej jesteśmy otwarci na nowych członków</b> i zainteresowanych - tylko w mniejszej skali i po wcześniejszym umówieniu się. Więcej informacji znajdziesz na <a href="https://wiki.hackerspace.pl/jak-dolaczyc">wiki.hackerspace.pl/jak-dolaczyc</a>.
|
||||
</p>
|
||||
<h2>Gdzie jest Hackerspace?</h2>
|
||||
<div id="map"></div>
|
||||
<p>
|
||||
Stowarzyszenie Warszawski Hackerspace, ul. Wolność 2A, 01-018 Warszawa.
|
||||
</p>
|
||||
<h2>Gdzie was znaleźć w Internecie?</h2>
|
||||
<p>
|
||||
Jeśli nalegasz, mamy rzadko aktualizowane konta na <a href="https://twitter.com/hackerspace.pl">Twitterze</a> i <a href="https://www.facebook.com/hackerspacepl">Facebooku</a>. Lepiej jednak kontaktować się z nami <a href="https://wiki.hackerspace.pl/kontakt">przez IRC, Matrixa lub mejlowo</a>.
|
||||
</p>
|
||||
</div>
|
||||
<div class="blog">
|
||||
<h2>Blog</h2>
|
||||
<p>
|
||||
Najnowsze wpisy z naszego <a href="https://blog.hackerspace.pl">bloga</a>:
|
||||
<ul>
|
||||
{{ range .Entries }}
|
||||
<li><a href="{{ .Link.Href }}">{{ .Title }}</a> <i>{{ .UpdatedHuman }}, {{ .Author }}</i></li>
|
||||
{{ else }}
|
||||
<li><i>Ups, nie udało się załadować wpisów.</i></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<span>© 2021 <a href="https://cs.hackerspace.pl/hscloud/-/tree/hswaw/site">Autorzy Strony</a>. Ten utwór jest dostępny na <a rel="license" href="http://creativecommons.org/licenses/by/4.0/">licencji Creative Commons Uznanie autorstwa 4.0 Międzynarodowe</a>.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -95,3 +88,4 @@ window.loadMap = () => {
|
|||
}
|
||||
</script>
|
||||
<script src="/static/leaflet/leaflet.js" crossorigin="" onload="loadMap()"></script>
|
||||
<script src="/static/site/led.js" crossorigin="" type="module" ></script>
|
||||
|
|
|
@ -5,9 +5,11 @@ import (
|
|||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"code.hackerspace.pl/hscloud/hswaw/site/calendar"
|
||||
"code.hackerspace.pl/hscloud/hswaw/site/templates"
|
||||
)
|
||||
|
||||
|
@ -60,11 +62,47 @@ func (s *service) handleIndex(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
render(w, tmplIndex, map[string]interface{}{
|
||||
"Entries": s.getFeeds(),
|
||||
"Events": s.getEvents(),
|
||||
"AtStatus": atStatus,
|
||||
"AtError": atError,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *service) handleJSONEvents(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(s.getEvents())
|
||||
}
|
||||
|
||||
// handleEvent is a fallback HTML-only event render view.
|
||||
// TODO(q3k): make this pretty by either making a template or redirecting to a
|
||||
// pretty viewer.
|
||||
func (s *service) handleEvent(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
uid := parts[len(parts)-1]
|
||||
|
||||
events := s.getEvents()
|
||||
var event *calendar.UpcomingEvent
|
||||
for _, ev := range events {
|
||||
if ev.UID == uid {
|
||||
event = ev
|
||||
break
|
||||
}
|
||||
}
|
||||
if event == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
render(w, template.Must(template.New("event").Parse(`<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<title>Event details: {{ .Summary }}</title>
|
||||
<body>
|
||||
<i>this interface intentionally left ugly...</i><br/>
|
||||
<b>summary:</b> {{ .Summary }}<br />
|
||||
<b>date:</b> {{ .WarsawDate }}<br />
|
||||
<pre>{{ .Description }}</pre>`)), event)
|
||||
}
|
||||
|
||||
func (s *service) handleSpaceAPI(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
|
Loading…
Reference in a new issue