Compare commits

...
Sign in to create a new pull request.

18 commits

Author SHA1 Message Date
5620b6a51f Add README.md 2023-10-01 12:55:37 +00:00
67624586ed change vertical/horizontal split based on orientation/screensize 2023-09-24 21:42:52 +02:00
ab6666bf0e chore: color markers based on state 2023-09-23 18:11:57 +02:00
f26a67633f chore: optimize EventList display 2023-09-23 18:11:09 +02:00
4be0ee36b4 upgrade dependencies 2023-09-23 12:57:01 +02:00
gidsi
204fc582f3
Update dependencies 2020-09-27 00:45:05 +02:00
gidsi
c7d0adf780
Add decentralized services 2020-09-27 00:30:22 +02:00
genofire
d2422d5036 fix deps - update 2019-12-28 19:21:48 +01:00
genofire
bcfc66731f fix SpaceData struct 2019-12-28 19:21:48 +01:00
genofire
89ae122fc3 gofmt 2019-12-28 19:21:48 +01:00
genofire
dc7db298bd improve SpaceData struct 2019-12-28 19:21:48 +01:00
gidsi
7ac5da8356
Fix styles 2019-04-05 23:53:16 +02:00
gidsi
f21f857e31
Remove old attribute 2019-04-04 00:01:30 +02:00
gidsi
e17a4bf627
Fix delete urls 2019-04-03 23:08:08 +02:00
gidsi
fcb31bdd10
Add id's to space urls 2019-04-03 22:29:12 +02:00
gidsi
e056451737
Adds cron 2019-04-02 23:12:08 +02:00
gidsi
1cc4af9c1b
finish chores 2019-04-02 22:14:49 +02:00
gidsi
64ad60a8e2
chores 2019-03-31 11:52:40 +02:00
42 changed files with 20375 additions and 415 deletions

10
README.md Normal file
View file

@ -0,0 +1,10 @@
# 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.

View file

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

View file

@ -13,3 +13,5 @@ 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
events := []Event{}
var 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,4 +4,6 @@ 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,7 +50,28 @@ func writeCalendar(calendar Calendar) {
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("calendar")
c.Upsert(bson.M{ "space": calendar.Space }, 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)
}
}
func updateSpaceurl(spaceUrl SpaceUrl) {
@ -63,11 +84,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)
}
@ -76,8 +97,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
}
@ -94,11 +115,10 @@ func readSpaceurl() []SpaceUrl {
c := session.DB(config.MongoDbDatabase).C("spaceurl")
result := []SpaceUrl{}
c.Find(bson.M{}).Iter().All(&result)
return result
}
func deleteSpaceurl(String id) {
func deleteSpaceurl(id string) error {
session, err := mgo.Dial(config.MongoDbServer)
if err != nil {
panic(err)
@ -108,7 +128,13 @@ func deleteSpaceurl(String id) {
session.SetMode(mgo.Monotonic, true)
c := session.DB(config.MongoDbDatabase).C("spaceurl")
c.Remove(bson.M{"_id": id})
err = c.Remove(bson.M{"id": id})
if err != nil {
log.Println(err)
return err
}
return nil
}
func readCalendar() []Calendar {
@ -125,4 +151,21 @@ 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,65 +1,100 @@
package main
import (
"net/http"
"encoding/json"
"gopkg.in/yaml.v2"
"log"
"io/ioutil"
"os"
"time"
"github.com/gorilla/mux"
"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"
)
var config = ConfigFile{}
func main() {
data, _ := ioutil.ReadFile("config.yaml")
yaml.Unmarshal(data, &config)
config.SharedSecret = os.Getenv("SHARED_SECRET")
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")
router := NewRouter()
http.Handle("/", router)
log.Fatal(http.ListenAndServe(":8080", router))
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))
}
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())
ReturnJson(w, readCalendar())
}
func DecentralizedServicesIndex(w http.ResponseWriter, r *http.Request) {
ReturnJson(w, readServices())
}
func SpaceUrlAdd(w http.ResponseWriter, r *http.Request) {
spaceUrl := SpaceUrl{}
createEntry(&spaceUrl, w, r)
spaceUrl.Validated = false
writeSpaceurl(spaceUrl)
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)
}
}
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) {
@ -67,36 +102,87 @@ func SpaceUrlDelete(w http.ResponseWriter, r *http.Request) {
SharedSecret := vars["SharedSecret"]
Id := vars["id"]
if SharedSecret == config.SharedSecret {
deleteSpaceurl(Id)
err := deleteSpaceurl(Id)
if err != nil {
w.WriteHeader(500)
} else {
w.WriteHeader(204)
}
} else {
w.WriteHeader(401)
}
}
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)
}
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"`
}

