Compare commits

..
Author SHA1 Message Date
greenkeeper[bot]
42553d5bd0 docs(readme): add Greenkeeper badge 2018-05-06 08:18:07 +00:00
greenkeeper[bot]
1db639647f chore(package): update dependencies 2018-05-06 08:18:04 +00:00
greenkeeper[bot]
4411192a4b chore: add Greenkeeper config file 2018-05-06 08:18:01 +00:00
43 changed files with 425 additions and 20373 deletions

View file

@ -1,10 +1,3 @@
# EVA frontend + backend
https://doku.ccc.de/SpaceAPI#spaceapi.ccc.de
> Timm hat 2016 ein SpaceAPI Directory für Chaos-nahe Gruppen geschrieben und es unter https://spaceapi.ccc.de deployed. Es sollten alle CCC(-nahen) Hackerspaces gelistet sein.
Der Code war ehemals auf zwei Repos verteilt
https://github.com/gidsi/eva-frontend + https://github.com/gidsi/eva-backend – dort gibt es auch noch offene Issues die man bei Bedarf auch Inspirationen abhohlen kann... – und wurde dann im Mono-Repo https://github.com/gidsi/spaceapi.ccc.de/ zusammengefasst, das jetzt hier ins Gitlab umgezogen ist.
Woher der Name EVA kommt ist aktuell nicht mehr direkt nachvollziehbar – falls du noch weißt wie wir damals auf diesen Namen gekommen sind, bearbeite gerne diesen Absatz.
[![Greenkeeper badge](https://badges.greenkeeper.io/gidsi/spaceapi.ccc.de.svg)](https://greenkeeper.io/)

View file

@ -5,10 +5,11 @@ COPY . .
RUN go get -d ./...
RUN go install ./...
FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /go/bin/spaceapi.ccc.de ./app
COPY --from=builder /go/bin/app .
COPY config.yaml config.yaml
EXPOSE 8080
CMD ["./app"]

View file

@ -13,5 +13,3 @@ SpaceAPI extensions
===================
* Key `ext_ccc` describes Chaos Computer Club relation status (Example values: `"chaostreff"` or `"erfa"`)

View file

@ -15,7 +15,7 @@ func getCalendars() {
outputChan := parser.GetOutputChan()
calendar := Calendar{}
calendar.Space = spaceData.Space
var events []Event
events := []Event{}
go func() {
for event := range outputChan {
events = append(events, mapEventObject(event))
@ -44,4 +44,4 @@ func mapEventObject(event *ics.Event) Event {
eventData.WholeDayEvent = event.GetWholeDayEvent()
return eventData
}
}

View file

@ -4,6 +4,4 @@ type ConfigFile struct {
SharedSecret string `yaml:"shared_secret,omitempty"`
MongoDbServer string `yaml:"mongodb_server,omitempty"`
MongoDbDatabase string `yaml:"mongodb_database,omitempty"`
DokuWikiUser string `yaml:"doku_wiki_user,omitempty"`
DokuWikiPassword string `yaml:"doku_wiki_password,omitempty"`
}
}

View file

@ -1,13 +1,13 @@
package main
import (
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
"log"
"gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson"
)
func writeSpaceData(data SpaceData) {
if data.Space != "" {
if(data.Space != "") {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
@ -17,7 +17,7 @@ func writeSpaceData(data SpaceData) {
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spacedata")
_, err = c.Upsert(bson.M{"space": data.Space}, data)
_, err = c.Upsert(bson.M{ "space": data.Space }, data)
if err != nil {
log.Fatal(err)
}
@ -34,9 +34,9 @@ func writeSpaceurl(spaceUrl SpaceUrl) {
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)
count, _ := c.Find(bson.M{ "url": spaceUrl.Url }).Count()
if(count == 0) {
c.Insert(spaceUrl);
}
}
@ -50,28 +50,7 @@ func writeCalendar(calendar Calendar) {
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("calendar")
c.Upsert(bson.M{"space": calendar.Space}, calendar)
}
func writeDecentralizedServices(services []DecentrealizedService) {
if len(services) == 0 {
return
}
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("decentralized_services")
c.DropCollection()
for _, service := range(services) {
c.Insert(service)
}
c.Upsert(bson.M{ "space": calendar.Space }, calendar)
}
func updateSpaceurl(spaceUrl SpaceUrl) {
@ -84,11 +63,11 @@ func updateSpaceurl(spaceUrl SpaceUrl) {
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spaceurl")
c.Update(bson.M{"url": spaceUrl.Url}, spaceUrl)
c.Update(bson.M{ "url": spaceUrl.Url }, spaceUrl);
}
func readSpacedata() []SpaceData {
session, err := mgo.Dial(config.MongoDbServer)
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
}
@ -97,8 +76,8 @@ func readSpacedata() []SpaceData {
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spacedata")
result := []SpaceData{}
c.Find(bson.M{}).Iter().All(&result)
result := []SpaceData{}
c.Find(bson.M{}).Iter().All(&result)
return result
}
@ -115,10 +94,11 @@ func readSpaceurl() []SpaceUrl {
c := session.DB(config.MongoDbDatabase).C("spaceurl")
result := []SpaceUrl{}
c.Find(bson.M{}).Iter().All(&result)
return result
}
func deleteSpaceurl(id string) error {
func deleteSpaceurl(String id) {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
@ -128,13 +108,7 @@ func deleteSpaceurl(id string) error {
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spaceurl")
err = c.Remove(bson.M{"id": id})
if err != nil {
log.Println(err)
return err
}
return nil
c.Remove(bson.M{"_id": id})
}
func readCalendar() []Calendar {
@ -151,21 +125,4 @@ func readCalendar() []Calendar {
c.Find(bson.M{}).Iter().All(&result)
return result
}
func readServices() []DecentrealizedService {
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("decentralized_services")
var result []DecentrealizedService
c.Find(bson.M{}).Iter().All(&result)
return result
}
}

View file

@ -1,100 +1,65 @@
package main
import (
"encoding/csv"
"encoding/json"
"github.com/gofrs/uuid"
"github.com/gorilla/mux"
"github.com/robfig/cron"
"gopkg.in/yaml.v2"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"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")
err := yaml.Unmarshal(data, &config)
if err != nil {
panic("Can't load config")
}
config.SharedSecret = os.Getenv("SHARED_SECRET")
config.DokuWikiUser = os.Getenv("DOKU_WIKI_USER")
config.DokuWikiPassword = os.Getenv("DOKU_WIKI_PASSWORD")
data, _ := ioutil.ReadFile("config.yaml")
yaml.Unmarshal(data, &config)
config.SharedSecret = os.Getenv("SHARED_SECRET")
updateDecentralizedServicesList()
c := cron.New()
err = c.AddFunc("@hourly", func() {
loadSpaceData()
getCalendars()
updateDecentralizedServicesList()
})
if err != nil {
log.Printf("Can't start cron %v", err)
} else {
c.Start()
}
router := NewRouter()
http.Handle("/", router)
log.Fatal(http.ListenAndServe(":8080", router))
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()
r, err := http.Get(url)
if err != nil {
return err
}
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(target)
return json.NewDecoder(r.Body).Decode(target)
}
func SpaceDataIndex(w http.ResponseWriter, r *http.Request) {
ReturnJson(w, readSpacedata())
ReturnJson(w, readSpacedata())
}
func SpaceUrlIndex(w http.ResponseWriter, r *http.Request) {
ReturnJson(w, readSpaceurl())
ReturnJson(w, readSpaceurl())
}
func CalendarIndex(w http.ResponseWriter, r *http.Request) {
ReturnJson(w, readCalendar())
}
func DecentralizedServicesIndex(w http.ResponseWriter, r *http.Request) {
ReturnJson(w, readServices())
ReturnJson(w, readCalendar())
}
func SpaceUrlAdd(w http.ResponseWriter, r *http.Request) {
spaceUrl := SpaceUrl{}
createEntry(&spaceUrl, w, r)
spaceUrl.Validated = false
generatedUuid, err := uuid.NewV4()
if err != nil {
log.Printf("%v", err)
w.WriteHeader(500)
} else {
spaceUrl.Id = generatedUuid.String()
writeSpaceurl(spaceUrl)
}
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)
}
vars := mux.Vars(r)
SharedSecret := vars["SharedSecret"]
if(SharedSecret == config.SharedSecret) {
spaceUrl := SpaceUrl{}
createEntry(&spaceUrl, w, r)
updateSpaceurl(spaceUrl)
}
}
func SpaceUrlDelete(w http.ResponseWriter, r *http.Request) {
@ -102,87 +67,36 @@ func SpaceUrlDelete(w http.ResponseWriter, r *http.Request) {
SharedSecret := vars["SharedSecret"]
Id := vars["id"]
if SharedSecret == config.SharedSecret {
err := deleteSpaceurl(Id)
if err != nil {
w.WriteHeader(500)
} else {
w.WriteHeader(204)
}
} else {
w.WriteHeader(401)
deleteSpaceurl(Id)
}
}
func loadSpaceData() {
spaceUrls := readSpaceurl()
spaceUrls := readSpaceurl()
timestamp := time.Now().Unix()
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)
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)
}
}
}
spaceUrl.LastUpdated = timestamp
updateSpaceurl(spaceUrl)
}
}
}
}
func refreshData(w http.ResponseWriter, r *http.Request) {
loadSpaceData()
getCalendars()
loadSpaceData()
getCalendars()
w.WriteHeader(204)
}
func updateDecentralizedServicesList() {
client := &http.Client{}
req, err := http.NewRequest("GET", "https://doku.ccc.de/index.php?title=Spezial:Ask&x=-5B-5BKategorie%3ADienste-5D-5D%2F-3FService-2DName%3DName%2F-3FService-2DProduct%3DProdukt%2F-3FService-2DIs-2DCentralized%3DZentralisiert%2F-3FService-2DVisibility%3DSichtbarkeit%2F-3FService-2DOrg%3DOrganisation%2F-3FService-2DType%3DTyp%2F-3FService-2DURL%3DURL%2F-3FService-2DContact%3DKontakt%2F-3FService-2DCheck-23ISO%3DOnline-20Check%2F-3FService-2DState%3DOnline-3F%2F-3FService-2DEncryption%3DVerschl%C3%BCsselung%2F-3FService-2DHas-2DIPv4%3DIPv4%2F-3FService-2DHas-2DIPv6%3DIPv6&mainlabel=-&limit=50&offset=0&format=csv&headers=show&searchlabel=CSV&default=%21no%20result%21&sep=%3B&valuesep=%3B&filename=dienste.csv", nil)
if err != nil{
log.Fatal(err)
}
req.SetBasicAuth(config.DokuWikiUser, config.DokuWikiPassword)
resp, err := client.Do(req)
if err != nil{
log.Fatal(err)
}
r := csv.NewReader(resp.Body)
r.Comma = ';'
var DecentrealizedServiceList []DecentrealizedService
for {
record, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
if len(record) > 9 && record[3] == "public" && record[9] == "wahr" && record[6] != "" {
foo := DecentrealizedService{
record[0],
record[1],
record[6],
}
DecentrealizedServiceList = append(DecentrealizedServiceList, foo)
}
}
writeDecentralizedServices(DecentrealizedServiceList)
}
type DecentrealizedService struct {
Name string `json:"name"`
Type string `json:"type"`
Url string `json:"url"`
}
w.WriteHeader(204)
}

