Browse Source

Add caching + complete regform

Joachim M. Giæver 2 years ago
parent
commit
492ffb6af9

+ 0 - 36
.env

@@ -1,36 +0,0 @@
-# In all environments, the following files are loaded if they exist,
-# the latter taking precedence over the former:
-#
-#  * .env                contains default values for the environment variables needed by the app
-#  * .env.local          uncommitted file with local overrides
-#  * .env.$APP_ENV       committed environment-specific defaults
-#  * .env.$APP_ENV.local uncommitted environment-specific overrides
-#
-# Real environment variables win over .env files.
-#
-# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
-#
-# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
-# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
-
-###> symfony/framework-bundle ###
-APP_ENV=dev
-APP_SECRET=35cc91abb62c2f3d175f44f750c08ddb
-#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
-#TRUSTED_HOSTS='^(localhost|example\.com)$'
-###< symfony/framework-bundle ###
-API_TOKEN=4ae9a18cbe797a4bbddd6d2e35c8e9b39f78589dd16a55cd861c9c48f673bdfd
-API_URL=api.webflow.com
-API_VERSION=1.0.0
-
-###> nelmio/cors-bundle ###
-CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$
-###< nelmio/cors-bundle ###
-
-###> doctrine/doctrine-bundle ###
-# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
-# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
-# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
-# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
-DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7
-###< doctrine/doctrine-bundle ###

+ 305 - 90
assets/js/regform.js

@@ -1,7 +1,15 @@
 import React, {useState} from "react";
 import axios from "axios";