View file

@ -3,21 +3,20 @@ 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:"whileDayEvent"`
}
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"`
}

15
backend/go.mod Normal file
View file

@ -0,0 +1,15 @@
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
)

24
backend/go.sum Normal file
View file

@ -0,0 +1,24 @@
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))
Methods("OPTIONS").
Name("Options Handler").
Handler(http.HandlerFunc(optionsHandler))
}
router.
Methods("OPTIONS").
Name("Options Handler").
Handler(http.HandlerFunc(optionsHandler))
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,7 +9,6 @@ type Route struct {
Handler http.HandlerFunc
}
type Routes []Route
var IndexRoutes = Routes{
@ -37,6 +36,12 @@ var IndexRoutes = Routes{
"/calendar",
CalendarIndex,
},
Route{
"ServicesIndex",
"GET",
"/services",
DecentralizedServicesIndex,
},
Route{
"SpaceUrlAdd",
"POST",
@ -61,4 +66,4 @@ var IndexRoutes = Routes{
"/refresh",
refreshData,
},
}
}

BIN
backend/spaceapi.ccc.de Executable file

Binary file not shown.

View file

@ -1,44 +1,78 @@
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 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"`
Blog Feed `json:"blog"`
Wiki Feed `json:"wiki"`
Calendar Feed `json:"calendar"`
}
// main struct
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"`
}
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"`
}

View file

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

View file

@ -2,16 +2,38 @@ 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
env_file: .env
restart: always
environment:
- SHARED_SECRET=${SHARED_SECRET:-secret}
- DOKU_WIKI_USER=${DOKU_WIKI_USER}
- DOKU_WIKI_PASSWORD=${DOKU_WIKI_PASSWORD}
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 as builder
FROM node:16-alpine as builder
WORKDIR /app
COPY . .

View file

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

View file