View file

@ -3,20 +3,21 @@ package main
import "time"
type Calendar struct {
Space string
Events []Event
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:"wholeDayEvent"`
}
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"`
}

View file

@ -1,15 +0,0 @@
module github.com/gidsi/spaceapi.ccc.de
go 1.13
require (
github.com/PuloV/ics-golang v0.0.0-20190808201353-a3394d3bcade
github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61 // indirect
github.com/gidsi/ics-golang v0.0.0-20190331185529-e95e328c4a08
github.com/gofrs/uuid v3.3.0+incompatible
github.com/gorilla/mux v1.8.0
github.com/robfig/cron v1.2.0
github.com/robfig/cron/v3 v3.0.1
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22
gopkg.in/yaml.v2 v2.3.0
)

View file

@ -1,24 +0,0 @@
github.com/PuloV/ics-golang v0.0.0-20190808201353-a3394d3bcade h1:odEkSCl2gLWPtvraEdCyBZbeYyMMTysWPLMurnB8sUY=
github.com/PuloV/ics-golang v0.0.0-20190808201353-a3394d3bcade/go.mod h1:f1P3hjG+t54/IrnXMnnw+gRmFCDR/ryj9xSQ7MPMkQw=
github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61 h1:o64h9XF42kVEUuhuer2ehqrlX8rZmvQSU0+Vpj1rF6Q=
github.com/channelmeter/iso8601duration v0.0.0-20150204201828-8da3af7a2a61/go.mod h1:Rp8e0DCtEKwXFOC6JPJQVTz8tuGoGvw6Xfexggh/ed0=
github.com/gidsi/ics-golang v0.0.0-20190331185529-e95e328c4a08 h1:x57vmmjqLdRGv3bZQqoyzt4S08z5pTk/vkWaqXGnswI=
github.com/gidsi/ics-golang v0.0.0-20190331185529-e95e328c4a08/go.mod h1:oIDQdzzcgVLYQRfohyxBaOVr7pyfRydoPcEdVNXry4s=
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ=
github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View file

