1
0
Fork 0

hswaw/site: wip new layout

Change-Id: I4da3a668429dee42c7292accb9e24b93703f1538
master
q3k 2021-07-11 16:03:43 +00:00 committed by radex
parent c35d52b19e
commit 717aad4ac6
14 changed files with 767 additions and 128 deletions

View File

@ -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}",
)

View File

@ -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.

View File

@ -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

View File

@ -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
View 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
}

View File

@ -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)
}

View File

@ -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",
)

View 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,
];

View File

@ -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
View 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);
});

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

View File

@ -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>&copy; 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>

View File

@ -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")