@ -9,7 +9,7 @@ http {
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
gzip on;
@ -44,16 +44,20 @@ 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 Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,30 +1,40 @@
{
"name": "eva-frontend",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"proxy": "https://spaceapi.ccc.de",
"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"
"@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"
},
"devDependencies": {
"react-scripts": "0.9.5"
"react-scripts": "^5.0.1"
},
"scripts": {
"start": "react-scripts start",
"start": "GENERATE_SOURCEMAP=false 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 as Router,
Route
BrowserRouter,
Route,
Routes
} 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 { MuiThemeProvider } from '@material-ui/core/styles';
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 muiTheme={getMuiTheme(theme)}>
<MuiThemeProvider theme={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>
<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>
</Provider>
</MuiThemeProvider>
);
export default App;
export default App;

View file

@ -1,26 +1,156 @@
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 TableCell from '@material-ui/core/TableCell';
import PropTypes from 'prop-types';
import InfoIcon from '@material-ui/icons/InfoOutlined';
import { actions as calendarActions, eventStruct } from '../redux/modules/calendar';
import { spacedataStruct } from '../redux/modules/spacedata';
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);
const mapStateToProps = state => ({
events: state.calendars.items,
spacedata: state.spacedata,
});
const mapDispatchToProps = {
...calendarActions,
...spaceDataActions,
};
class EventList extends React.Component {
static propTypes = {
events: React.PropTypes.arrayOf(
React.PropTypes.shape(eventStruct),
events: PropTypes.arrayOf(
PropTypes.shape(eventStruct),
),
fetchCalendars: React.PropTypes.func,
fetchCalendars: PropTypes.func,
spacedata: spacedataStruct,
};
defaultProps = {
static defaultProps = {
events: [],
};
@ -28,56 +158,60 @@ 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 (
<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>
<WrappedVirtualizedTable
rowCount={rows.length}
rowGetter={({ index }) => rows[index]}
columns={columns}
/>
);
}
}
export default connect(mapStateToProps, {
...calendarActions,
})(EventList);
export default connect(mapStateToProps, mapDispatchToProps)(EventList);

View file

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

View file

@ -1,10 +1,24 @@
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) => {
const color = props.highlight ? theme.palette.accent2Color : theme.palette.primary1Color;
// change color based on space status
const color = markerColor(props.spacedata);
// const color = props.highlight ? theme.palette.secondary.light :
const style = {
container: {
@ -23,7 +37,10 @@ const Marker = (props) => {
radius={5}
center={[props.spacedata.location.lat, props.spacedata.location.lon]}
>
<Popup>
<Popup
onOpen={() => props.toggleFilterSpacedata(props.spacedata.space)}
onClose={() => props.toggleFilterSpacedata(props.spacedata.space)}
>
<div style={style.container}>
<div>
{props.spacedata.space}
@ -40,7 +57,8 @@ const Marker = (props) => {
Marker.propTypes = {
spacedata: spacedataElementStruct.isRequired,
highlight: React.PropTypes.bool.isRequired,
highlight: PropTypes.bool.isRequired,
toggleFilterSpacedata: PropTypes.func.isRequired,
};
export default Marker;

View file

@ -0,0 +1,100 @@
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,14 +1,72 @@
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 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 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: React.PropTypes.shape({}),
style: PropTypes.shape({}),
};
static defaultProps = {
@ -58,7 +116,7 @@ class SpaceApiInput extends React.Component {
.set('Content-Type', 'application/json')
.end((err) => {
if (!err) {
this.spaceApiInput.input.value = '';
this.spaceApiInput.value = '';
this.setState({ open: true });
}
});
@ -77,27 +135,28 @@ class SpaceApiInput extends React.Component {
</p>
<div style={style.formContainer}>
<TextField
hintText={'https://example.com/yourspaceapi.json'}
placeholder={'https://example.com/yourspaceapi.json'}
name={'spaceapi-input'}
onChange={this.handleInputChange}
ref={ref => (this.spaceApiInput = ref)}
inputRef={ref => (this.spaceApiInput = ref)}
style={{ width: '100%', maxWidth: '340px' }}
/>
<FloatingActionButton
style={{ marginLeft: '20px' }}
mini
onTouchTap={this.handleButtonClick}
onClick={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' }}
onRequestClose={() => this.setState({ open: false })}
/>
onClose={() => this.setState({ open: false })}
>
<MySnackbarContent />
</Snackbar>
</div>
);
}

View file

@ -1,15 +1,45 @@
import React from 'react';
import {
Table,
TableBody,
TableRow,
TableRowColumn,
} from 'material-ui/Table';
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 { 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: React.PropTypes.func.isRequired,
fetchSpacedata: PropTypes.func.isRequired,
spacedata: spacedataStruct,
};
@ -28,36 +58,33 @@ export class SpaceList extends React.Component {
(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>
<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>
);
}
}
export default SpaceList;
export default withStyles(styles)(SpaceList);

View file

@ -1,43 +1,99 @@
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 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 { 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;
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;

View file

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

View file

@ -0,0 +1,13 @@
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 'react';
import PropTypes from 'prop-types';
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);
console.log(ex, event);
return [];
}
}

View file

@ -0,0 +1,44 @@
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 'react';
import PropTypes from 'prop-types';
import request from 'superagent';
import { createAction, handleActions } from 'redux-actions';
import config from '../../api/config';

View file

@ -1,9 +1,10 @@
import { PropTypes } from 'react';
import PropTypes from 'prop-types';
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,
@ -14,9 +15,11 @@ 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
@ -45,9 +48,23 @@ 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({
@ -67,4 +84,5 @@ export default handleActions({
return newState;
},
[SPACEURL_DELETE]: (state, { payload }) => ({ items: state.items.filter(ele => ele.id === payload.id) }),
}, { items: [] });

View file

@ -2,9 +2,11 @@ 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,3 +1,29 @@
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 { fade } from 'material-ui/utils/colorManipulator';
import { spacing, colors } from 'material-ui/styles';
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';
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),
export default createTheme({
typography: {
fontFamily: 'Roboto, sans-serif',
useNextVariants: true,
},
};
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,
},
});

View file

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

View file

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