@ -26,4 +26,4 @@ func logError(err error) {
if err != nil {
log.Fatal(err)
}
}
}

View file

@ -1,9 +1,9 @@
package main
import (
"net/http"
"encoding/json"
"fmt"
"net/http"
)
type DataObject interface {
@ -45,4 +45,4 @@ func createEntry(i interface{}, w http.ResponseWriter, r *http.Request) {
panic(err)
}
}
}
}

View file

@ -1,9 +1,9 @@
package main
import (
"net/http"
"github.com/gorilla/mux"
"log"
"net/http"
)
func NewRouter() *mux.Router {
@ -13,21 +13,21 @@ func NewRouter() *mux.Router {
handler = Logger(route.Handler, route.Name)
router.
Methods(route.Method).
Path(route.Pattern).
Name(route.Name).
Handler(handler)
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.
Methods("OPTIONS").
Name("Options Handler").
Handler(http.HandlerFunc(optionsHandler))
router.NotFoundHandler = http.HandlerFunc(notFound)
@ -48,4 +48,4 @@ func optionsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE")
w.WriteHeader(200)
}
}

View file

@ -9,6 +9,7 @@ type Route struct {
Handler http.HandlerFunc
}
type Routes []Route
var IndexRoutes = Routes{
@ -36,12 +37,6 @@ var IndexRoutes = Routes{
"/calendar",
CalendarIndex,
},
Route{
"ServicesIndex",
"GET",
"/services",
DecentralizedServicesIndex,
},
Route{
"SpaceUrlAdd",
"POST",
@ -66,4 +61,4 @@ var IndexRoutes = Routes{
"/refresh",
refreshData,
},
}
}

Binary file not shown.

View file

@ -1,78 +1,44 @@
package main
// non standard should start with ext_
type Location struct {
Address string `json:"address"`
// non standard
ExtFloor int `json:"ext_floor"`
Lon float32 `json:"lon"`
Lat float32 `json:"lat"`
}
type SpaceFed struct {
SpaceNet bool `json:"spacenet"`
Spacesaml bool `json:"spacesaml"`
SpacePhone bool `json:"spacephone"`
}
type Stream struct {
M4 bool `json:"m4,omitempty"`
MJPEG bool `json:"mjpeg,omitempty"`
UStream bool `json:"ustream,omitempty"`
}
type State struct {
Open bool `json:"open"`
Lastchange float64 `json:"lastchange,omitempty"`
TriggerPerson string `json:"trigger_person,omitempty"`
Message string `json:"message,omitempty"`
Icon struct {
Open string `json:"open"`
Closed string `json:"closed"`
} `json:"icon,omitempty"`
}
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 Feed struct {
Type string `json:"type"`
Url string `json:"url"`
}
type Feeds struct {
Blog Feed `json:"blog"`
Wiki Feed `json:"wiki"`
Calendar Feed `json:"calendar"`
type Location struct {
Address string `json:"address"`
Lon float32 `json:"lon"`
Lat float32 `json:"lat"`
}
// main struct
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"`
SpaceFed SpaceFed `json:"spacefed,omitempty"`
Cam []string `json:"cam,omitempty"`
Stream *Stream `json:"stream,omitempty"`
State State `json:"state"`
// missing: `json:"events,omitempty"`
Contact *Contact `json:"contact,omitempty"`
IssueReportChannels []string `json:"issue_report_channels"`
// missing: `json:"sensors,omitempty"`
Feeds Feeds `json:"feeds,omitempty"`
// missing: `json:"cache,omitempty"`
Projects []string `json:"projects,omitempty"`
// missing: `json:"radio_show,omitempty"`
// not in spaceapi.io/docs
Ext_ccc string `json:"ext_ccc"`
}
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"`
}

View file

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

View file

