Compare commits
18 commits
greenkeepe
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5620b6a51f | |||
| 67624586ed | |||
| ab6666bf0e | |||
| f26a67633f | |||
| 4be0ee36b4 | |||
|
|
204fc582f3 |
||
|
|
c7d0adf780 |
||
|
|
d2422d5036 | ||
|
|
bcfc66731f | ||
|
|
89ae122fc3 | ||
|
|
dc7db298bd | ||
|
|
7ac5da8356 |
||
|
|
f21f857e31 |
||
|
|
e17a4bf627 |
||
|
|
fcb31bdd10 |
||
|
|
e056451737 |
||
|
|
1cc4af9c1b |
||
|
|
64ad60a8e2 |
42 changed files with 20375 additions and 415 deletions
10
README.md
Normal file
10
README.md
Normal 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.
|
||||||
|
|
@ -5,11 +5,10 @@ COPY . .
|
||||||
RUN go get -d ./...
|
RUN go get -d ./...
|
||||||
RUN go install ./...
|
RUN go install ./...
|
||||||
|
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
RUN apk --no-cache add ca-certificates
|
RUN apk --no-cache add ca-certificates tzdata
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /go/bin/app .
|
COPY --from=builder /go/bin/spaceapi.ccc.de ./app
|
||||||
COPY config.yaml config.yaml
|
COPY config.yaml config.yaml
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD ["./app"]
|
CMD ["./app"]
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,5 @@ SpaceAPI extensions
|
||||||
===================
|
===================
|
||||||
|
|
||||||
* Key `ext_ccc` describes Chaos Computer Club relation status (Example values: `"chaostreff"` or `"erfa"`)
|
* Key `ext_ccc` describes Chaos Computer Club relation status (Example values: `"chaostreff"` or `"erfa"`)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ func getCalendars() {
|
||||||
outputChan := parser.GetOutputChan()
|
outputChan := parser.GetOutputChan()
|
||||||
calendar := Calendar{}
|
calendar := Calendar{}
|
||||||
calendar.Space = spaceData.Space
|
calendar.Space = spaceData.Space
|
||||||
events := []Event{}
|
var events []Event
|
||||||
go func() {
|
go func() {
|
||||||
for event := range outputChan {
|
for event := range outputChan {
|
||||||
events = append(events, mapEventObject(event))
|
events = append(events, mapEventObject(event))
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,6 @@ type ConfigFile struct {
|
||||||
SharedSecret string `yaml:"shared_secret,omitempty"`
|
SharedSecret string `yaml:"shared_secret,omitempty"`
|
||||||
MongoDbServer string `yaml:"mongodb_server,omitempty"`
|
MongoDbServer string `yaml:"mongodb_server,omitempty"`
|
||||||
MongoDbDatabase string `yaml:"mongodb_database,omitempty"`
|
MongoDbDatabase string `yaml:"mongodb_database,omitempty"`
|
||||||
|
DokuWikiUser string `yaml:"doku_wiki_user,omitempty"`
|
||||||
|
DokuWikiPassword string `yaml:"doku_wiki_password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"gopkg.in/mgo.v2"
|
"gopkg.in/mgo.v2"
|
||||||
"gopkg.in/mgo.v2/bson"
|
"gopkg.in/mgo.v2/bson"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeSpaceData(data SpaceData) {
|
func writeSpaceData(data SpaceData) {
|
||||||
if(data.Space != "") {
|
if data.Space != "" {
|
||||||
session, err := mgo.Dial(config.MongoDbServer)
|
session, err := mgo.Dial(config.MongoDbServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -35,8 +35,8 @@ func writeSpaceurl(spaceUrl SpaceUrl) {
|
||||||
|
|
||||||
c := session.DB(config.MongoDbDatabase).C("spaceurl")
|
c := session.DB(config.MongoDbDatabase).C("spaceurl")
|
||||||
count, _ := c.Find(bson.M{"url": spaceUrl.Url}).Count()
|
count, _ := c.Find(bson.M{"url": spaceUrl.Url}).Count()
|
||||||
if(count == 0) {
|
if count == 0 {
|
||||||
c.Insert(spaceUrl);
|
c.Insert(spaceUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,6 +53,27 @@ func writeCalendar(calendar 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) {
|
func updateSpaceurl(spaceUrl SpaceUrl) {
|
||||||
session, err := mgo.Dial(config.MongoDbServer)
|
session, err := mgo.Dial(config.MongoDbServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -63,7 +84,7 @@ func updateSpaceurl(spaceUrl SpaceUrl) {
|
||||||
session.SetMode(mgo.Monotonic, true)
|
session.SetMode(mgo.Monotonic, true)
|
||||||
|
|
||||||
c := session.DB(config.MongoDbDatabase).C("spaceurl")
|
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 {
|
func readSpacedata() []SpaceData {
|
||||||
|
|
@ -94,11 +115,10 @@ func readSpaceurl() []SpaceUrl {
|
||||||
c := session.DB(config.MongoDbDatabase).C("spaceurl")
|
c := session.DB(config.MongoDbDatabase).C("spaceurl")
|
||||||
result := []SpaceUrl{}
|
result := []SpaceUrl{}
|
||||||
c.Find(bson.M{}).Iter().All(&result)
|
c.Find(bson.M{}).Iter().All(&result)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteSpaceurl(String id) {
|
func deleteSpaceurl(id string) error {
|
||||||
session, err := mgo.Dial(config.MongoDbServer)
|
session, err := mgo.Dial(config.MongoDbServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -108,7 +128,13 @@ func deleteSpaceurl(String id) {
|
||||||
session.SetMode(mgo.Monotonic, true)
|
session.SetMode(mgo.Monotonic, true)
|
||||||
|
|
||||||
c := session.DB(config.MongoDbDatabase).C("spaceurl")
|
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 {
|
func readCalendar() []Calendar {
|
||||||
|
|
@ -126,3 +152,20 @@ func readCalendar() []Calendar {
|
||||||
|
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,45 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"encoding/csv"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"github.com/gofrs/uuid"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/robfig/cron"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"log"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var config = ConfigFile{}
|
var config = ConfigFile{}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
data, _ := ioutil.ReadFile("config.yaml")
|
data, _ := ioutil.ReadFile("config.yaml")
|
||||||
yaml.Unmarshal(data, &config)
|
err := yaml.Unmarshal(data, &config)
|
||||||
|
if err != nil {
|
||||||
|
panic("Can't load config")
|
||||||
|
}
|
||||||
config.SharedSecret = os.Getenv("SHARED_SECRET")
|
config.SharedSecret = os.Getenv("SHARED_SECRET")
|
||||||
|
config.DokuWikiUser = os.Getenv("DOKU_WIKI_USER")
|
||||||
|
config.DokuWikiPassword = os.Getenv("DOKU_WIKI_PASSWORD")
|
||||||
|
|
||||||
|
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()
|
router := NewRouter()
|
||||||
http.Handle("/", router)
|
http.Handle("/", router)
|
||||||
|
|
@ -45,17 +68,29 @@ 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) {
|
func SpaceUrlAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
spaceUrl := SpaceUrl{}
|
spaceUrl := SpaceUrl{}
|
||||||
createEntry(&spaceUrl, w, r)
|
createEntry(&spaceUrl, w, r)
|
||||||
spaceUrl.Validated = false
|
spaceUrl.Validated = false
|
||||||
|
|
||||||
|
generatedUuid, err := uuid.NewV4()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%v", err)
|
||||||
|
w.WriteHeader(500)
|
||||||
|
} else {
|
||||||
|
spaceUrl.Id = generatedUuid.String()
|
||||||
writeSpaceurl(spaceUrl)
|
writeSpaceurl(spaceUrl)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func SpaceUrlUpdate(w http.ResponseWriter, r *http.Request) {
|
func SpaceUrlUpdate(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
SharedSecret := vars["SharedSecret"]
|
SharedSecret := vars["SharedSecret"]
|
||||||
if(SharedSecret == config.SharedSecret) {
|
if SharedSecret == config.SharedSecret {
|
||||||
spaceUrl := SpaceUrl{}
|
spaceUrl := SpaceUrl{}
|
||||||
createEntry(&spaceUrl, w, r)
|
createEntry(&spaceUrl, w, r)
|
||||||
updateSpaceurl(spaceUrl)
|
updateSpaceurl(spaceUrl)
|
||||||
|
|
@ -67,7 +102,14 @@ func SpaceUrlDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
SharedSecret := vars["SharedSecret"]
|
SharedSecret := vars["SharedSecret"]
|
||||||
Id := vars["id"]
|
Id := vars["id"]
|
||||||
if SharedSecret == config.SharedSecret {
|
if SharedSecret == config.SharedSecret {
|
||||||
deleteSpaceurl(Id)
|
err := deleteSpaceurl(Id)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(204)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(401)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -77,11 +119,10 @@ func loadSpaceData() {
|
||||||
timestamp := time.Now().Unix()
|
timestamp := time.Now().Unix()
|
||||||
|
|
||||||
for _, spaceUrl := range spaceUrls {
|
for _, spaceUrl := range spaceUrls {
|
||||||
if(spaceUrl.Validated && int64(spaceUrl.LastUpdated + 60) < timestamp) {
|
if spaceUrl.Validated && int64(spaceUrl.LastUpdated+60) < timestamp {
|
||||||
spaceData := SpaceData{}
|
spaceData := SpaceData{}
|
||||||
err := getJson(spaceUrl.Url, &spaceData)
|
err := getJson(spaceUrl.Url, &spaceData)
|
||||||
if err != nil
|
if err != nil {
|
||||||
{
|
|
||||||
log.Println(spaceUrl.Url)
|
log.Println(spaceUrl.Url)
|
||||||
log.Println(err)
|
log.Println(err)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -100,3 +141,48 @@ func refreshData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
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"`
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,5 @@ type Event struct {
|
||||||
Class string `json:"class"`
|
Class string `json:"class"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Sequence int `json:"sequence"`
|
Sequence int `json:"sequence"`
|
||||||
WholeDayEvent bool `json:"whileDayEvent"`
|
WholeDayEvent bool `json:"wholeDayEvent"`
|
||||||
|
|
||||||
}
|
}
|
||||||
15
backend/go.mod
Normal file
15
backend/go.mod
Normal 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
24
backend/go.sum
Normal 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=
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DataObject interface {
|
type DataObject interface {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRouter() *mux.Router {
|
func NewRouter() *mux.Router {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ type Route struct {
|
||||||
Handler http.HandlerFunc
|
Handler http.HandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Routes []Route
|
type Routes []Route
|
||||||
|
|
||||||
var IndexRoutes = Routes{
|
var IndexRoutes = Routes{
|
||||||
|
|
@ -37,6 +36,12 @@ var IndexRoutes = Routes{
|
||||||
"/calendar",
|
"/calendar",
|
||||||
CalendarIndex,
|
CalendarIndex,
|
||||||
},
|
},
|
||||||
|
Route{
|
||||||
|
"ServicesIndex",
|
||||||
|
"GET",
|
||||||
|
"/services",
|
||||||
|
DecentralizedServicesIndex,
|
||||||
|
},
|
||||||
Route{
|
Route{
|
||||||
"SpaceUrlAdd",
|
"SpaceUrlAdd",
|
||||||
"POST",
|
"POST",
|
||||||
|
|
|
||||||
BIN
backend/spaceapi.ccc.de
Executable file
BIN
backend/spaceapi.ccc.de
Executable file
Binary file not shown.
|
|
@ -1,16 +1,38 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
type Feed struct {
|
// non standard should start with ext_
|
||||||
Type string `json:"type"`
|
|
||||||
Url string `json:"url"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Location struct {
|
type Location struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
|
// non standard
|
||||||
|
ExtFloor int `json:"ext_floor"`
|
||||||
Lon float32 `json:"lon"`
|
Lon float32 `json:"lon"`
|
||||||
Lat float32 `json:"lat"`
|
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 {
|
type Contact struct {
|
||||||
Twitter string `json:"twitter"`
|
Twitter string `json:"twitter"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
|
|
@ -20,8 +42,9 @@ type Contact struct {
|
||||||
IssueMail string `json:"issue_mail"`
|
IssueMail string `json:"issue_mail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type Feed struct {
|
||||||
Open bool `json:"open"`
|
Type string `json:"type"`
|
||||||
|
Url string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Feeds struct {
|
type Feeds struct {
|
||||||
|
|
@ -30,15 +53,26 @@ type Feeds struct {
|
||||||
Calendar Feed `json:"calendar"`
|
Calendar Feed `json:"calendar"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// main struct
|
||||||
|
|
||||||
type SpaceData struct {
|
type SpaceData struct {
|
||||||
Api string `json:"api"`
|
Api string `json:"api"`
|
||||||
Space string `json:"space"`
|
Space string `json:"space"`
|
||||||
Logo string `json:"logo"`
|
Logo string `json:"logo"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Location Location `json:"location"`
|
Location Location `json:"location"`
|
||||||
Contact Contact `json:"contact"`
|
SpaceFed SpaceFed `json:"spacefed,omitempty"`
|
||||||
IssueReportChannels []string `json:"issue_report_channels"`
|
Cam []string `json:"cam,omitempty"`
|
||||||
|
Stream *Stream `json:"stream,omitempty"`
|
||||||
State State `json:"state"`
|
State State `json:"state"`
|
||||||
Feeds Feeds `json:"feeds"`
|
// 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"`
|
Ext_ccc string `json:"ext_ccc"`
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
type SpaceUrl struct {
|
type SpaceUrl struct {
|
||||||
|
Id string `json:"id"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Validated bool `json:"validated"`
|
Validated bool `json:"validated"`
|
||||||
LastUpdated int64 `json:"lastUpdated"`
|
LastUpdated int64 `json:"lastUpdated"`
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,38 @@ version: "3"
|
||||||
services:
|
services:
|
||||||
frontend:
|
frontend:
|
||||||
build: ./frontend
|
build: ./frontend
|
||||||
|
networks:
|
||||||
|
spaceapi-network:
|
||||||
|
ipv4_address: 172.16.238.10
|
||||||
image: gidsi/spaceapi-ccc-frontend:latest
|
image: gidsi/spaceapi-ccc-frontend:latest
|
||||||
|
restart: always
|
||||||
|
expose:
|
||||||
|
- "80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
backend:
|
backend:
|
||||||
build: ./backend
|
build: ./backend
|
||||||
|
networks:
|
||||||
|
- spaceapi-network
|
||||||
image: gidsi/spaceapi-ccc-backend:latest
|
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:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
database:
|
database:
|
||||||
image: mongo:latest
|
image: mongo:latest
|
||||||
|
networks:
|
||||||
|
- spaceapi-network
|
||||||
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- /opt/eva:/data/db
|
- /opt/eva:/data/db
|
||||||
|
|
||||||
|
networks:
|
||||||
|
spaceapi-network:
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: 172.16.238.0/24
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node as builder
|
FROM node:16-alpine as builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
eve-frontend
|
eva-frontend
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
|
The React frontent running https://spaceapi.ccc.de
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
```
|
```
|
||||||
npm install -g yarn
|
npm install
|
||||||
yarn install
|
npm start
|
||||||
yarn start
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,10 @@ http {
|
||||||
proxy_set_header Connection 'upgrade';
|
proxy_set_header Connection 'upgrade';
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_cache_bypass $http_upgrade;
|
proxy_cache_bypass $http_upgrade;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
send_timeout 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /map/tiles/ {
|
location /map/tiles/ {
|
||||||
|
|
|
||||||
19196
frontend/package-lock.json
generated
Normal file
19196
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,30 +1,40 @@
|
||||||
{
|
{
|
||||||
"name": "eva-frontend",
|
"name": "eva-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"proxy": "https://spaceapi.ccc.de",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"leaflet": "^1.0.3",
|
"@material-ui/core": "^4.12.4",
|
||||||
"material-ui": "^0.17.4",
|
"@material-ui/icons": "^4.11.3",
|
||||||
"moment": "^2.18.1",
|
"classnames": "^2.3.2",
|
||||||
"react": "^15.5.4",
|
"leaflet": "^1.9.4",
|
||||||
"react-dom": "^15.5.4",
|
"moment": "^2.29.4",
|
||||||
"react-leaflet": "^1.1.6",
|
"prop-types": "^15.8.1",
|
||||||
"react-redux": "^5.0.4",
|
"react": "^16.14.0",
|
||||||
"react-router-dom": "^4.1.1",
|
"react-dom": "^16.14.0",
|
||||||
"react-tap-event-plugin": "^2.0.1",
|
"react-leaflet": "^2.8.0",
|
||||||
"redux": "^3.6.0",
|
"react-redux": "^7.2.9",
|
||||||
"redux-actions": "^2.0.2",
|
"react-router-dom": "^6.16.0",
|
||||||
"redux-thunk": "^2.2.0",
|
"react-virtualized": "^9.22.5",
|
||||||
"rrule": "^2.2.0",
|
"redux": "^4.2.1",
|
||||||
"superagent": "^3.5.2"
|
"redux-actions": "^3.0.0",
|
||||||
|
"redux-thunk": "^2.4.2",
|
||||||
|
"rrule": "^2.7.2",
|
||||||
|
"superagent": "^8.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"react-scripts": "0.9.5"
|
"react-scripts": "^5.0.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "GENERATE_SOURCEMAP=false react-scripts start",
|
||||||
"build": "react-scripts build",
|
"build": "react-scripts build",
|
||||||
"test": "react-scripts test --env=jsdom",
|
"test": "react-scripts test --env=jsdom",
|
||||||
"eject": "react-scripts eject"
|
"eject": "react-scripts eject"
|
||||||
}
|
},
|
||||||
|
"browserslist": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not ie <= 11",
|
||||||
|
"not op_mini all"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
BrowserRouter as Router,
|
BrowserRouter,
|
||||||
Route
|
Route,
|
||||||
|
Routes
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
import injectTapEventPlugin from 'react-tap-event-plugin';
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
|
import { MuiThemeProvider } from '@material-ui/core/styles';
|
||||||
import getMuiTheme from 'material-ui/styles/getMuiTheme';
|
|
||||||
import theme from './style/theme';
|
import theme from './style/theme';
|
||||||
import store from './redux/store';
|
import store from './redux/store';
|
||||||
import IndexContainer from './views/Index';
|
import IndexContainer from './views/Index';
|
||||||
|
import ServicesList from "./views/ServicesList";
|
||||||
import SpaceList from './views/SpaceList';
|
import SpaceList from './views/SpaceList';
|
||||||
import UrlListView from './views/UrlListView';
|
import UrlListView from './views/UrlListView';
|
||||||
import layout from './layout';
|
import layout from './layout';
|
||||||
|
|
||||||
injectTapEventPlugin();
|
|
||||||
|
|
||||||
const App = () => (
|
const App = () => (
|
||||||
<MuiThemeProvider muiTheme={getMuiTheme(theme)}>
|
<MuiThemeProvider theme={theme}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router>
|
<BrowserRouter>
|
||||||
<div>
|
<Routes>
|
||||||
<Route path="/list" component={layout(<SpaceList />)} />
|
<Route path="/" element={layout(<IndexContainer />)()} />
|
||||||
<Route path="/urls" component={layout(<UrlListView />)} />
|
<Route path="/list" element={layout(<SpaceList />)()} />
|
||||||
<Route exact path="/" component={layout(<IndexContainer />)} />
|
<Route path="/services" element={layout(<ServicesList />)()} />
|
||||||
</div>
|
<Route path="/urls" element={layout(<UrlListView />)()} />
|
||||||
</Router>
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</MuiThemeProvider>
|
</MuiThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,156 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Table, TableBody, TableRow, TableRowColumn }
|
import TableCell from '@material-ui/core/TableCell';
|
||||||
from 'material-ui/Table';
|
import PropTypes from 'prop-types';
|
||||||
import InfoIcon from 'material-ui/svg-icons/action/info-outline';
|
import InfoIcon from '@material-ui/icons/InfoOutlined';
|
||||||
import { actions as calendarActions, eventStruct } from '../redux/modules/calendar';
|
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 => ({
|
const mapStateToProps = state => ({
|
||||||
events: state.calendars.items,
|
events: state.calendars.items,
|
||||||
spacedata: state.spacedata,
|
spacedata: state.spacedata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...calendarActions,
|
||||||
|
...spaceDataActions,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
class EventList extends React.Component {
|
class EventList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
events: React.PropTypes.arrayOf(
|
events: PropTypes.arrayOf(
|
||||||
React.PropTypes.shape(eventStruct),
|
PropTypes.shape(eventStruct),
|
||||||
),
|
),
|
||||||
fetchCalendars: React.PropTypes.func,
|
fetchCalendars: PropTypes.func,
|
||||||
spacedata: spacedataStruct,
|
spacedata: spacedataStruct,
|
||||||
};
|
};
|
||||||
|
|
||||||
defaultProps = {
|
static defaultProps = {
|
||||||
events: [],
|
events: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -28,56 +158,60 @@ class EventList extends React.Component {
|
||||||
this.props.fetchCalendars();
|
this.props.fetchCalendars();
|
||||||
}
|
}
|
||||||
|
|
||||||
formatDate = date => (date.format('DD.MM.YYYY'));
|
|
||||||
formatTime = date => (date.format('HH:mm'));
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
|
||||||
<Table
|
const rows = this.props.events.filter(event =>
|
||||||
selectable
|
|
||||||
multiSelectable
|
|
||||||
>
|
|
||||||
<TableBody
|
|
||||||
showRowHover
|
|
||||||
stripedRows
|
|
||||||
displayRowCheckbox={false}
|
|
||||||
>
|
|
||||||
{this.props.events
|
|
||||||
.filter(event =>
|
|
||||||
(
|
(
|
||||||
this.props.spacedata.filter.indexOf(event.space) !== -1
|
this.props.spacedata.filter.indexOf(event.space) !== -1
|
||||||
|| this.props.spacedata.filter.length === 0
|
|| this.props.spacedata.filter.length === 0
|
||||||
)
|
)
|
||||||
)
|
).map(event => {
|
||||||
.map(event => (
|
return {
|
||||||
<TableRow
|
...event,
|
||||||
key={event.importId + event.start.toLocaleString() + event.description}
|
start: event.start.format('dd DD. MMM., HH:mm') ?? '',
|
||||||
>
|
summary: event.summary || event.description,
|
||||||
<TableRowColumn style={{ width: '80px', padding: '5px' }}>
|
link: event.url && <a href={event.url}>
|
||||||
{this.formatDate(event.start)}
|
<InfoIcon style={{ cursor: 'pointer', color: '#fff' }} />
|
||||||
</TableRowColumn>
|
</a>,
|
||||||
<TableRowColumn style={{ width: '55px', padding: '5px' }}>
|
}
|
||||||
{event.wholeDayEvent ? null : this.formatTime(event.start)}
|
});
|
||||||
</TableRowColumn>
|
|
||||||
<TableRowColumn>
|
const columns = [
|
||||||
{event.summary || event.description}
|
{
|
||||||
</TableRowColumn>
|
width: 160,
|
||||||
<TableRowColumn>
|
label: 'Date',
|
||||||
{event.space}
|
dataKey: 'start',
|
||||||
</TableRowColumn>
|
},
|
||||||
<TableRowColumn style={{ textAlign: 'right' }}>
|
{
|
||||||
{event.url && <a href={event.url}>
|
width: 200,
|
||||||
<InfoIcon style={{ cursor: 'pointer' }} />
|
flexGrow: 1,
|
||||||
</a>}
|
label: 'Summary',
|
||||||
</TableRowColumn>
|
dataKey: 'summary',
|
||||||
</TableRow>
|
},
|
||||||
))}
|
{
|
||||||
</TableBody>
|
width: 10,
|
||||||
</Table>
|
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}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, {
|
export default connect(mapStateToProps, mapDispatchToProps)(EventList);
|
||||||
...calendarActions,
|
|
||||||
})(EventList);
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { Map as LeafletMap, TileLayer } from 'react-leaflet';
|
import { Map as LeafletMap, TileLayer } from 'react-leaflet';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import Marker from './Marker';
|
import Marker from './Marker';
|
||||||
|
|
@ -15,8 +16,8 @@ const mapDispatchToProps = {
|
||||||
class Map extends React.Component {
|
class Map extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
spacedata: spacedataStruct.isRequired,
|
spacedata: spacedataStruct.isRequired,
|
||||||
fetchSpacedata: React.PropTypes.func.isRequired,
|
fetchSpacedata: PropTypes.func.isRequired,
|
||||||
toggleFilterSpacedata: React.PropTypes.func.isRequired,
|
toggleFilterSpacedata: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
|
|
@ -29,10 +30,10 @@ class Map extends React.Component {
|
||||||
<LeafletMap
|
<LeafletMap
|
||||||
center={centerGermany}
|
center={centerGermany}
|
||||||
zoom={5}
|
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
|
<TileLayer
|
||||||
url="https://spaceapi.ccc.de/map/tiles/{z}/{x}/{y}.png"
|
url="/map/tiles/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
{this.props.spacedata.items.map(
|
{this.props.spacedata.items.map(
|
||||||
spacedata => (
|
spacedata => (
|
||||||
|
|
@ -40,8 +41,7 @@ class Map extends React.Component {
|
||||||
spacedata={spacedata}
|
spacedata={spacedata}
|
||||||
key={spacedata.space}
|
key={spacedata.space}
|
||||||
highlight={
|
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}
|
toggleFilterSpacedata={this.props.toggleFilterSpacedata}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { CircleMarker, Popup } from 'react-leaflet';
|
import { CircleMarker, Popup } from 'react-leaflet';
|
||||||
import theme from '../style/theme';
|
import theme from '../style/theme';
|
||||||
import { spacedataElementStruct } from '../redux/modules/spacedata';
|
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 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 = {
|
const style = {
|
||||||
container: {
|
container: {
|
||||||
|
|
@ -23,7 +37,10 @@ const Marker = (props) => {
|
||||||
radius={5}
|
radius={5}
|
||||||
center={[props.spacedata.location.lat, props.spacedata.location.lon]}
|
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 style={style.container}>
|
||||||
<div>
|
<div>
|
||||||
{props.spacedata.space}
|
{props.spacedata.space}
|
||||||
|
|
@ -40,7 +57,8 @@ const Marker = (props) => {
|
||||||
|
|
||||||
Marker.propTypes = {
|
Marker.propTypes = {
|
||||||
spacedata: spacedataElementStruct.isRequired,
|
spacedata: spacedataElementStruct.isRequired,
|
||||||
highlight: React.PropTypes.bool.isRequired,
|
highlight: PropTypes.bool.isRequired,
|
||||||
|
toggleFilterSpacedata: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Marker;
|
export default Marker;
|
||||||
|
|
|
||||||
100
frontend/src/components/ServiceList.jsx
Normal file
100
frontend/src/components/ServiceList.jsx
Normal 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);
|
||||||
|
|
@ -1,14 +1,72 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import request from 'superagent';
|
import request from 'superagent';
|
||||||
import TextField from 'material-ui/TextField';
|
import TextField from '@material-ui/core/TextField';
|
||||||
import FloatingActionButton from 'material-ui/FloatingActionButton';
|
import FloatingActionButton from '@material-ui/core/Fab';
|
||||||
import ContentAdd from 'material-ui/svg-icons/content/add';
|
import SnackbarContent from '@material-ui/core/SnackbarContent';
|
||||||
import Snackbar from 'material-ui/Snackbar';
|
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';
|
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 {
|
class SpaceApiInput extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
style: React.PropTypes.shape({}),
|
style: PropTypes.shape({}),
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
|
@ -58,7 +116,7 @@ class SpaceApiInput extends React.Component {
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.end((err) => {
|
.end((err) => {
|
||||||
if (!err) {
|
if (!err) {
|
||||||
this.spaceApiInput.input.value = '';
|
this.spaceApiInput.value = '';
|
||||||
this.setState({ open: true });
|
this.setState({ open: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -77,27 +135,28 @@ class SpaceApiInput extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
<div style={style.formContainer}>
|
<div style={style.formContainer}>
|
||||||
<TextField
|
<TextField
|
||||||
hintText={'https://example.com/yourspaceapi.json'}
|
placeholder={'https://example.com/yourspaceapi.json'}
|
||||||
name={'spaceapi-input'}
|
name={'spaceapi-input'}
|
||||||
onChange={this.handleInputChange}
|
onChange={this.handleInputChange}
|
||||||
ref={ref => (this.spaceApiInput = ref)}
|
inputRef={ref => (this.spaceApiInput = ref)}
|
||||||
style={{ width: '100%', maxWidth: '340px' }}
|
style={{ width: '100%', maxWidth: '340px' }}
|
||||||
/>
|
/>
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
style={{ marginLeft: '20px' }}
|
style={{ marginLeft: '20px' }}
|
||||||
mini
|
onClick={this.handleButtonClick}
|
||||||
onTouchTap={this.handleButtonClick}
|
|
||||||
>
|
>
|
||||||
<ContentAdd />
|
<ContentAdd />
|
||||||
</FloatingActionButton>
|
</FloatingActionButton>
|
||||||
</div>
|
</div>
|
||||||
<Snackbar
|
<Snackbar
|
||||||
|
variant={'info'}
|
||||||
open={this.state.open}
|
open={this.state.open}
|
||||||
message={'Die URL wurde hinzugefuegt und befindet sich nun im review.'}
|
|
||||||
autoHideDuration={4000}
|
autoHideDuration={4000}
|
||||||
style={{ minWidth: '490px' }}
|
style={{ minWidth: '490px' }}
|
||||||
onRequestClose={() => this.setState({ open: false })}
|
onClose={() => this.setState({ open: false })}
|
||||||
/>
|
>
|
||||||
|
<MySnackbarContent />
|
||||||
|
</Snackbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,45 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import Table from '@material-ui/core/Table';
|
||||||
Table,
|
import TableBody from '@material-ui/core/TableBody';
|
||||||
TableBody,
|
import TableRow from '@material-ui/core/TableRow';
|
||||||
TableRow,
|
import TableCell from '@material-ui/core/TableCell';
|
||||||
TableRowColumn,
|
import PropTypes from 'prop-types';
|
||||||
} from 'material-ui/Table';
|
import classNames from 'classnames';
|
||||||
import { spacedataStruct } from '../redux/modules/spacedata';
|
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 {
|
export class SpaceList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
fetchSpacedata: React.PropTypes.func.isRequired,
|
fetchSpacedata: PropTypes.func.isRequired,
|
||||||
spacedata: spacedataStruct,
|
spacedata: spacedataStruct,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -28,36 +58,33 @@ export class SpaceList extends React.Component {
|
||||||
(a, b) => a.space.toUpperCase().localeCompare(b.space.toUpperCase())
|
(a, b) => a.space.toUpperCase().localeCompare(b.space.toUpperCase())
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Table
|
<div style={{ paddingTop:'60px' }}>
|
||||||
selectable
|
<Table>
|
||||||
multiSelectable
|
<TableBody>
|
||||||
>
|
|
||||||
<TableBody
|
|
||||||
showRowHover
|
|
||||||
stripedRows
|
|
||||||
displayRowCheckbox={false}
|
|
||||||
>
|
|
||||||
{items
|
{items
|
||||||
.map(space => (
|
.map((space, index) => (
|
||||||
<TableRow key={space.space}>
|
<TableRow key={space.space} className={classNames({
|
||||||
<TableRowColumn>
|
[this.props.classes.tableRowEven]: index % 2
|
||||||
|
})} >
|
||||||
|
<TableCell style={{ color: '#fff', border: 0 }}>
|
||||||
{space.space}
|
{space.space}
|
||||||
</TableRowColumn>
|
</TableCell>
|
||||||
<TableRowColumn>
|
<TableCell style={{ border: 0 }}>
|
||||||
<a
|
<a
|
||||||
href={space.url}
|
href={space.url}
|
||||||
style={{ textDecoration: 'none', color: 'white' }}
|
style={{ textDecoration: 'none', color: 'white' }}
|
||||||
>
|
>
|
||||||
{space.url}
|
{space.url}
|
||||||
</a>
|
</a>
|
||||||
</TableRowColumn>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SpaceList;
|
export default withStyles(styles)(SpaceList);
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,99 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import Toolbar from '@material-ui/core/Toolbar';
|
||||||
Toolbar as MuiToolbar,
|
import AppBar from '@material-ui/core/AppBar';
|
||||||
ToolbarGroup,
|
import Typography from '@material-ui/core/Typography';
|
||||||
ToolbarTitle,
|
import IconButton from '@material-ui/core/IconButton';
|
||||||
} from 'material-ui/Toolbar';
|
import ClickAwayListener from '@material-ui/core/ClickAwayListener';
|
||||||
import IconMenu from 'material-ui/IconMenu';
|
import Grow from '@material-ui/core/Grow';
|
||||||
import IconButton from 'material-ui/IconButton';
|
import Paper from '@material-ui/core/Paper';
|
||||||
import NavigationExpandMoreIcon from 'material-ui/svg-icons/navigation/expand-more';
|
import Popper from '@material-ui/core/Popper';
|
||||||
import MenuItem from 'material-ui/MenuItem';
|
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';
|
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;
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import Table from '@material-ui/core/Table';
|
||||||
Table,
|
import TableBody from '@material-ui/core/TableBody';
|
||||||
TableBody,
|
import TableRow from '@material-ui/core/TableRow';
|
||||||
TableRow,
|
import TableCell from '@material-ui/core/TableCell';
|
||||||
TableRowColumn,
|
import TextField from '@material-ui/core/TextField';
|
||||||
} from 'material-ui/Table';
|
import PropTypes from 'prop-types';
|
||||||
import TextField from 'material-ui/TextField';
|
import Button from '@material-ui/core/Button';
|
||||||
import FlatButton from 'material-ui/FlatButton';
|
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { spaceUrlStruct } from '../redux/modules/spaceurl';
|
import { spaceUrlStruct } from '../redux/modules/spaceurl';
|
||||||
|
|
||||||
export class UrlList extends React.Component {
|
export class UrlList extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
fetchSpaceUrl: React.PropTypes.func.isRequired,
|
fetchSpaceUrl: PropTypes.func.isRequired,
|
||||||
validateSpaceUrl: React.PropTypes.func.isRequired,
|
validateSpaceUrl: PropTypes.func.isRequired,
|
||||||
|
deleteSpaceUrl: PropTypes.func.isRequired,
|
||||||
spaceurls: spaceUrlStruct,
|
spaceurls: spaceUrlStruct,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -38,42 +38,44 @@ export class UrlList extends React.Component {
|
||||||
url: spaceUrl.url,
|
url: spaceUrl.url,
|
||||||
validated: true,
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Table
|
<Table>
|
||||||
selectable
|
<TableBody>
|
||||||
multiSelectable
|
|
||||||
>
|
|
||||||
<TableBody
|
|
||||||
showRowHover
|
|
||||||
stripedRows
|
|
||||||
displayRowCheckbox={false}
|
|
||||||
>
|
|
||||||
{this.props.spaceurls.items
|
{this.props.spaceurls.items
|
||||||
.map(spaceurl => (
|
.map(spaceurl => (
|
||||||
<TableRow key={spaceurl.url}>
|
<TableRow key={spaceurl.url}>
|
||||||
<TableRowColumn>
|
<TableCell>
|
||||||
<a
|
<a
|
||||||
href={spaceurl.url}
|
href={spaceurl.url}
|
||||||
style={{ color: 'white', textDecoration: 'none' }}
|
style={{ color: 'white', textDecoration: 'none' }}
|
||||||
>
|
>
|
||||||
{spaceurl.url}
|
{spaceurl.url}
|
||||||
</a>
|
</a>
|
||||||
</TableRowColumn>
|
</TableCell>
|
||||||
<TableRowColumn>
|
<TableCell>
|
||||||
{this.getFormatedDateTime(spaceurl.lastUpdated)}
|
{this.getFormatedDateTime(spaceurl.lastUpdated)}
|
||||||
</TableRowColumn>
|
</TableCell>
|
||||||
<TableRowColumn>
|
<TableCell>
|
||||||
{!spaceurl.validated ? <FlatButton
|
{!spaceurl.validated ? <Button
|
||||||
label={'validated'}
|
onClick={() => this.validateSpaceUrl(spaceurl)}
|
||||||
onTouchTap={() => this.validateSpaceUrl(spaceurl)}
|
>validated</Button> : null}
|
||||||
primary
|
</TableCell>
|
||||||
/> : null}
|
<TableCell>
|
||||||
</TableRowColumn>
|
<Button
|
||||||
|
onClick={() => this.deleteSpaceUrl(spaceurl)}
|
||||||
|
>
|
||||||
|
delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
@ -81,7 +83,7 @@ export class UrlList extends React.Component {
|
||||||
</Table>
|
</Table>
|
||||||
<TextField
|
<TextField
|
||||||
name={'secret-input'}
|
name={'secret-input'}
|
||||||
ref={ref => (this.secretInput = ref)}
|
onChange={(event)=> this.setState({ secret: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
13
frontend/src/containers/ServiceList.jsx
Normal file
13
frontend/src/containers/ServiceList.jsx
Normal 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);
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { PropTypes } from 'react';
|
import PropTypes from 'prop-types';
|
||||||
import request from 'superagent';
|
import request from 'superagent';
|
||||||
import flatten from 'lodash/flatten';
|
import flatten from 'lodash/flatten';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import RRule from 'rrule';
|
import { RRule } from 'rrule';
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
import { createAction, handleActions } from 'redux-actions';
|
||||||
import config from '../../api/config';
|
import config from '../../api/config';
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ export default handleActions({
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
catch (ex) {
|
catch (ex) {
|
||||||
console.log(ex);
|
console.log(ex, event);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
44
frontend/src/redux/modules/services.js
Normal file
44
frontend/src/redux/modules/services.js
Normal 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: [] });
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PropTypes } from 'react';
|
import PropTypes from 'prop-types';
|
||||||
import request from 'superagent';
|
import request from 'superagent';
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
import { createAction, handleActions } from 'redux-actions';
|
||||||
import config from '../../api/config';
|
import config from '../../api/config';
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { PropTypes } from 'react';
|
import PropTypes from 'prop-types';
|
||||||
import request from 'superagent';
|
import request from 'superagent';
|
||||||
import { createAction, handleActions } from 'redux-actions';
|
import { createAction, handleActions } from 'redux-actions';
|
||||||
import config from '../../api/config';
|
import config from '../../api/config';
|
||||||
|
|
||||||
export const itemStruct = PropTypes.shape({
|
export const itemStruct = PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
validated: PropTypes.bool.isRequired,
|
validated: PropTypes.bool.isRequired,
|
||||||
lastUpdated: PropTypes.number.isRequired,
|
lastUpdated: PropTypes.number.isRequired,
|
||||||
|
|
@ -14,9 +15,11 @@ export const spaceUrlStruct = PropTypes.shape({
|
||||||
|
|
||||||
const SPACEURL_FETCHED = 'SPACEURL_FETCHED';
|
const SPACEURL_FETCHED = 'SPACEURL_FETCHED';
|
||||||
const SPACEURL_VALIDATE = 'SPACEURL_VALIDATE';
|
const SPACEURL_VALIDATE = 'SPACEURL_VALIDATE';
|
||||||
|
const SPACEURL_DELETE = 'SPACEURL_DELETE';
|
||||||
|
|
||||||
export const fetched = createAction(SPACEURL_FETCHED, result => result);
|
export const fetched = createAction(SPACEURL_FETCHED, result => result);
|
||||||
export const validate = createAction(SPACEURL_VALIDATE, result => result);
|
export const validate = createAction(SPACEURL_VALIDATE, result => result);
|
||||||
|
export const deleteSpace = createAction(SPACEURL_DELETE, result => result);
|
||||||
|
|
||||||
export const fetchSpaceUrl = () => (dispatch) => {
|
export const fetchSpaceUrl = () => (dispatch) => {
|
||||||
request
|
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 = {
|
export const actions = {
|
||||||
fetchSpaceUrl,
|
fetchSpaceUrl,
|
||||||
validateSpaceUrl,
|
validateSpaceUrl,
|
||||||
|
deleteSpaceUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default handleActions({
|
export default handleActions({
|
||||||
|
|
@ -67,4 +84,5 @@ export default handleActions({
|
||||||
|
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
|
[SPACEURL_DELETE]: (state, { payload }) => ({ items: state.items.filter(ele => ele.id === payload.id) }),
|
||||||
}, { items: [] });
|
}, { items: [] });
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,11 @@ import { combineReducers } from 'redux';
|
||||||
import spacedataReducer from './modules/spacedata';
|
import spacedataReducer from './modules/spacedata';
|
||||||
import calendarsReducer from './modules/calendar';
|
import calendarsReducer from './modules/calendar';
|
||||||
import spaceUrlsReducer from './modules/spaceurl';
|
import spaceUrlsReducer from './modules/spaceurl';
|
||||||
|
import servicesReducer from './modules/services';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
spacedata: spacedataReducer,
|
spacedata: spacedataReducer,
|
||||||
calendars: calendarsReducer,
|
calendars: calendarsReducer,
|
||||||
spaceurls: spaceUrlsReducer,
|
spaceurls: spaceUrlsReducer,
|
||||||
|
services: servicesReducer,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,29 @@
|
||||||
body {
|
body {
|
||||||
background-color: #333333;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
import { fade } from 'material-ui/utils/colorManipulator';
|
import grey from '@material-ui/core/colors/grey';
|
||||||
import { spacing, colors } from 'material-ui/styles';
|
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 {
|
export default createTheme({
|
||||||
spacing,
|
typography: {
|
||||||
fontFamily: 'Roboto, sans-serif',
|
fontFamily: 'Roboto, sans-serif',
|
||||||
palette: {
|
useNextVariants: true,
|
||||||
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),
|
|
||||||
},
|
},
|
||||||
};
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,16 @@ import React from 'react';
|
||||||
import Map from '../components/Map';
|
import Map from '../components/Map';
|
||||||
import EventList from '../components/EventList';
|
import EventList from '../components/EventList';
|
||||||
|
|
||||||
const style = {
|
|
||||||
container: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const IndexContainer = () => (
|
const IndexContainer = () => (
|
||||||
<div style={style.container}>
|
<div id="indexContainer">
|
||||||
|
<div style={{ flex: 2 }}>
|
||||||
<Map />
|
<Map />
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: 2 }}>
|
||||||
<EventList />
|
<EventList />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default IndexContainer;
|
export default IndexContainer;
|
||||||
|
|
|
||||||
10
frontend/src/views/ServicesList.jsx
Normal file
10
frontend/src/views/ServicesList.jsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ServiceList from '../containers/ServiceList';
|
||||||
|
|
||||||
|
const ServiceListView = () => (
|
||||||
|
<div>
|
||||||
|
<ServiceList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ServiceListView;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue