merge eva repos into single repository

This commit is contained in:
gidsi 2018-04-20 18:08:55 +02:00
commit 200dd620ae
52 changed files with 2281 additions and 0 deletions

14
LICENSE.md Normal file
View file

@ -0,0 +1,14 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

2
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
eva-backend
.idea

15
backend/Dockerfile Normal file
View file

@ -0,0 +1,15 @@
FROM golang:alpine as builder
RUN apk --no-cache add git
WORKDIR /go/src/app
COPY . .
RUN go get -d -v ./...
RUN go install -v ./...
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /go/bin/app .
COPY config.yaml config.yaml
EXPOSE 8080
CMD ["./app", "-shared_secret=$SHARED_SECRET"]

15
backend/README.md Normal file
View file

@ -0,0 +1,15 @@
eva-backend
===========
See yaml file for database config
```bash
go get
go install
SHARED_SECRET=foo eva-backend
```
SpaceAPI extensions
===================
* Key `ext_ccc` describes Chaos Computer Club relation status (Example values: `"chaostreff"` or `"erfa"`)

47
backend/calendar.go Normal file
View file

@ -0,0 +1,47 @@
package main
import (
"github.com/gidsi/ics-golang"
"log"
)
func getCalendars() {
spaceDataArray := readSpacedata()
for _, spaceData := range spaceDataArray {
parser := ics.New()
parserChan := parser.GetInputChan()
parserChan <- spaceData.Feeds.Calendar.Url
outputChan := parser.GetOutputChan()
calendar := Calendar{}
calendar.Space = spaceData.Space
events := []Event{}
go func() {
for event := range outputChan {
events = append(events, mapEventObject(event))
}
}()
parser.Wait()
calendar.Events = events
writeCalendar(calendar)
log.Println("calendar write done for " + spaceData.Space)
}
}
func mapEventObject(event *ics.Event) Event {
eventData := Event{}
eventData.Start = event.GetStart()
eventData.ImportedId = event.GetImportedID()
eventData.Status = event.GetStatus()
eventData.Description = event.GetDescription()
eventData.Location = event.GetLocation()
eventData.Summary = event.GetSummary()
eventData.Rrule = event.GetRRule()
eventData.Url = event.GetUrl()
eventData.Class = event.GetClass()
eventData.Sequence = event.GetSequence()
eventData.WholeDayEvent = event.GetWholeDayEvent()
return eventData
}

3
backend/config.yaml Normal file
View file

@ -0,0 +1,3 @@
---
mongodb_server: database
mongodb_database: eva

7
backend/configFile.go Normal file
View file

@ -0,0 +1,7 @@
package main
type ConfigFile struct {
SharedSecret string `yaml:"shared_secret,omitempty"`
MongoDbServer string `yaml:"mongodb_server,omitempty"`
MongoDbDatabase string `yaml:"mongodb_database,omitempty"`
}

115
backend/database.go Normal file
View file

@ -0,0 +1,115 @@
package main
import (
"log"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func writeSpaceData(data SpaceData) {
if(data.Space != "") {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spacedata")
_, err = c.Upsert(bson.M{ "space": data.Space }, data)
if err != nil {
log.Fatal(err)
}
}
}
func writeSpaceurl(spaceUrl SpaceUrl) {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spaceurl")
count, _ := c.Find(bson.M{ "url": spaceUrl.Url }).Count()
if(count == 0) {
c.Insert(spaceUrl);
}
}
func writeCalendar(calendar Calendar) {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("calendar")
c.Upsert(bson.M{ "space": calendar.Space }, calendar)
}
func updateSpaceurl(spaceUrl SpaceUrl) {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spaceurl")
c.Update(bson.M{ "url": spaceUrl.Url }, spaceUrl);
}
func readSpacedata() []SpaceData {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spacedata")
result := []SpaceData{}
c.Find(bson.M{}).Iter().All(&result)
return result
}
func readSpaceurl() []SpaceUrl {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spaceurl")
result := []SpaceUrl{}
c.Find(bson.M{}).Iter().All(&result)
return result
}
func readCalendar() []Calendar {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
}
defer session.Close()
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("calendar")
result := []Calendar{}
c.Find(bson.M{}).Iter().All(&result)
return result
}

92
backend/eva-backend.go Normal file
View file

@ -0,0 +1,92 @@
package main
import (
"net/http"
"encoding/json"
"gopkg.in/yaml.v2"
"log"
"io/ioutil"
"os"
"time"
"github.com/gorilla/mux"
)
var config = ConfigFile{}
func main() {
data, _ := ioutil.ReadFile("config.yaml")
yaml.Unmarshal(data, &config)
config.SharedSecret = os.Getenv("SHARED_SECRET")
router := NewRouter()
http.Handle("/", router)
log.Fatal(http.ListenAndServe(":8080", router))
}
func getJson(url string, target interface{}) error {
r, err := http.Get(url)
if err != nil {
return err
}
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(target)
}
func SpaceDataIndex(w http.ResponseWriter, r *http.Request) {
ReturnJson(w, readSpacedata())
}
func SpaceUrlIndex(w http.ResponseWriter, r *http.Request) {
ReturnJson(w, readSpaceurl())
}
func CalendarIndex(w http.ResponseWriter, r *http.Request) {
ReturnJson(w, readCalendar())
}
func SpaceUrlAdd(w http.ResponseWriter, r *http.Request) {
spaceUrl := SpaceUrl{}
createEntry(&spaceUrl, w, r)
spaceUrl.Validated = false
writeSpaceurl(spaceUrl)
}
func SpaceUrlUpdate(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
SharedSecret := vars["SharedSecret"]
if(SharedSecret == config.SharedSecret) {
spaceUrl := SpaceUrl{}
createEntry(&spaceUrl, w, r)
updateSpaceurl(spaceUrl)
}
}
func loadSpaceData() {
spaceUrls := readSpaceurl()
timestamp := time.Now().Unix()
for _, spaceUrl := range spaceUrls {
if(spaceUrl.Validated && int64(spaceUrl.LastUpdated + 60) < timestamp) {
spaceData := SpaceData{}
err := getJson(spaceUrl.Url, &spaceData)
if(err != nil) {
log.Println(spaceUrl.Url)
log.Println(err)
} else {
writeSpaceData(spaceData)
spaceUrl.LastUpdated = timestamp
updateSpaceurl(spaceUrl)
}
}
}
}
func refreshData(w http.ResponseWriter, r *http.Request) {
loadSpaceData()
getCalendars()
w.WriteHeader(204)
}

23
backend/event.go Normal file
View file

@ -0,0 +1,23 @@
package main
import "time"
type Calendar struct {
Space string
Events []Event
}
type Event struct {
Start time.Time `json:"start"`
ImportedId string `json:"importId"`
Status string `json:"status"`
Description string `json:"description"`
Location string `json:"location"`
Summary string `json:"summary"`
Rrule string `json:"rrule"`
Class string `json:"class"`
Url string `json:"url"`
Sequence int `json:"sequence"`
WholeDayEvent bool `json:"whileDayEvent"`
}

29
backend/logger.go Normal file
View file

@ -0,0 +1,29 @@
package main
import (
"log"
"net/http"
"time"
)
func Logger(inner http.Handler, name string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inner.ServeHTTP(w, r)
log.Printf(
"%s\t%s\t%s\t%s",
r.Method,
r.RequestURI,
name,
time.Since(start),
)
})
}
func logError(err error) {
if err != nil {
log.Fatal(err)
}
}

48
backend/response.go Normal file
View file

@ -0,0 +1,48 @@
package main
import (
"net/http"
"encoding/json"
"fmt"
)
type DataObject interface {
Save() DataObject
}
func Index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello? Yes, this is dog!")
}
func ReturnJson(w http.ResponseWriter, v interface{}) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "content-type")
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(v); err != nil {
panic(err)
}
}
func ReturnDataNotFound(w http.ResponseWriter) {
w.WriteHeader(http.StatusNotFound)
}
func createEntry(i interface{}, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "content-type")
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
if err := json.NewDecoder(r.Body).Decode(i); err != nil {
w.WriteHeader(422) // unprocessable entity
if err := json.NewEncoder(w).Encode(err); err != nil {
panic(err)
}
} else {
w.WriteHeader(http.StatusCreated)
if err := json.NewEncoder(w).Encode(i); err != nil {
panic(err)
}
}
}

51
backend/router.go Normal file
View file

@ -0,0 +1,51 @@
package main
import (
"net/http"
"github.com/gorilla/mux"
"log"
)
func NewRouter() *mux.Router {
router := mux.NewRouter().StrictSlash(true)
for _, route := range IndexRoutes {
var handler http.Handler
handler = Logger(route.Handler, route.Name)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
router.
Methods("OPTIONS").
Name("Options Handler").
Handler(http.HandlerFunc(optionsHandler))
}
router.
Methods("OPTIONS").
Name("Options Handler").
Handler(http.HandlerFunc(optionsHandler))
router.NotFoundHandler = http.HandlerFunc(notFound)
return router
}
func notFound(w http.ResponseWriter, r *http.Request) {
log.Print("---------------------------------")
log.Print(w)
log.Print("---------------------------------")
log.Print(r)
log.Print("---------------------------------")
w.WriteHeader(http.StatusNotFound)
}
func optionsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE")
w.WriteHeader(200)
}

58
backend/routes.go Normal file
View file

@ -0,0 +1,58 @@
package main
import "net/http"
type Route struct {
Name string
Method string
Pattern string
Handler http.HandlerFunc
}
type Routes []Route
var IndexRoutes = Routes{
Route{
"Index",
"GET",
"/",
Index,
},
Route{
"SpaceDataIndex",
"GET",
"/spaces",
SpaceDataIndex,
},
Route{
"SpaceUrlIndex",
"GET",
"/urls",
SpaceUrlIndex,
},
Route{
"CalendarIndex",
"GET",
"/calendar",
CalendarIndex,
},
Route{
"SpaceUrlAdd",
"POST",
"/urls",
SpaceUrlAdd,
},
Route{
"SpaceUrlUpdate",
"PUT",
"/urls/{SharedSecret}",
SpaceUrlUpdate,
},
Route{
"RefreshData",
"GET",
"/refresh",
refreshData,
},
}

44
backend/spacedata.go Normal file
View file

@ -0,0 +1,44 @@
package main
type Feed struct {
Type string `json:"type"`
Url string `json:"url"`
}
type Location struct {
Address string `json:"address"`
Lon float32 `json:"lon"`
Lat float32 `json:"lat"`
}
type Contact struct {
Twitter string `json:"twitter"`
Phone string `json:"phone"`
Irc string `json:"irc"`
Email string `json:"email"`
Ml string `json:"ml"`
IssueMail string `json:"issue_mail"`
}
type State struct {
Open bool `json:"open"`
}
type Feeds struct {
Blog Feed `json:"blog"`
Wiki Feed `json:"wiki"`
Calendar Feed `json:"calendar"`
}
type SpaceData struct {
Api string `json:"api"`
Space string `json:"space"`
Logo string `json:"logo"`
Url string `json:"url"`
Location Location `json:"location"`
Contact Contact `json:"contact"`
IssueReportChannels []string `json:"issue_report_channels"`
State State `json:"state"`
Feeds Feeds `json:"feeds"`
Ext_ccc string `json:"ext_ccc"`
}

7
backend/spaceurl.go Normal file
View file

@ -0,0 +1,7 @@
package main
type SpaceUrl struct {
Url string `json:"url"`
Validated bool `json:"validated"`
LastUpdated int64 `json:"lastUpdated"`
}

17
docker-compose.yml Normal file
View file

@ -0,0 +1,17 @@
version: "3"
services:
frontend:
build: ./frontend
image: gidsi/spaceapi-ccc-frontend:latest
depends_on:
- backend
backend:
build: ./backend
image: gidsi/spaceapi-ccc-backend:latest
env_file: .env
depends_on:
- database
database:
image: mongo:latest
volumes:
- /opt/eva:/data/db

18
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,18 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
npm-debug.log*
yarn-debug.log*
yarn-error.log*

11
frontend/.travis.yml Normal file
View file

@ -0,0 +1,11 @@
language: node_js
node_js:
- "5"
cache:
directories:
- node_modules
script:
- npm run deploy

11
frontend/Dockerfile Normal file
View file

@ -0,0 +1,11 @@
FROM node as builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/build/ /usr/share/nginx/html/
RUN mkdir -p /tmp/osm/cache
COPY nginx.conf /etc/nginx/nginx.conf

11
frontend/README.md Normal file
View file

@ -0,0 +1,11 @@
eve-frontend
=======================
Usage
-----
```
npm install -g yarn
yarn install
yarn start
```

59
frontend/nginx.conf Normal file
View file

@ -0,0 +1,59 @@
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
proxy_cache_path /tmp/osm/cache keys_zone=osm:512m inactive=7d;
proxy_temp_path /tmp/osm/temp;
upstream osm {
server b.basemaps.cartocdn.com;
#server b.tile.openstreetmap.org;
keepalive 8;
}
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
try_files $uri /index.html;
}
location /static {
root /usr/share/nginx/html/;
}
location /api/ {
proxy_pass http://backend:8080/;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /map/tiles/ {
proxy_set_header Host b.basemaps.cartocdn.com;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://osm/dark_all/;
proxy_cache osm;
proxy_cache_valid 7d;
expires 7d;
}
}
}

30
frontend/package.json Normal file
View file

@ -0,0 +1,30 @@
{
"name": "eva-frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"leaflet": "^1.0.3",
"material-ui": "^0.17.4",
"moment": "^2.18.1",
"react": "^15.5.4",
"react-dom": "^15.5.4",
"react-leaflet": "^1.1.6",
"react-redux": "^5.0.4",
"react-router-dom": "^4.1.1",
"react-tap-event-plugin": "^2.0.1",
"redux": "^3.6.0",
"redux-actions": "^2.0.2",
"redux-thunk": "^2.2.0",
"rrule": "^2.2.0",
"superagent": "^3.5.2"
},
"devDependencies": {
"react-scripts": "0.9.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="%PUBLIC_URL%/leaflet.css" />
<script src="%PUBLIC_URL%/leaflet.js"></script>
<title>CCC Spaces</title>
</head>
<body>
<div id="root">
<noscript>
<h2>Kein Javascript, keine Kekse.</h2>
<p>
Ohne Javascript lüpt dat Ding nich, sorry.
</p>
</noscript>
</div>
</body>
</html>

618
frontend/public/leaflet.css Normal file
View file

@ -0,0 +1,618 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-map-pane svg,
.leaflet-map-pane canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg,
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer {
max-width: none !important;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-drag {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile {
will-change: opacity;
}
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
-o-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-control-zoom-out {
font-size: 20px;
}
.leaflet-touch .leaflet-control-zoom-in {
font-size: 22px;
}
.leaflet-touch .leaflet-control-zoom-out {
font-size: 24px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-o-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-clickable {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}

File diff suppressed because one or more lines are too long

33
frontend/src/App.js Normal file
View file

@ -0,0 +1,33 @@
import React from 'react'
import {
BrowserRouter as Router,
Route
} from 'react-router-dom';
import injectTapEventPlugin from 'react-tap-event-plugin';
import { Provider } from 'react-redux';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import theme from './style/theme';
import store from './redux/store';
import IndexContainer from './views/Index';
import SpaceList from './views/SpaceList';
import UrlListView from './views/UrlListView';
import layout from './layout';
injectTapEventPlugin();
const App = () => (
<MuiThemeProvider muiTheme={getMuiTheme(theme)}>
<Provider store={store}>
<Router>
<div>
<Route path="/list" component={layout(<SpaceList />)} />
<Route path="/urls" component={layout(<UrlListView />)} />
<Route exact path="/" component={layout(<IndexContainer />)} />
</div>
</Router>
</Provider>
</MuiThemeProvider>
);
export default App;

8
frontend/src/App.test.js Normal file
View file

@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});

View file

@ -0,0 +1,5 @@
export default {
api: {
url: '/api',
},
};

View file

@ -0,0 +1,83 @@
import React from 'react';
import { connect } from 'react-redux';
import { Table, TableBody, TableRow, TableRowColumn }
from 'material-ui/Table';
import InfoIcon from 'material-ui/svg-icons/action/info-outline';
import { actions as calendarActions, eventStruct } from '../redux/modules/calendar';
import { spacedataStruct } from '../redux/modules/spacedata';
const mapStateToProps = state => ({
events: state.calendars.items,
spacedata: state.spacedata,
});
class EventList extends React.Component {
static propTypes = {
events: React.PropTypes.arrayOf(
React.PropTypes.shape(eventStruct),
),
fetchCalendars: React.PropTypes.func,
spacedata: spacedataStruct,
};
defaultProps = {
events: [],
};
componentWillMount() {
this.props.fetchCalendars();
}
formatDate = date => (date.format('DD.MM.YYYY'));
formatTime = date => (date.format('HH:mm'));
render() {
return (
<Table
selectable
multiSelectable
>
<TableBody
showRowHover
stripedRows
displayRowCheckbox={false}
>
{this.props.events
.filter(event =>
(
this.props.spacedata.filter.indexOf(event.space) !== -1
|| this.props.spacedata.filter.length === 0
)
)
.map(event => (
<TableRow
key={event.importId + event.start.toLocaleString() + event.description}
>
<TableRowColumn style={{ width: '80px', padding: '5px' }}>
{this.formatDate(event.start)}
</TableRowColumn>
<TableRowColumn style={{ width: '55px', padding: '5px' }}>
{event.wholeDayEvent ? null : this.formatTime(event.start)}
</TableRowColumn>
<TableRowColumn>
{event.summary || event.description}
</TableRowColumn>
<TableRowColumn>
{event.space}
</TableRowColumn>
<TableRowColumn style={{ textAlign: 'right' }}>
{event.url && <a href={event.url}>
<InfoIcon style={{ cursor: 'pointer' }} />
</a>}
</TableRowColumn>
</TableRow>
))}
</TableBody>
</Table>
);
}
}
export default connect(mapStateToProps, {
...calendarActions,
})(EventList);

View file

@ -0,0 +1,54 @@
import React from 'react';
import { Map as LeafletMap, TileLayer } from 'react-leaflet';
import { connect } from 'react-redux';
import Marker from './Marker';
import { actions as spaceDataActions, spacedataStruct } from '../redux/modules/spacedata';
const mapStateToProps = state => ({
spacedata: state.spacedata,
});
const mapDispatchToProps = {
...spaceDataActions,
};
class Map extends React.Component {
static propTypes = {
spacedata: spacedataStruct.isRequired,
fetchSpacedata: React.PropTypes.func.isRequired,
toggleFilterSpacedata: React.PropTypes.func.isRequired,
};
componentWillMount() {
this.props.fetchSpacedata();
}
render() {
const centerGermany = [51.163375, 10.447683];
return (
<LeafletMap
center={centerGermany}
zoom={5}
style={{ width: '100vw', height: '50vh', margin: 0, padding: 0, maxWidth: '100%' }}
>
<TileLayer
url="https://spaceapi.ccc.de/map/tiles/{z}/{x}/{y}.png"
/>
{this.props.spacedata.items.map(
spacedata => (
<Marker
spacedata={spacedata}
key={spacedata.space}
highlight={
this.props.spacedata.filter.length === 0
|| this.props.spacedata.filter.indexOf(spacedata.space) !== -1}
toggleFilterSpacedata={this.props.toggleFilterSpacedata}
/>
)
)}
</LeafletMap>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Map);

View file

@ -0,0 +1,46 @@
import React from 'react';
import { CircleMarker, Popup } from 'react-leaflet';
import theme from '../style/theme';
import { spacedataElementStruct } from '../redux/modules/spacedata';
const Marker = (props) => {
const color = props.highlight ? theme.palette.accent2Color : theme.palette.primary1Color;
const style = {
container: {
display: 'flex',
},
logo: {
width: '50px',
marginRight: '5px',
},
};
return (
<CircleMarker
fillColor={color}
color={color}
radius={5}
center={[props.spacedata.location.lat, props.spacedata.location.lon]}
>
<Popup>
<div style={style.container}>
<div>
{props.spacedata.space}
<br />
<a href={props.spacedata.url}>
{props.spacedata.url}
</a>
</div>
</div>
</Popup>
</CircleMarker>
);
};
Marker.propTypes = {
spacedata: spacedataElementStruct.isRequired,
highlight: React.PropTypes.bool.isRequired,
};
export default Marker;

View file

@ -0,0 +1,106 @@
import React from 'react';
import request from 'superagent';
import TextField from 'material-ui/TextField';
import FloatingActionButton from 'material-ui/FloatingActionButton';
import ContentAdd from 'material-ui/svg-icons/content/add';
import Snackbar from 'material-ui/Snackbar';
import config from '../api/config';
class SpaceApiInput extends React.Component {
static propTypes = {
style: React.PropTypes.shape({}),
};
static defaultProps = {
style: {},
};
state = {
open: false
};
getStyle = () => ({
formContainer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
paddingLeft: '20px',
paddingRight: '20px',
paddingBottom: '40px',
},
container: {
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
paddingTop: '50px',
},
hint: {
color: 'white',
width: '100%',
maxWidth: '550px',
fontSize: '13px',
textAlign: 'center',
},
});
handleInputChange = (event) => {
this.setState({ url: event.target.value, input: event.target });
};
handleButtonClick = () => {
request
.post(`${config.api.url}/urls`)
.send({
url: this.state.url,
})
.set('Content-Type', 'application/json')
.end((err) => {
if (!err) {
this.spaceApiInput.input.value = '';
this.setState({ open: true });
}
});
};
render() {
const style = this.getStyle();
return (
<div style={style.container}>
<p style={style.hint}>
Trage die API-URL deines Hackerspaces hier ein und wir werden sie nach
kurzer Prüfung freischalten. Bei Fragen oder Problemen wende dich an&nbsp;
<a href={'mailto:lokal@ccc.de'} style={{ color: 'white', textDecoration: 'none' }}>
{'lokal@ccc.de'}
</a>.
</p>
<div style={style.formContainer}>
<TextField
hintText={'https://example.com/yourspaceapi.json'}
name={'spaceapi-input'}
onChange={this.handleInputChange}
ref={ref => (this.spaceApiInput = ref)}
style={{ width: '100%', maxWidth: '340px' }}
/>
<FloatingActionButton
style={{ marginLeft: '20px' }}
mini
onTouchTap={this.handleButtonClick}
>
<ContentAdd />
</FloatingActionButton>
</div>
<Snackbar
open={this.state.open}
message={'Die URL wurde hinzugefuegt und befindet sich nun im review.'}
autoHideDuration={4000}
style={{ minWidth: '490px' }}
onRequestClose={() => this.setState({ open: false })}
/>
</div>
);
}
}
export default SpaceApiInput;

View file

@ -0,0 +1,63 @@
import React from 'react';
import {
Table,
TableBody,
TableRow,
TableRowColumn,
} from 'material-ui/Table';
import { spacedataStruct } from '../redux/modules/spacedata';
export class SpaceList extends React.Component {
static propTypes = {
fetchSpacedata: React.PropTypes.func.isRequired,
spacedata: spacedataStruct,
};
static defaultProps = {
spacedata: {
items: [],
},
};
componentWillMount() {
this.props.fetchSpacedata();
}
render() {
const items = this.props.spacedata.items.sort(
(a, b) => a.space.toUpperCase().localeCompare(b.space.toUpperCase())
);
return (
<Table
selectable
multiSelectable
>
<TableBody
showRowHover
stripedRows
displayRowCheckbox={false}
>
{items
.map(space => (
<TableRow key={space.space}>
<TableRowColumn>
{space.space}
</TableRowColumn>
<TableRowColumn>
<a
href={space.url}
style={{ textDecoration: 'none', color: 'white' }}
>
{space.url}
</a>
</TableRowColumn>
</TableRow>
)
)}
</TableBody>
</Table>
);
}
}
export default SpaceList;

View file

@ -0,0 +1,43 @@
import React from 'react';
import {
Toolbar as MuiToolbar,
ToolbarGroup,
ToolbarTitle,
} from 'material-ui/Toolbar';
import IconMenu from 'material-ui/IconMenu';
import IconButton from 'material-ui/IconButton';
import NavigationExpandMoreIcon from 'material-ui/svg-icons/navigation/expand-more';
import MenuItem from 'material-ui/MenuItem';
import { Link } from 'react-router-dom';
const Toolbar = () => (
<MuiToolbar>
<ToolbarTitle
text={'CCC Spaces'}
/>
<ToolbarGroup>
<IconMenu
iconButtonElement={
<IconButton touch>
<NavigationExpandMoreIcon />
</IconButton>
}
>
<MenuItem
primaryText={'Events'}
containerElement={<Link to="/" />}
/>
<MenuItem
primaryText={'Spaces'}
containerElement={<Link to="/list" />}
/>
<MenuItem
primaryText={'Impressum'}
href={'http://ccc.de/de/imprint'}
/>
</IconMenu>
</ToolbarGroup>
</MuiToolbar>
);
export default Toolbar;

View file

@ -0,0 +1,91 @@
import React from 'react';
import {
Table,
TableBody,
TableRow,
TableRowColumn,
} from 'material-ui/Table';
import TextField from 'material-ui/TextField';
import FlatButton from 'material-ui/FlatButton';
import moment from 'moment';
import { spaceUrlStruct } from '../redux/modules/spaceurl';
export class UrlList extends React.Component {
static propTypes = {
fetchSpaceUrl: React.PropTypes.func.isRequired,
validateSpaceUrl: React.PropTypes.func.isRequired,
spaceurls: spaceUrlStruct,
};
static defaultProps = {
spaceurls: {
items: [],
},
};
componentWillMount() {
this.props.fetchSpaceUrl();
}
getFormatedDateTime = timestamp => (
moment
.unix(timestamp)
.format('DD.MM.YYYY HH:mm')
);
validateSpaceUrl = (spaceUrl) => {
const validatedSpaceUrl = {
url: spaceUrl.url,
validated: true,
};
this.props.validateSpaceUrl(validatedSpaceUrl, this.secretInput.input.value);
};
render() {
return (
<div>
<Table
selectable
multiSelectable
>
<TableBody
showRowHover
stripedRows
displayRowCheckbox={false}
>
{this.props.spaceurls.items
.map(spaceurl => (
<TableRow key={spaceurl.url}>
<TableRowColumn>
<a
href={spaceurl.url}
style={{ color: 'white', textDecoration: 'none' }}
>
{spaceurl.url}
</a>
</TableRowColumn>
<TableRowColumn>
{this.getFormatedDateTime(spaceurl.lastUpdated)}
</TableRowColumn>
<TableRowColumn>
{!spaceurl.validated ? <FlatButton
label={'validated'}
onTouchTap={() => this.validateSpaceUrl(spaceurl)}
primary
/> : null}
</TableRowColumn>
</TableRow>
)
)}
</TableBody>
</Table>
<TextField
name={'secret-input'}
ref={ref => (this.secretInput = ref)}
/>
</div>
);
}
}
export default UrlList;

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { actions as spaceActions } from '../redux/modules/spacedata';
import Component from '../components/SpaceList';
const mapStateToProps = state => ({
spacedata: state.spacedata,
});
const mapDispatchToProps = {
...spaceActions,
};
export default connect(mapStateToProps, mapDispatchToProps)(Component);

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { actions as spaceUrlActions } from '../redux/modules/spaceurl';
import Component from '../components/UrlList';
const mapStateToProps = state => ({
spaceurls: state.spaceurls,
});
const mapDispatchToProps = {
...spaceUrlActions,
};
export default connect(mapStateToProps, mapDispatchToProps)(Component);

11
frontend/src/index.js Normal file
View file

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './style/css/reset.css';
import './style/css/style.css';
ReactDOM.render(
<App />,
document.getElementById('root')
);

View file

@ -0,0 +1,13 @@
import React from 'react';
import Toolbar from '../components/Toolbar';
const Layout = children => (
() => (
<div>
<Toolbar />
{children}
</div>
)
);
export default Layout;

View file

@ -0,0 +1,84 @@
import { PropTypes } from 'react';
import request from 'superagent';
import flatten from 'lodash/flatten';
import moment from 'moment';
import RRule from 'rrule';
import { createAction, handleActions } from 'redux-actions';
import config from '../../api/config';
export const eventStruct = {
start: PropTypes.string.isRequired,
WholeDayEvent: PropTypes.bool.isRequired,
Description: PropTypes.string.isRequired,
Summary: PropTypes.string.isRequired,
space: PropTypes.string.isRequired,
};
const CALENDARS_FETCHED = 'CALENDARS_FETCHED';
export const fetched = createAction(CALENDARS_FETCHED, result => result);
export const fetchCalendars = () => (dispatch) => {
request
.get(`${config.api.url}/calendar`)
.set('Content-Type', 'application/json')
.end(
(err, res) => {
if (!err) {
dispatch(fetched(res.body));
}
}
);
};
export const actions = {
fetchCalendars,
};
export default handleActions({
[CALENDARS_FETCHED]: (state, { payload }) => {
const items = flatten(flatten(
payload.map(
calendar => (
calendar.Events.map((event) => {
if (event.rrule) {
try {
const options = RRule.parseString(event.rrule);
options.dtstart = moment(event.start).toDate();
const rule = new RRule(options);
return rule.between(
moment().toDate(),
moment().add(3, 'months').toDate()
).map(date => (
{
...event,
space: calendar.Space,
start: moment(date),
end: null,
}
));
}
catch (ex) {
console.log(ex);
return [];
}
}
return [
{
...event,
space: calendar.Space,
start: moment(event.start),
end: moment(event.end),
},
];
})
)
)
))
.filter(event => event.start.isAfter())
.sort((a, b) => a.start - b.start);
return { items };
},
}, { items: [] });

View file

@ -0,0 +1,72 @@
import { PropTypes } from 'react';
import request from 'superagent';
import { createAction, handleActions } from 'redux-actions';
import config from '../../api/config';
export const filterStruct = PropTypes.arrayOf(PropTypes.string);
export const spacedataElementStruct = PropTypes.shape({
space: PropTypes.string.isRequired,
location: PropTypes.shape({
lat: PropTypes.number.isRequired,
lon: PropTypes.number.isRequired,
}),
});
export const itemsStruct = PropTypes.arrayOf(spacedataElementStruct);
export const spacedataStruct = PropTypes.shape({
items: itemsStruct,
filter: filterStruct,
});
const SPACEDATA_FETCHED = 'SPACEDATA_FETCHED';
const SPACEDATA_FILTER = 'SPACEDATA_FILTER';
export const fetched = createAction(SPACEDATA_FETCHED, result => result);
export const filter = createAction(SPACEDATA_FILTER, result => result);
export const fetchSpacedata = () => (dispatch) => {
request
.get(`${config.api.url}/spaces`)
.set('Content-Type', 'application/json')
.end(
(err, res) => {
if (!err) {
dispatch(fetched(res.body));
}
}
);
};
export const toggleFilterSpacedata = space => (dispatch) => {
dispatch(
filter(
{
space,
}
)
);
};
export const actions = {
fetchSpacedata,
toggleFilterSpacedata,
};
export default handleActions({
[SPACEDATA_FETCHED]: (state, { payload }) => (
{
...state,
items: payload,
}
),
[SPACEDATA_FILTER]: (state, { payload }) => {
const newState = { ...state };
if (newState.filter.indexOf(payload.space) === -1) {
newState.filter.push(payload.space);
} else {
newState.filter.splice(newState.filter.indexOf(payload.space), 1);
}
return newState;
},
}, { items: [], filter: [] });

View file

@ -0,0 +1,70 @@
import { PropTypes } from 'react';
import request from 'superagent';
import { createAction, handleActions } from 'redux-actions';
import config from '../../api/config';
export const itemStruct = PropTypes.shape({
url: PropTypes.string.isRequired,
validated: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number.isRequired,
});
export const spaceUrlStruct = PropTypes.shape({
items: PropTypes.arrayOf(itemStruct),
});
const SPACEURL_FETCHED = 'SPACEURL_FETCHED';
const SPACEURL_VALIDATE = 'SPACEURL_VALIDATE';
export const fetched = createAction(SPACEURL_FETCHED, result => result);
export const validate = createAction(SPACEURL_VALIDATE, result => result);
export const fetchSpaceUrl = () => (dispatch) => {
request
.get(`${config.api.url}/urls`)
.set('Content-Type', 'application/json')
.end(
(err, res) => {
if (!err) {
dispatch(fetched(res.body));
}
}
);
};
export const validateSpaceUrl = (spaceUrl, secret) => (dispatch) => {
request
.put(`${config.api.url}/urls/${secret}`)
.send(spaceUrl)
.set('Content-Type', 'application/json')
.end(
(err) => {
if (!err) {
dispatch(validate(spaceUrl));
}
}
);
};
export const actions = {
fetchSpaceUrl,
validateSpaceUrl,
};
export default handleActions({
[SPACEURL_FETCHED]: (state, { payload }) => ({
...state,
items: payload,
}),
[SPACEURL_VALIDATE]: (state, { payload }) => {
const newState = {
...state,
};
newState.items.forEach(spaceUrl => ({
...spaceUrl,
validated: spaceUrl.url === payload.url ? true : spaceUrl.validated,
}));
return newState;
},
}, { items: [] });

View file

@ -0,0 +1,10 @@
import { combineReducers } from 'redux';
import spacedataReducer from './modules/spacedata';
import calendarsReducer from './modules/calendar';
import spaceUrlsReducer from './modules/spaceurl';
export default combineReducers({
spacedata: spacedataReducer,
calendars: calendarsReducer,
spaceurls: spaceUrlsReducer,
});

View file

@ -0,0 +1,15 @@
import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './rootReducer';
const initialState = {};
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(thunk)
)
);
export default store;

View file

@ -0,0 +1,4 @@
html, body {
margin: 0;
padding: 0;
}

View file

@ -0,0 +1,3 @@
body {
background-color: #333333;
}

View file

@ -0,0 +1,23 @@
import { fade } from 'material-ui/utils/colorManipulator';
import { spacing, colors } from 'material-ui/styles';
export default {
spacing,
fontFamily: 'Roboto, sans-serif',
palette: {
primary1Color: colors.grey900,
primary2Color: colors.grey900,
primary3Color: colors.grey600,
accent1Color: colors.grey500,
accent2Color: colors.grey500,
accent3Color: colors.grey100,
textColor: colors.fullWhite,
secondaryTextColor: fade(colors.fullWhite, 0.7),
alternateTextColor: '#303030',
canvasColor: '#303030',
borderColor: fade(colors.fullWhite, 0.3),
disabledColor: fade(colors.fullWhite, 0.3),
pickerHeaderColor: fade(colors.fullWhite, 0.12),
clockCircleColor: fade(colors.fullWhite, 0.12),
},
};

View file

@ -0,0 +1,21 @@
import React from 'react';
import Map from '../components/Map';
import EventList from '../components/EventList';
const style = {
container: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
},
};
const IndexContainer = () => (
<div style={style.container}>
<Map />
<EventList />
</div>
);
export default IndexContainer;

View file

@ -0,0 +1,12 @@
import React from 'react';
import SpaceList from '../containers/SpaceList';
import SpaceApiInput from '../components/SpaceApiInput';
const SpaceListView = () => (
<div>
<SpaceList />
<SpaceApiInput />
</div>
);
export default SpaceListView;

View file

@ -0,0 +1,10 @@
import React from 'react';
import UrlList from '../containers/UrlList';
const UrlListView = () => (
<div>
<UrlList />
</div>
);
export default UrlListView;