@ -2,38 +2,16 @@ version: "3"
services:
frontend:
build: ./frontend
networks:
spaceapi-network:
ipv4_address: 172.16.238.10
image: gidsi/spaceapi-ccc-frontend:latest
restart: always
expose:
- "80"
depends_on:
- backend
backend:
build: ./backend
networks:
- spaceapi-network
image: gidsi/spaceapi-ccc-backend:latest
restart: always
environment:
- SHARED_SECRET=${SHARED_SECRET:-secret}
- DOKU_WIKI_USER=${DOKU_WIKI_USER}
- DOKU_WIKI_PASSWORD=${DOKU_WIKI_PASSWORD}
env_file: .env
depends_on:
- database
database:
image: mongo:latest
networks:
- spaceapi-network
restart: always
volumes:
- /opt/eva:/data/db
networks:
spaceapi-network:
ipam:
driver: default
config:
- subnet: 172.16.238.0/24

View file

@ -1,4 +1,4 @@
FROM node:16-alpine as builder
FROM node as builder
WORKDIR /app
COPY . .

View file

@ -1,14 +1,11 @@
eva-frontend
eve-frontend
=======================
The React frontent running https://spaceapi.ccc.de
Usage
-----
```
npm install
npm start
```
npm install -g yarn
yarn install
yarn start
```

View file

@ -9,7 +9,7 @@ http {
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
@ -44,20 +44,16 @@ http {
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
send_timeout 600;
}
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;
}
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;
}
}
}

19196
frontend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,40 +1,30 @@
{
"name": "eva-frontend",
"version": "1.1.0",
"version": "1.0.0",
"private": true,
"proxy": "https://spaceapi.ccc.de",
"dependencies": {
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"classnames": "^2.3.2",
"leaflet": "^1.9.4",
"moment": "^2.29.4",
"prop-types": "^15.8.1",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-leaflet": "^2.8.0",
"react-redux": "^7.2.9",
"react-router-dom": "^6.16.0",
"react-virtualized": "^9.22.5",
"redux": "^4.2.1",
"redux-actions": "^3.0.0",
"redux-thunk": "^2.4.2",
"rrule": "^2.7.2",
"superagent": "^8.1.2"
"leaflet": "^1.0.3",
"material-ui": "^0.20.0",
"moment": "^2.18.1",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-leaflet": "^1.1.6",
"react-redux": "^5.0.4",
"react-router-dom": "^4.1.1",
"react-tap-event-plugin": "^3.0.2",
"redux": "^4.0.0",
"redux-actions": "^2.0.2",
"redux-thunk": "^2.2.0",
"rrule": "^2.2.0",
"superagent": "^3.5.2"
},
"devDependencies": {
"react-scripts": "^5.0.1"
"react-scripts": "1.1.4"
},
"scripts": {
"start": "GENERATE_SOURCEMAP=false react-scripts start",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}
}

View file

@ -1,33 +1,33 @@
import React from 'react'
import {
BrowserRouter,
Route,
Routes
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/core/styles';
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 ServicesList from "./views/ServicesList";
import SpaceList from './views/SpaceList';
import UrlListView from './views/UrlListView';
import layout from './layout';
injectTapEventPlugin();
const App = () => (
<MuiThemeProvider theme={theme}>
<MuiThemeProvider muiTheme={getMuiTheme(theme)}>
<Provider store={store}>
<BrowserRouter>
<Routes>
<Route path="/" element={layout(<IndexContainer />)()} />
<Route path="/list" element={layout(<SpaceList />)()} />
<Route path="/services" element={layout(<ServicesList />)()} />
<Route path="/urls" element={layout(<UrlListView />)()} />
</Routes>
</BrowserRouter>
<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;
export default App;

View file

@ -1,156 +1,26 @@
import React from 'react';
import { connect } from 'react-redux';
import TableCell from '@material-ui/core/TableCell';
import PropTypes from 'prop-types';
import InfoIcon from '@material-ui/icons/InfoOutlined';
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 { actions as spaceDataActions, spacedataStruct } from '../redux/modules/spacedata';
import classNames from 'classnames';
import { withStyles } from '@material-ui/core/styles';
import { AutoSizer, Column, Table } from 'react-virtualized';
const styles = theme => ({
table: {
fontFamily: theme.typography.fontFamily,
border: 0,
},
flexContainer: {
display: 'flex',
alignItems: 'center',
boxSizing: 'border-box',
},
tableRow: {},
tableRowHover: {
'&:hover': {
backgroundColor: theme.palette.grey[600],
},
},
tableRowEven: {
backgroundColor: theme.palette.grey[700],
},
tableCell: {
flex: 1,
color: '#fff',
border: 0,
},
noClick: {
cursor: 'initial',
},
});
class MuiVirtualizedTable extends React.PureComponent {
getRowClassName = ({ index }) => {
const { classes, rowClassName } = this.props;
return classNames(classes.tableRow, classes.flexContainer, rowClassName, {
[classes.tableRowHover]: index !== -1,
[classes.tableRowEven]: index % 2,
});
};
cellRenderer = ({ cellData, columnIndex = null }) => {
const { columns, classes, rowHeight, onRowClick } = this.props;
return (
<TableCell
component="div"
className={classNames(classes.tableCell, classes.flexContainer, {
[classes.noClick]: onRowClick == null,
})}
variant="body"
style={{ height: rowHeight }}
align={(columnIndex != null && columns[columnIndex].numeric) || false ? 'right' : 'left'}
>
{cellData}
</TableCell>
);
};
render() {
const { classes, columns, ...tableProps } = this.props;
return (
<AutoSizer>
{({ height, width }) => (
<Table
className={classes.table}
height={height}
width={width}
{...tableProps}
rowClassName={this.getRowClassName}
>
{columns.map(({ cellContentRenderer = null, className, dataKey, ...other }, index) => {
let renderer;
if (cellContentRenderer != null) {
renderer = cellRendererProps =>
this.cellRenderer({
cellData: cellContentRenderer(cellRendererProps),
columnIndex: index,
});
} else {
renderer = this.cellRenderer;
}
return (
<Column
key={dataKey}
headerRenderer={() => {}}
className={classNames(classes.flexContainer, className)}
cellRenderer={renderer}
dataKey={dataKey}
{...other}
/>
);
})}
</Table>
)}
</AutoSizer>
);
}
}
MuiVirtualizedTable.propTypes = {
classes: PropTypes.object.isRequired,
columns: PropTypes.arrayOf(
PropTypes.shape({
cellContentRenderer: PropTypes.func,
dataKey: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
}),
).isRequired,
headerHeight: PropTypes.number,
onRowClick: PropTypes.func,
rowClassName: PropTypes.string,
rowHeight: PropTypes.oneOfType([PropTypes.number, PropTypes.func]),
sort: PropTypes.func,
};
MuiVirtualizedTable.defaultProps = {
rowHeight: 40,
headerHeight: 0,
};
const WrappedVirtualizedTable = withStyles(styles)(MuiVirtualizedTable);
import { spacedataStruct } from '../redux/modules/spacedata';
const mapStateToProps = state => ({
events: state.calendars.items,
spacedata: state.spacedata,
});
const mapDispatchToProps = {
...calendarActions,
...spaceDataActions,
};
class EventList extends React.Component {
static propTypes = {
events: PropTypes.arrayOf(
PropTypes.shape(eventStruct),
events: React.PropTypes.arrayOf(
React.PropTypes.shape(eventStruct),
),
fetchCalendars: PropTypes.func,
fetchCalendars: React.PropTypes.func,
spacedata: spacedataStruct,
};
static defaultProps = {
defaultProps = {
events: [],
};
@ -158,60 +28,56 @@ class EventList extends React.Component {
this.props.fetchCalendars();
}
formatDate = date => (date.format('DD.MM.YYYY'));
formatTime = date => (date.format('HH:mm'));
render() {
const rows = this.props.events.filter(event =>
(
this.props.spacedata.filter.indexOf(event.space) !== -1
|| this.props.spacedata.filter.length === 0
)
).map(event => {
return {
...event,
start: event.start.format('dd DD. MMM., HH:mm') ?? '',
summary: event.summary || event.description,
link: event.url && <a href={event.url}>
<InfoIcon style={{ cursor: 'pointer', color: '#fff' }} />
</a>,
}
});
const columns = [
{
width: 160,
label: 'Date',
dataKey: 'start',
},
{
width: 200,
flexGrow: 1,
label: 'Summary',
dataKey: 'summary',
},
{
width: 10,
label: 'Link',
dataKey: 'link',
},
{
width: 250,
label: 'Space',
dataKey: 'space',
align: 'right',
},
];
if ( this.props.spacedata.filter.length ) {
delete columns[3];
}
return (
<WrappedVirtualizedTable
rowCount={rows.length}
rowGetter={({ index }) => rows[index]}
columns={columns}
/>
<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, mapDispatchToProps)(EventList);
export default connect(mapStateToProps, {
...calendarActions,
})(EventList);

View file

@ -1,5 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Map as LeafletMap, TileLayer } from 'react-leaflet';
import { connect } from 'react-redux';
import Marker from './Marker';
@ -16,8 +15,8 @@ const mapDispatchToProps = {
class Map extends React.Component {
static propTypes = {
spacedata: spacedataStruct.isRequired,
fetchSpacedata: PropTypes.func.isRequired,
toggleFilterSpacedata: PropTypes.func.isRequired,
fetchSpacedata: React.PropTypes.func.isRequired,
toggleFilterSpacedata: React.PropTypes.func.isRequired,
};
componentWillMount() {
@ -30,10 +29,10 @@ class Map extends React.Component {
<LeafletMap
center={centerGermany}
zoom={5}
style={{ margin: 0, padding: 0, height: '100%', width: '100%', maxWidth: '100%' }}
style={{ width: '100vw', height: '50vh', margin: 0, padding: 0, maxWidth: '100%' }}
>
<TileLayer
url="/map/tiles/{z}/{x}/{y}.png"
url="https://spaceapi.ccc.de/map/tiles/{z}/{x}/{y}.png"
/>
{this.props.spacedata.items.map(
spacedata => (
@ -41,7 +40,8 @@ class Map extends React.Component {
spacedata={spacedata}
key={spacedata.space}
highlight={
this.props.spacedata.filter.indexOf(spacedata.space) !== -1}
this.props.spacedata.filter.length === 0
|| this.props.spacedata.filter.indexOf(spacedata.space) !== -1}
toggleFilterSpacedata={this.props.toggleFilterSpacedata}
/>
)

View file

@ -1,24 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { CircleMarker, Popup } from 'react-leaflet';
import theme from '../style/theme';
import { spacedataElementStruct } from '../redux/modules/spacedata';
const markerColor = (spacedata) => {
switch (spacedata.state?.open) {
case true:
return theme.palette.secondary.light;
case false:
return theme.palette.error.light;
default:
return theme.palette.primary.main;
}
};
const Marker = (props) => {
// change color based on space status
const color = markerColor(props.spacedata);
// const color = props.highlight ? theme.palette.secondary.light :
const color = props.highlight ? theme.palette.accent2Color : theme.palette.primary1Color;
const style = {
container: {
@ -37,10 +23,7 @@ const Marker = (props) => {
radius={5}
center={[props.spacedata.location.lat, props.spacedata.location.lon]}
>
<Popup
onOpen={() => props.toggleFilterSpacedata(props.spacedata.space)}
onClose={() => props.toggleFilterSpacedata(props.spacedata.space)}
>
<Popup>
<div style={style.container}>
<div>
{props.spacedata.space}
@ -57,8 +40,7 @@ const Marker = (props) => {
Marker.propTypes = {
spacedata: spacedataElementStruct.isRequired,
highlight: PropTypes.bool.isRequired,
toggleFilterSpacedata: PropTypes.func.isRequired,
highlight: React.PropTypes.bool.isRequired,
};
export default Marker;

View file

@ -1,100 +0,0 @@
import React from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { serviceStruct } from '../redux/modules/services';
import {withStyles} from "@material-ui/core";
const styles = theme => ({
table: {
fontFamily: theme.typography.fontFamily,
border: 0,
},
flexContainer: {
display: 'flex',
alignItems: 'center',
boxSizing: 'border-box',
},
tableRow: {},
tableRowHover: {
'&:hover': {
backgroundColor: theme.palette.grey[600],
},
},
tableRowEven: {
backgroundColor: theme.palette.grey[700],
},
tableCell: {
flex: 1,
color: '#fff',
border: 0,
},
noClick: {
cursor: 'initial',
},
});
const formatType = type => {
if (type.indexOf("[") === 0) {
return type.substring(type.indexOf(" ") + 1, type.length - 1);
}
return type;
}
export class ServiceList extends React.Component {
static propTypes = {
fetchServices: PropTypes.func.isRequired,
services: serviceStruct,
};
static defaultProps = {
services: {
items: [],
},
};
componentWillMount() {
this.props.fetchServices();
}
render() {
const items = this.props.services.items.sort(
(a, b) => formatType(a.type).toUpperCase().localeCompare(formatType(b.type).toUpperCase())
);
return (
<div style={{ paddingTop:'60px' }}>
<Table>
<TableBody>
{items
.map((service, index) => (
<TableRow key={service.name} className={classNames({
[this.props.classes.tableRowEven]: index % 2
})} >
<TableCell style={{ color: '#fff', border: 0 }}>
{service.name}
</TableCell>
<TableCell style={{ color: '#fff', border: 0 }}>
{formatType(service.type)}
</TableCell>
<TableCell style={{ color: '#fff', border: 0 }}>
<a
href={service.url}
style={{ textDecoration: 'none', color: 'white' }}
>
{service.url}
</a>
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</div>
);
}
}
export default withStyles(styles)(ServiceList);

View file

@ -1,72 +1,14 @@
import React from 'react';
import request from 'superagent';
import TextField from '@material-ui/core/TextField';
import FloatingActionButton from '@material-ui/core/Fab';
import SnackbarContent from '@material-ui/core/SnackbarContent';
import ContentAdd from '@material-ui/icons/AddOutlined';
import Snackbar from '@material-ui/core/Snackbar';
import PropTypes from 'prop-types';
import InfoIcon from '@material-ui/icons/Info';
import CloseIcon from '@material-ui/icons/Close';
import IconButton from '@material-ui/core/IconButton';
import { withStyles } from '@material-ui/core/styles';
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';
const styles = theme => ({
table: {
fontFamily: theme.typography.fontFamily,
border: 0,
},
flexContainer: {
display: 'flex',
alignItems: 'center',
boxSizing: 'border-box',
},
snackbar: {
backgroundColor: theme.palette.grey[700],
width: '100%',
},
});
class MuiSnackbarContent extends React.PureComponent {
render() {
console.log(this.props.classes);
return (
<SnackbarContent
aria-describedby="client-snackbar"
className={this.props.classes.snackbar}
message={
<div>
<InfoIcon/>
<div style={{ paddingLeft: '10px', paddingTop: '3px', float: 'right' }} >
Die URL wurde hinzugefuegt und befindet sich nun im review.
</div>
</div>
}
action={[
<IconButton
key="close"
aria-label="Close"
color="inherit"
onClick={() => this.setState({open: false})}
>
<CloseIcon/>
</IconButton>,
]}
/>
);
}
}
MuiSnackbarContent.propTypes = {
classes: PropTypes.object.isRequired,
};
const MySnackbarContent = withStyles(styles)(MuiSnackbarContent);
class SpaceApiInput extends React.Component {
static propTypes = {
style: PropTypes.shape({}),
style: React.PropTypes.shape({}),
};
static defaultProps = {
@ -116,7 +58,7 @@ class SpaceApiInput extends React.Component {
.set('Content-Type', 'application/json')
.end((err) => {
if (!err) {
this.spaceApiInput.value = '';
this.spaceApiInput.input.value = '';
this.setState({ open: true });
}
});
@ -135,28 +77,27 @@ class SpaceApiInput extends React.Component {
</p>
<div style={style.formContainer}>
<TextField
placeholder={'https://example.com/yourspaceapi.json'}
hintText={'https://example.com/yourspaceapi.json'}
name={'spaceapi-input'}
onChange={this.handleInputChange}
inputRef={ref => (this.spaceApiInput = ref)}
ref={ref => (this.spaceApiInput = ref)}
style={{ width: '100%', maxWidth: '340px' }}
/>
<FloatingActionButton
style={{ marginLeft: '20px' }}
onClick={this.handleButtonClick}
mini
onTouchTap={this.handleButtonClick}
>
<ContentAdd />
</FloatingActionButton>
</div>
<Snackbar
variant={'info'}
open={this.state.open}
message={'Die URL wurde hinzugefuegt und befindet sich nun im review.'}
autoHideDuration={4000}
style={{ minWidth: '490px' }}
onClose={() => this.setState({ open: false })}
>
<MySnackbarContent />
</Snackbar>
onRequestClose={() => this.setState({ open: false })}
/>
</div>
);
}

View file

@ -1,45 +1,15 @@
import React from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
Table,
TableBody,
TableRow,
TableRowColumn,
} from 'material-ui/Table';
import { spacedataStruct } from '../redux/modules/spacedata';
import {withStyles} from "@material-ui/core";
const styles = theme => ({
table: {
fontFamily: theme.typography.fontFamily,
border: 0,
},
flexContainer: {
display: 'flex',
alignItems: 'center',
boxSizing: 'border-box',
},
tableRow: {},
tableRowHover: {
'&:hover': {
backgroundColor: theme.palette.grey[600],
},
},
tableRowEven: {
backgroundColor: theme.palette.grey[700],
},
tableCell: {
flex: 1,
color: '#fff',
border: 0,
},
noClick: {
cursor: 'initial',
},
});
export class SpaceList extends React.Component {
static propTypes = {
fetchSpacedata: PropTypes.func.isRequired,
fetchSpacedata: React.PropTypes.func.isRequired,
spacedata: spacedataStruct,
};
@ -58,33 +28,36 @@ export class SpaceList extends React.Component {
(a, b) => a.space.toUpperCase().localeCompare(b.space.toUpperCase())
);
return (
<div style={{ paddingTop:'60px' }}>
<Table>
<TableBody>
{items
.map((space, index) => (
<TableRow key={space.space} className={classNames({
[this.props.classes.tableRowEven]: index % 2
})} >
<TableCell style={{ color: '#fff', border: 0 }}>
{space.space}
</TableCell>
<TableCell style={{ border: 0 }}>
<a
href={space.url}
style={{ textDecoration: 'none', color: 'white' }}
>
{space.url}
</a>
</TableCell>
</TableRow>
)
)}
</TableBody>
</Table>
</div>
<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 withStyles(styles)(SpaceList);
export default SpaceList;

View file

@ -1,99 +1,43 @@
import React from 'react';
import Toolbar from '@material-ui/core/Toolbar';
import AppBar from '@material-ui/core/AppBar';
import Typography from '@material-ui/core/Typography';
import IconButton from '@material-ui/core/IconButton';
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import Grow from '@material-ui/core/Grow';
import Paper from '@material-ui/core/Paper';
import Popper from '@material-ui/core/Popper';
import MenuItem from '@material-ui/core/MenuItem';
import MenuList from '@material-ui/core/MenuList';
import MenuIcon from '@material-ui/icons/Menu';
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>
);
class MyToolbar extends React.Component {
state = {
open: false,
};
handleToggle = () => {
this.setState(state => ({ open: !state.open }));
};
handleClose = event => {
if (this.anchorEl.contains(event.target)) {
return;
}
this.setState({ open: false });
};
render() {
return (
<AppBar position="fixed">
<Toolbar id="toolbar" >
<div style={{ width: "100%", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<Typography variant="h6" color="inherit">
CCC Spaces
</Typography>
<div>
<IconButton
buttonRef={node => {
this.anchorEl = node;
}}
aria-owns={this.state.open ? 'menu-list-grow' : undefined}
aria-haspopup="true"
onClick={this.handleToggle}
>
<MenuIcon />
</IconButton>
<Popper open={this.state.open} anchorEl={this.anchorEl} transition disablePortal>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
id="menu-list-grow"
style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }}
>
<Paper>
<ClickAwayListener onClickAway={this.handleClose}>
<MenuList>
<MenuItem
onClick={() => window.location.href = '/'}
containerElement={<Link to="/" />}
>
Events
</MenuItem>
<MenuItem
onClick={() => window.location.href = '/list'}
containerElement={<Link to="/list" />}
>
Spaces
</MenuItem>
<MenuItem
onClick={() => window.location.href = '/services'}
containerElement={<Link to="/services" />}
>
Services
</MenuItem>
<MenuItem
onClick={() => window.location.href = 'http://ccc.de/de/imprint'}
>
Impressum
</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
</Toolbar>
</AppBar>
);
}
}
export default MyToolbar;
export default Toolbar;

View file

@ -1,19 +1,19 @@
import React from 'react';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableRow from '@material-ui/core/TableRow';
import TableCell from '@material-ui/core/TableCell';
import TextField from '@material-ui/core/TextField';
import PropTypes from 'prop-types';
import Button from '@material-ui/core/Button';
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: PropTypes.func.isRequired,
validateSpaceUrl: PropTypes.func.isRequired,
deleteSpaceUrl: PropTypes.func.isRequired,
fetchSpaceUrl: React.PropTypes.func.isRequired,
validateSpaceUrl: React.PropTypes.func.isRequired,
spaceurls: spaceUrlStruct,
};
@ -38,44 +38,42 @@ export class UrlList extends React.Component {
url: spaceUrl.url,
validated: true,
};
this.props.validateSpaceUrl(validatedSpaceUrl, this.state.secret);
};
deleteSpaceUrl = (spaceUrl) => {
this.props.deleteSpaceUrl(spaceUrl.id, this.state.secret);
this.props.validateSpaceUrl(validatedSpaceUrl, this.secretInput.input.value);
};
render() {
return (
<div>
<Table>
<TableBody>
<Table
selectable
multiSelectable
>
<TableBody
showRowHover
stripedRows
displayRowCheckbox={false}
>
{this.props.spaceurls.items
.map(spaceurl => (
<TableRow key={spaceurl.url}>
<TableCell>
<TableRowColumn>
<a
href={spaceurl.url}
style={{ color: 'white', textDecoration: 'none' }}
>
{spaceurl.url}
</a>
</TableCell>
<TableCell>
</TableRowColumn>
<TableRowColumn>
{this.getFormatedDateTime(spaceurl.lastUpdated)}
</TableCell>
<TableCell>
{!spaceurl.validated ? <Button
onClick={() => this.validateSpaceUrl(spaceurl)}
>validated</Button> : null}
</TableCell>
<TableCell>
<Button
onClick={() => this.deleteSpaceUrl(spaceurl)}
>
delete
</Button>
</TableCell>
</TableRowColumn>
<TableRowColumn>
{!spaceurl.validated ? <FlatButton
label={'validated'}
onTouchTap={() => this.validateSpaceUrl(spaceurl)}
primary
/> : null}
</TableRowColumn>
</TableRow>
)
)}
@ -83,7 +81,7 @@ export class UrlList extends React.Component {
</Table>
<TextField
name={'secret-input'}
onChange={(event)=> this.setState({ secret: event.target.value })}
ref={ref => (this.secretInput = ref)}
/>
</div>
);

View file

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

View file

@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import { PropTypes } from 'react';
import request from 'superagent';
import flatten from 'lodash/flatten';
import moment from 'moment';
import { RRule } from 'rrule';
import RRule from 'rrule';
import { createAction, handleActions } from 'redux-actions';
import config from '../../api/config';
@ -60,7 +60,7 @@ export default handleActions({
));
}
catch (ex) {
console.log(ex, event);
console.log(ex);
return [];
}
}

View file

@ -1,44 +0,0 @@
import PropTypes from 'prop-types';
import request from 'superagent';
import { createAction, handleActions } from 'redux-actions';
import config from '../../api/config';
export const serviceElementStruct = PropTypes.shape({
name: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
});
export const itemsStruct = PropTypes.arrayOf(serviceElementStruct);
export const serviceStruct = PropTypes.shape({
items: itemsStruct,
});
const SERVICES_FETCHED = 'SERVICES_FETCHED';
export const fetched = createAction(SERVICES_FETCHED, result => result);
export const fetchServices = () => (dispatch) => {
request
.get(`${config.api.url}/services`)
.set('Content-Type', 'application/json')
.end(
(err, res) => {
if (!err) {
dispatch(fetched(res.body));
}
}
);
};
export const actions = {
fetchServices,
};
export default handleActions({
[SERVICES_FETCHED]: (state, { payload }) => (
{
...state,
items: payload,
}
),
}, { items: [] });

View file

@ -1,4 +1,4 @@
import PropTypes from 'prop-types';
import { PropTypes } from 'react';
import request from 'superagent';
import { createAction, handleActions } from 'redux-actions';
import config from '../../api/config';

View file

@ -1,10 +1,9 @@
import PropTypes from 'prop-types';
import { PropTypes } from 'react';
import request from 'superagent';
import { createAction, handleActions } from 'redux-actions';
import config from '../../api/config';
export const itemStruct = PropTypes.shape({
id: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
validated: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number.isRequired,
@ -15,11 +14,9 @@ export const spaceUrlStruct = PropTypes.shape({
const SPACEURL_FETCHED = 'SPACEURL_FETCHED';
const SPACEURL_VALIDATE = 'SPACEURL_VALIDATE';
const SPACEURL_DELETE = 'SPACEURL_DELETE';
export const fetched = createAction(SPACEURL_FETCHED, result => result);
export const validate = createAction(SPACEURL_VALIDATE, result => result);
export const deleteSpace = createAction(SPACEURL_DELETE, result => result);
export const fetchSpaceUrl = () => (dispatch) => {
request
@ -48,23 +45,9 @@ export const validateSpaceUrl = (spaceUrl, secret) => (dispatch) => {
);
};
export const deleteSpaceUrl = (spaceUrlId, secret) => (dispatch) => {
request
.delete(`${config.api.url}/urls/${spaceUrlId}/${secret}`)
.set('Content-Type', 'application/json')
.end(
(err) => {
if (!err) {
dispatch(deleteSpace(spaceUrlId));
}
}
);
};
export const actions = {
fetchSpaceUrl,
validateSpaceUrl,
deleteSpaceUrl,
};
export default handleActions({
@ -84,5 +67,4 @@ export default handleActions({
return newState;
},
[SPACEURL_DELETE]: (state, { payload }) => ({ items: state.items.filter(ele => ele.id === payload.id) }),
}, { items: [] });

View file

@ -2,11 +2,9 @@ import { combineReducers } from 'redux';
import spacedataReducer from './modules/spacedata';
import calendarsReducer from './modules/calendar';
import spaceUrlsReducer from './modules/spaceurl';
import servicesReducer from './modules/services';
export default combineReducers({
spacedata: spacedataReducer,
calendars: calendarsReducer,
spaceurls: spaceUrlsReducer,
services: servicesReducer,
});

View file

@ -1,29 +1,3 @@
body {
background-color: #333333;
}
#indexContainer {
display: flex;
flex-direction: column;
height: 100vh;
margin-top: 60px;
}
@media (min-width: 680px) {
#indexContainer {
flex-direction: row;
}
}
@media (orientation: landscape) {
#toolbar {
min-height: 20px !important;
.MuiIconButton-root {
padding: 3px !important;
}
}
#indexContainer {
margin-top: 32px;
}
}

View file

@ -1,23 +1,23 @@
import grey from '@material-ui/core/colors/grey';
import green from '@material-ui/core/colors/green';
import red from '@material-ui/core/colors/red';
import { createTheme } from '@material-ui/core/styles';
import { fade } from 'material-ui/utils/colorManipulator';
import { spacing, colors } from 'material-ui/styles';
export default createTheme({
typography: {
fontFamily: 'Roboto, sans-serif',
useNextVariants: true,
},
export default {
spacing,
fontFamily: 'Roboto, sans-serif',
palette: {
primary: grey,
secondary: green,
error: red,
// Used by `getContrastText()` to maximize the contrast between the background and
// the text.
contrastThreshold: 3,
// Used to shift a color's luminance by approximately
// two indexes within its tonal palette.
// E.g., shift from Red 500 to Red 300 or Red 700.
tonalOffset: 0.2,
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

@ -2,15 +2,19 @@ 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 id="indexContainer">
<div style={{ flex: 2 }}>
<Map />
</div>
<div style={{ flex: 2 }}>
<EventList />
</div>
<div style={style.container}>
<Map />
<EventList />
</div>
);

View file

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

9
greenkeeper.json Normal file
View file

@ -0,0 +1,9 @@
{
"groups": {
"default": {
"packages": [
"frontend/package.json"
]
}
}
}