+import DateFnsUtils from '@date-io/date-fns';
+import {nb} from 'date-fns/locale'
+import {DateTimePicker, MuiPickersUtilsProvider} from '@material-ui/pickers';
+import {makeStyles} from '@material-ui/core/styles';
+
+import ReCAPTCHA from "react-google-recaptcha";
+
 import {
     Button,
+    ButtonGroup,
     Container,
     Checkbox,
     CssBaseline,
@@ -9,20 +17,26 @@ import {
     DialogTitle,
     DialogContent,
     DialogActions,
-    Grid,
     FormControl,
     FormControlLabel,
     FormGroup,
+    Grid,
+    IconButton,
     Paper,
     TextField,
     Typography,
 } from '@material-ui/core';
-import {makeStyles} from '@material-ui/core/styles';
+
+import {
+    Add,
+    Remove,
+} from '@material-ui/icons'
 
 const useStyles = makeStyles((theme) => ({
     fieldset: {
         //border: 'none',
-        //borderBottom: '1px solid',
+        marginBottom: theme.spacing(3),
+        paddingBottom: theme.spacing(3),
     },
     form: {
         width: '100%',
@@ -37,107 +51,303 @@ const useStyles = makeStyles((theme) => ({
     }
 }));
 
+const SubmitButton = (props) => {
+
+    const [isApproved, setIsApproved] = useState(false);
+    const recaptchaRef = React.createRef();
+
+    const onChange = (value) => {
+        console.log("Captcha value:", value);
+        setIsApproved(true);
+    }
+
+    return (
+        <React.Fragment>
+            <center>
+                <ReCAPTCHA
+                    onChange={onChange}
+                    onExpired={() => isApproved ? setIsApproved(false) : false}
+                    ref={recaptchaRef}
+                    sitekey={"6LfRxgAVAAAAALlMOyD5h_a6gvWulxGqCC5gKJvd"}
+                    size={"invisible"}
+                />
+            </center>
+            <Button 
+                color={"primary"}
+                variant={"contained"}
+                fullWidth
+                type={"submit"}
+                onClick={(e) => {
+                    recaptchaRef.current.execute();
+                }}
+            >Send skjema</Button>
+        </React.Fragment>
+    );
+
+}
+
 const RegForm = (props) => {
     const classes = useStyles();
+
+    const [inputs, updateInputs] = useState({
+        dates: [{date: new Date(), duration: 0}],
+    });
+
+    const inputHandler = (event) => {
+        const target = event.target
+        updateInputs((prevState) => ({...prevState, [target.id]: target.value}));
+    };
+
+
+    const inputCategoryHandler = (type, event, category) => {
+        const keys = Object.keys(inputs).filter(idx => idx == type);
+        let arr = keys.length == 0 ? [] : inputs[keys[0]];
+
+        if (event.target.checked)
+            arr.push(category);
+        else 
+            arr = arr.filter(item => item.id != category.id);
+
+        updateInputs((prevState) => ({...prevState, [type]: arr}));
+    }
+
+    const dateAddHandler = (idx) => {
+        inputs.dates.splice(idx, 0, {...inputs.dates[idx]});
+        updateInputs((prevState) => ({...prevState, dates: inputs.dates}));
+    }
+
+    const dateRemoveHandler = (idx) => {
+        if (inputs.dates.length == 1)
+            return;
+
+        if (!inputs.dates[idx])
+            return;
+
+        updateInputs(prevState => ({...prevState, dates: [...inputs.dates.slice(0, idx), ...inputs.dates.slice(idx+1)]}));
+    }
+
+    const dateSaveHandler = (idx, e) => {
+        if (!inputs.dates[idx])
+            return;
+        inputs.dates[idx].date = e;
+        updateInputs((prevState) => ({...prevState, dates: inputs.dates}));
+    }
+
+    const dateDurationSaveHandler = (idx, e) => {
+        if (!inputs.dates[idx])
+            return;
+        
+        const target = e.target;
+        inputs.dates[idx].duration = parseInt(target.value, 10);
+
+        updateInputs((prevState) => ({...prevState, dates: inputs.dates}));
+    }
+
+    const submitHandler = (event) => {
+        event.PreventDefault();
+        console.log(event);
+    }
+
     return (
         <Container component="main" maxWidth="md">
             <CssBaseline />
             <Paper className={classes.paper}>
-                <Typography component={"h1"} variant={"h5"}>
-                    Registrer ditt event
-                </Typography>
                 <form className={classes.form}>
                     <FormControl component={"fieldset"} className={classes.fieldset}>
-                    <Grid container spacing={3}>
-                        <Grid item xs={12}>
-                            <TextField 
-                                id={"organizer"}
-                                label={"Arrangør"}
-                                name={"organizer"}
-                                variant={"outlined"}
-                                margin={"normal"}
-                                required
-                                fullWidth
-                                autoFocus
-                            />
-                        </Grid>
-                        <Grid item xs={12} sm={4}>
-                            <TextField 
-                                id={"contact_name"}
-                                label={"Kontakt person"}
-                                name={"contact_name"}
-                                variant={"outlined"}
-                                margin={"normal"}
-                                required
-                                fullWidth
-                            />
-                        </Grid>
-                        <Grid item xs={12} sm={4}>
-                            <TextField
-                                id={"contact_email"}
-                                label={"Epostadresse"}
-                                name={"contact_email"}
-                                variant={"outlined"}
-                                margin={"normal"}
-                                type={"email"}
-                                required
-                                fullWidth
-                            />
-                        </Grid>
-                        <Grid item xs={12} sm={4}>
-                            <TextField
-                                id={"contact_phone"}
-                                label={"Telefonnummer"}
-                                name={"contact_phone"}
-                                variant={"outlined"}
-                                margin={"normal"}
-                                type={"tel"}
-                                required
-                                fullWidth
-                            />
+                        <Typography component={"legend"} variant={"h5"}>
+                            Om arrangøren
+                        </Typography>
+                        <Grid container spacing={3}>
+                            <Grid item xs={12} sm={7} md={8}>
+                                <TextField 
+                                    id={"organizer"}
+                                    label={"Navn på arrangør"}
+                                    onChange={inputHandler}
+                                    required
+                                    fullWidth
+                                    autoFocus
+                                />
+                            </Grid>
+                            <Grid item xs={12} sm={5} md={4}>
+                                <TextField 
+                                    id={"organization_number"}
+                                    label={"Org.nummer"}
+                                    onChange={inputHandler}
+                                    required
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={12} sm={6}>
+                                <TextField 
+                                    id={"contact_name"}
+                                    label={"Kontakt person"}
+                                    onChange={inputHandler}
+                                    required
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={12} sm={6}>
+                                <TextField
+                                    id={"contact_email"}
+                                    label={"Epostadresse"}
+                                    onChange={inputHandler}
+                                    type={"email"}
+                                    required
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={12} sm={6}>
+                                <TextField
+                                    id={"contact_phone"}
+                                    label={"Telefonnummer"}
+                                    onChange={inputHandler}
+                                    type={"tel"}
+                                    required
+                                    fullWidth
+                                />
+                            </Grid>
+                            <Grid item xs={12} sm={6}>
+                                <TextField
+                                    id={"contact_homepage"}
+                                    label={"Hjemmesideadresse"}
+                                    placeholder={"https:// ...."}
+                                    required
+                                    fullWidth
+                                />
+                            </Grid>
                         </Grid>
-                    </Grid>
                     </FormControl>
                     <FormControl component={"fieldset"} className={classes.fieldset}>
-                        <legend>Arrangement</legend>
+                        <Typography component={"legend"} variant={"h5"}>
+                            Om arrangementet
+                        </Typography>
                         <Grid container spacing={3}>
-                            <Grid item xs={12} sm={8}>
+                            <Grid item xs={12}>
                                 <TextField
                                     id={"event_name"}
                                     label={"Navn på arrangement"}
-                                    name={"event_name"}
-                                    variant={"outlined"}
-                                    margin={"normal"}
+                                    onChange={inputHandler}
                                     required
                                     fullWidth
                                 />
                             </Grid>
+                            {inputs.dates.map((date, idx) => (
+                                <React.Fragment key={idx}>
+                                    <Grid item xs={6}>
+                                        <MuiPickersUtilsProvider utils={DateFnsUtils} locale={nb}>
+                                            <DateTimePicker 
+                                                fullWidth 
+                                                format={"do MMMM y HH:mm (h:mm a)"} 
+                                                ampm={false} 
+                                                value={date.date} 
+                                                onChange={e => dateSaveHandler(idx, e)} 
+                                                margin={"normal"} 
+                                            />
+                                        </MuiPickersUtilsProvider>
+                                    </Grid>
+                                    <Grid item xs={4}>
+                                        <TextField
+                                            id={"event_duration"}
+                                            label={"Varighet (timer)"}
+                                            onChange={e => dateDurationSaveHandler(idx, e)}
+                                            type={"number"}
+                                            placeholder={"antall timer"}
+                                            value={date.duration}
+                                            fullWidth
+                                        />
+                                    </Grid>
+                                    <Grid item xs={1}>
+                                        <IconButton onClick={() => dateAddHandler(idx)}><Add /></IconButton>
+                                    </Grid>
+                                    <Grid item xs={1}>
+                                        <IconButton  onClick={() => dateRemoveHandler(idx)}><Remove /></IconButton>
+                                    </Grid>
+                                </React.Fragment>
+                            ))}
+                            <Grid item xs={12} sm={8}>
+                                <TextField
+                                    id={'event_location'}
+                                    label={"Sted"}
+                                    onChange={inputHandler}
+                                    placeholder={"Hvor skjer arrangementet?"}
+                                    fullWidth
+                                    required
+                                />
+                            </Grid>
                             <Grid item xs={12} sm={4}>
                                 <TextField
                                     id={"event_capacity"}
                                     label={"Kapasitet"}
-                                    name={"event_capacity"}
-                                    variant={"outlined"}
-                                    margin={"normal"}
+                                    placeholder={"Antall personer"}
+                                    onChange={inputHandler}
                                     type={"number"}
                                     required
                                     fullWidth
                                 />
                             </Grid>
+                            <Grid item xs={12}>
+                                <TextField
+                                    id={"event_description"}
+                                    label={"Beskrivelse av arrangement"}
+                                    onChange={inputHandler}
+                                    placeholder={"Forklar detaljert om ditt arrangement - dette skal besøkende lese!"}
+                                    multiline
+                                    fullWidth
+                                    required
+                                />
+                            </Grid>
+                            <Grid item xs={12} sm={6}>
+                                <TextField
+                                    id="event_photo"
+                                    label={"Photo-link til arrangement"}
+                                    onChange={inputHandler}
+                                    placeholder={"Kan for eks. være en bilde-fil eller mappe med bilder i Dropbox."}
+                                    fullWidth
+                                    required
+                                />
+                            </Grid>
+                            <Grid item xs={12} sm={6}>
+                                <TextField
+                                    id="event_booking"
+                                    label={"Link til booking"}
+                                    onChange={inputHandler}
+                                    placeholder={"https:// ..."}
+                                    fullWidth
+                                    required
+                                />
+                            </Grid>
                             <Grid item xs={12} sm={6}>
-                                <Categories cid={"5ed0f77a42f75848a95ebf03"} />
+                                <CategorySelection 
+                                    cid={"5ed0f77a42f75848a95ebf03"} 
+                                    category="Kategorier"
+                                    name="categories"
+                                    selected={inputs.categories || []}
+                                    inputCategoryHandler={inputCategoryHandler}
+                                />
                             </Grid>
                             <Grid item xs={12} sm={6}>
+                                <CategorySelection 
+                                    cid={"5ed10cffd2b21cfb3b269088"} 
+                                    category="Målgrupper"
+                                    name="audiences"
+                                    selected={inputs.audiences || []}
+                                    inputCategoryHandler={inputCategoryHandler}
+                                />
                             </Grid>
                         </Grid>
                     </FormControl>
+                    <Grid container justify={"center"}>
+                        <Grid item xs={8}>
+                            <SubmitButton text={"Send skjema"}/>
+                        </Grid>
+                    </Grid>
                 </form>
             </Paper>
         </Container>
     )
 }
 
-const Categories = (props) => {
+const CategorySelection = (props) => {
     const [error, setError] = useState(null);
     const [isLoading, setIsLoading] = useState(true);
     const [availableItems, setAvailableItems] = useState([]);
@@ -146,41 +356,46 @@ const Categories = (props) => {
         axios.get("https://magy.giaever.online/tff/api/webflow_items?cid=" + props.cid)
             .then((response) => {
                 const data = response.data['hydra:member'];
-                setAvailableItems(data);
+                setAvailableItems(response.data['hydra:member']);
             })
             .catch((error) => {
-                console.log(error);
                 setError(error);
             })
             .then(() => {
-                console.log("DONE");
                 setIsLoading(false);
             });
     }, []);
 
     return (
-        <DialogCancelOk
-            buttonDesc={isLoading ? "Laster kategorier" : "Velg kategori"}
-            buttonDisabled={isLoading}
-            dialogTitle={"Velg kategori(er)"}
-        >
-            {isLoading ? 
-                <p>
-                    Laster...
-                </p>
-                :
-                <FormGroup>
-                    {availableItems.map(item => <FormControlLabel 
-                        key={item.id}
-                        control={
-                            <Checkbox checked={false} value={item.id} />
-                        }
-                        label={item.name}
-                    />
-                    )}
-                </FormGroup>
-            }
-        </DialogCancelOk>
+        <React.Fragment>
+            <DialogCancelOk
+                buttonDesc={isLoading ? `Laster ${props.category}` : `Velg ${props.category}`}
+                buttonDisabled={isLoading}
+                dialogTitle={`Velg ${props.category}`}
+            >
+                {isLoading ? 
+                    <p>
+                        Laster...
+                    </p>
+                    :
+                    <FormGroup>
+                        {availableItems.map(item => <FormControlLabel 
+                            key={item.id}
+                            control={
+                                <Checkbox 
+                                    checked={props.selected.filter(sitem => sitem.id == item.id).length > 0} 
+                                    value={item.id}
+                                    onChange={() => props.inputCategoryHandler(props.name, event, item)}
+                                />
+                            }
+                            label={item.name}
+                        />
+                        )}
+                    </FormGroup>
+                }
+            </DialogCancelOk>
+            <ul>{props.selected.map(item => <li key={item.id}>{item.name}</li>)}</ul>
+        </React.Fragment>
     )
 }
 
@@ -201,7 +416,7 @@ const DialogCancelOk = (props) => {
                 fullWidth
                 disabled={props.buttonDisabled}
                 color={"secondary"}
-                variant={"outlined"}
+                variant={props.buttonDisabled ? "outlined" : "contained"}
                 onClick={handleClickOpen}>
                     {props.buttonDesc}
             </Button>

+ 1 - 1
config/services.yaml

@@ -24,7 +24,7 @@ services:
         tags: ['controller.service_arguments']
 
     App\Http\WebflowApiClient:
-      arguments: ['%env(API_TOKEN)%', '%env(API_URL)%', '%env(API_VERSION)%']
+      arguments: ['@cache.app', '%env(API_TOKEN)%', '%env(API_URL)%', '%env(API_VERSION)%']
 
     # add more service definitions when explicit configuration is needed
     # please note that last definitions always *replace* previous ones

+ 7 - 1
package.json

@@ -15,10 +15,16 @@
         "build": "encore production --progress"
     },
     "dependencies": {
+        "@date-io/date-fns": "1.x",
         "@material-ui/core": "^4.10.1",
+        "@material-ui/icons": "^4.9.1",
+        "@material-ui/pickers": "^3.2.10",
         "axios": "^0.19.2",
+        "date-fns": "^2.14.0",
         "prop-types": "^15.7.2",
         "react": "^16.13.1",
-        "react-dom": "^16.13.1"
+        "react-dom": "^16.13.1",
+        "react-google-recaptcha": "^2.0.1",
+        "react-recaptcha-google": "^1.1.1"
     }
 }

+ 5 - 1
src/Controller/IndexController.php

@@ -18,7 +18,11 @@ class IndexController extends AbstractController
     public function index(WebflowSites $wfsites, WebflowApiClient $wfapi)
     {
         dump($wfsites->getSiteByShortName("tromsup"));
-        $site = WebflowSite::byId($wfapi, '5ebabfe546c816388d66c03a');
+        $site = WebflowSite::byId($wfapi, '5ebabfe546c816388d66c03a')->load();
+        $site = WebflowSite::byId($wfapi, '5ebabfe546c816388d66c03a')->load();
+        $site = WebflowSite::byId($wfapi, '5ebabfe546c816388d66c03a')->load();
+        $site = WebflowSite::byId($wfapi, '5ebabfe546c816388d66c03a')->load();
+        $site = WebflowSite::byId($wfapi, '5ebabfe546c816388d66c03a')->load();
 
         dump($site);
 

+ 6 - 1
src/Entity/WebflowItem.php

@@ -10,7 +10,7 @@ use App\Filter\SearchFilter;
 
 /**
  * @ApiResource(
- *  collectionOperations={"get"},
+ *  collectionOperations={"get", "post"},
  *  itemOperations={"get"}
  * )
  * @ApiFilter(SearchFilter::class, properties={"cid": "exact"})
@@ -24,6 +24,7 @@ class WebflowItem extends AbstractEntity {
 
     private $cid;
 
+    private $reCaptcha = null;
 
     protected $archived;
 
@@ -54,4 +55,8 @@ class WebflowItem extends AbstractEntity {
         return $this->draft;
     }
 
+    public function setReCaptcha(string $value) {
+        $this->reCaptcha = $value;
+    }
+
 }

+ 1 - 1
src/Http/WebflowApi/WebflowApiCollections.php

@@ -5,7 +5,7 @@ namespace App\Http\WebflowApi;
 class WebflowApiCollections extends AbstractWebflowApiCollection implements \IteratorAggregate{
 
     protected function getLoadScope(): string {
-        return sprintf('/sites/%s/collections', $this->parent->data['_id']);
+        return sprintf('sites/%s/collections', $this->parent->data['_id']);
     }
 
     public function createEntry($key, $value): ?AbstractWebflowApiField {

+ 44 - 8
src/Http/WebflowApiClient.php

@@ -2,17 +2,23 @@
 
 namespace App\Http;
 
+use Symfony\Component\Cache\Adapter\TraceableAdapter;
+use Symfony\Component\HttpClient\CachingHttpClient;
 use Symfony\Component\HttpClient\HttpClient;
+use Symfony\Component\HttpClient\Response\MockResponse;
+use Symfony\Component\HttpKernel\HttpCache\Store;
+use Symfony\Contracts\Cache\ItemInterface;
 use Symfony\Contracts\HttpClient\HttpClientInterface;
 use Symfony\Contracts\HttpClient\ResponseInterface;
 
-class WebflowApiClient implements WebflowApiClientInterface {
+class WebflowApiClient {
 
     private $client;
+    private $cache;
     private $url;
     private $options = [];
 
-    public function __construct(string $token, string $api_url, string $version = '1.0.0') {
+    public function __construct(TraceableAdapter $cache, string $token, string $api_url, string $version = '1.0.0', string $cache_dir) {
 
         $this->url = $api_url;
 
@@ -27,6 +33,9 @@ class WebflowApiClient implements WebflowApiClientInterface {
             sprintf('https://%s', $api_url), 
             $this->options
         );
+        
+        $this->cache = $cache;
+        dump($this->cache);
     }
 
 
@@ -34,14 +43,41 @@ class WebflowApiClient implements WebflowApiClientInterface {
         return sprintf('https://%s/%s', $this->url, ltrim($scope, '/'));
     }
 
-    public function get(string $scope): ResponseInterface {
-        return $this->client->request('GET',
-            $this->scopeFromBase($scope),
-        );
+    private function scopeId(string $scope_string): string {
+        return "webflow_api_" . sha1($scope_string);
     }
 
-    public function getClient(): HttpClientInterface {
-        return $this->client;
+    public function get(string $scope, int $ttl = 300): ResponseInterface {
+
+        $id = $this->scopeId($scope);
+
+        if ($ttl <= 0)  
+            return $this->client->request('GET',
+                $this->scopeFromBase($scope),
+            );
+
+        $item = $this->cache->get($id, function (ItemInterface $item) use ($scope, $ttl) {
+            $item->expiresAfter($ttl);
+
+            $response = $this->client->request('GET',
+                $this->scopeFromBase($scope),
+            );
+
+            if ($response->getStatusCode() >= 300)
+                return;
+
+            $item->set([
+                'header' => $response->getHeaders(),
+                'content' => $response->getContent(),
+            ]);
+
+            $this->cache->save($item);
+
+            return $item->get();
+        });
+
+        $this->cache->commit();
+        return MockResponse::fromRequest('GET', $scope, $item['header'], new MockResponse($item['content']));
     }
 
 }

+ 0 - 9
src/Http/WebflowApiClientInterface.php

@@ -1,9 +0,0 @@
-<?php
-
-namespace App\Http;
-
-interface WebflowApiClientInterface {
-    public function getClient();
-}
-
-?>

File diff suppressed because it is too large
+ 492 - 24
yarn.lock


Some files were not shown because too many files changed in this diff