├── www ├── libs │ └── .gitkeep ├── robots.txt ├── adminer.php ├── favicon.ico ├── flags │ ├── flags.png │ └── flags.min.css ├── img │ ├── octopus.png │ ├── og-image.jpg │ ├── spinner.gif │ ├── eventigo-header.png │ ├── header-background.jpg │ └── header-background-old.jpg ├── favicon │ ├── favicon.ico │ ├── mstile-70x70.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── apple-touch-icon.png │ ├── android-chrome-144x144.png │ ├── android-chrome-192x192.png │ ├── android-chrome-36x36.png │ ├── android-chrome-48x48.png │ ├── android-chrome-72x72.png │ ├── android-chrome-96x96.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-precomposed.png │ ├── browserconfig.xml │ ├── manifest.json │ └── safari-pinned-tab.svg ├── css │ ├── newsletter.css │ ├── newsletter.less │ ├── palette.less │ ├── style.css │ ├── admin.css │ └── admin.less ├── index.php ├── js │ ├── admin.js │ ├── lib.js │ └── plugins │ │ └── eventFilter.js ├── .maintenance.php ├── web.config └── .htaccess ├── .bowerrc ├── log ├── .htaccess └── web.config ├── temp ├── .htaccess └── web.config ├── app ├── .htaccess ├── config │ ├── decorator.neon │ ├── extensions │ │ ├── translation.neon │ │ ├── facebook.neon │ │ ├── restful.neon │ │ └── console.neon │ ├── database.neon │ ├── parameters.neon │ ├── config.neon │ ├── templates │ │ └── config.local.neon │ ├── factories.neon │ └── services.neon ├── Modules │ ├── Front │ │ ├── Components │ │ │ ├── Subscription │ │ │ │ ├── Subscription.latte │ │ │ │ ├── SubscriptionFactoryInterface.php │ │ │ │ └── Subscription.php │ │ │ ├── Tags │ │ │ │ ├── TagsFactoryInterface.php │ │ │ │ ├── Tags.latte │ │ │ │ └── Tags.php │ │ │ ├── Sign │ │ │ │ ├── SignInFactoryInterface.php │ │ │ │ ├── SignIn.latte │ │ │ │ └── SignIn.php │ │ │ ├── Settings │ │ │ │ ├── SettingsFactoryInterface.php │ │ │ │ └── Settings.php │ │ │ ├── SubscriptionTags │ │ │ │ ├── SubscriptionTagsFactoryInterface.php │ │ │ │ └── SubscriptionTags.latte │ │ │ └── EventsList │ │ │ │ ├── EventsListFactoryInterface.php │ │ │ │ └── EventsList.php │ │ ├── Model │ │ │ ├── Exceptions │ │ │ │ └── Subscription │ │ │ │ │ └── EmailExistsException.php │ │ │ └── EventsIterator.php │ │ └── Presenters │ │ │ ├── templates │ │ │ ├── Homepage │ │ │ │ ├── discover.latte │ │ │ │ └── default.latte │ │ │ └── Profile │ │ │ │ └── settings.latte │ │ │ ├── SignPresenter.php │ │ │ ├── RedirectPresenter.php │ │ │ ├── AbstractBasePresenter.php │ │ │ └── ProfilePresenter.php │ ├── Core │ │ ├── Presenters │ │ │ ├── templates │ │ │ │ └── Error │ │ │ │ │ ├── 4xx.latte │ │ │ │ │ ├── 405.latte │ │ │ │ │ ├── 410.latte │ │ │ │ │ ├── 403.latte │ │ │ │ │ ├── 404.latte │ │ │ │ │ └── 500.phtml │ │ │ ├── Error4xxPresenter.php │ │ │ ├── ErrorPresenter.php │ │ │ └── AbstractBasePresenter.php │ │ ├── Model │ │ │ ├── CountryModel.php │ │ │ ├── OrganiserModel.php │ │ │ ├── TagGroupModel.php │ │ │ ├── EventSeriesModel.php │ │ │ ├── EventRedirectModel.php │ │ │ ├── EventSources │ │ │ │ ├── AbstractEventSource.php │ │ │ │ ├── Utils │ │ │ │ │ └── EventSource.php │ │ │ │ ├── Srazy │ │ │ │ │ └── SrazyEventSource.php │ │ │ │ ├── Meetup │ │ │ │ │ └── MeetupEventSource.php │ │ │ │ └── Facebook │ │ │ │ │ └── FacebookEventSource.php │ │ │ ├── EventTagModel.php │ │ │ ├── UserTagModel.php │ │ │ ├── Entity │ │ │ │ ├── Tag.php │ │ │ │ └── EventTag.php │ │ │ ├── AbstractIterator.php │ │ │ ├── AbstractBaseModel.php │ │ │ └── TagModel.php │ │ ├── Components │ │ │ ├── Form │ │ │ │ └── Form.php │ │ │ └── AbstractBaseControl.php │ │ └── Utils │ │ │ ├── Collection.php │ │ │ ├── Helper.php │ │ │ ├── Filters.php │ │ │ └── DateTime.php │ ├── Newsletter │ │ ├── Model │ │ │ ├── NoEventsFoundException.php │ │ │ ├── Exception.php │ │ │ ├── UserNewsletterModel.php │ │ │ ├── NewsletterModel.php │ │ │ └── Entity │ │ │ │ └── Newsletter.php │ │ ├── Presenters │ │ │ ├── templates │ │ │ │ ├── Newsletter │ │ │ │ │ ├── default.latte │ │ │ │ │ ├── unsubscribe.latte │ │ │ │ │ └── newEvents.latte │ │ │ │ └── @layout.latte │ │ │ └── NewsletterPresenter.php │ │ └── Console │ │ │ ├── CreateNewsletterCommand.php │ │ │ ├── SendNewslettersCommand.php │ │ │ └── RenderNewslettersCommand.php │ ├── Admin │ │ ├── Components │ │ │ ├── SignIn │ │ │ │ ├── SignInFormFactoryInterface.php │ │ │ │ └── SignInForm.php │ │ │ ├── EventForm │ │ │ │ └── EventFormFactoryInterface.php │ │ │ ├── SourceForm │ │ │ │ ├── SourceFormFactoryInterface.php │ │ │ │ └── SourceForm.php │ │ │ ├── SourcesTable │ │ │ │ └── SourcesTableFactoryInterface.php │ │ │ ├── EventsTable │ │ │ │ ├── NotApprovedEventsTableFactoryInterface.php │ │ │ │ ├── NotApprovedEventsTable.latte │ │ │ │ └── js.latte │ │ │ └── DataTable │ │ │ │ └── AbstractDataTable.php │ │ ├── Presenters │ │ │ ├── AbstractBasePresenter.php │ │ │ ├── templates │ │ │ │ ├── Sources │ │ │ │ │ ├── create.latte │ │ │ │ │ └── default.latte │ │ │ │ ├── Sign │ │ │ │ │ ├── in.latte │ │ │ │ │ └── @layout.latte │ │ │ │ ├── Events │ │ │ │ │ ├── default.latte │ │ │ │ │ └── create.latte │ │ │ │ └── Dashboard │ │ │ │ │ └── default.latte │ │ │ ├── ExceptionPresenter.php │ │ │ ├── DashboardPresenter.php │ │ │ ├── SignPresenter.php │ │ │ └── SourcesPresenter.php │ │ ├── Model │ │ │ ├── SourceModel.php │ │ │ ├── OrganiserService.php │ │ │ ├── Authenticator.php │ │ │ └── SourceService.php │ │ └── Console │ │ │ ├── CrawlSourcesCommand.php │ │ │ └── GeneratePasswordCommand.php │ ├── Api │ │ └── V1 │ │ │ ├── Presenters │ │ │ └── EventsPresenter.php │ │ │ └── Model │ │ │ └── EventApiService.php │ └── Email │ │ ├── Model │ │ ├── Entity │ │ │ ├── Email.php │ │ │ └── BasicEmail.php │ │ └── EmailService.php │ │ └── Presenters │ │ └── EmailPresenter.php ├── lang │ ├── tags.cs_CZ.neon │ ├── newsletter.cs_CZ.neon │ ├── email.cs_CZ.neon │ └── front.cs_CZ.neon ├── web.config ├── bootstrap.php └── Router │ └── RouterFactory.php ├── reset.sh ├── events.sql.zip ├── resources ├── favicon.psd └── eventigo-header.psd ├── .editorconfig ├── .gitignore ├── tests ├── config │ └── config.test.neon ├── ContainerFactory.php └── Modules │ └── Newsletter │ └── Model │ └── NewsletterServiceTest.php ├── db └── migrations │ ├── 20160329220253_user_newsletter_html.php │ ├── 20160413064329_user_token.php │ ├── 20160320055050_bigger_code.php │ ├── 20160428174045_user_last_access.php │ ├── 20160317185357_password_column.php │ ├── 20160406194807_tag_group_icon.php │ ├── 20160514211953_event_approved.php │ ├── 20170205232804_demo_user.php │ ├── 20160514061722_event_state.php │ ├── 20170627065214_abroad_events.php │ ├── 20160309173103_nullable_columns.php │ ├── 20160505142316_newsletter_table.php │ ├── 20170626060205_venue.php │ ├── 20160323174545_event_redirect.php │ ├── 20160512205838_events_unique_index.php │ ├── 20160504063746_facebook.php │ ├── 20160324192410_users_events.php │ ├── 20160512150334_newsletter_content_limit.php │ ├── 20160327201056_sources.php │ ├── 20160402220653_tag_group.php │ ├── 20160322091004_users_events_series_and_organisers.php │ ├── 20160421173837_newsletter_refactoring.php │ └── 20160318171343_organisers.php ├── bower.json ├── bin └── console ├── phpunit.xml ├── .travis.yml ├── phinx.yml.template ├── phpstan.neon ├── composer.json ├── easy-coding-standard.neon └── README.md /www/libs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/robots.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "www/libs" 3 | } -------------------------------------------------------------------------------- /log/.htaccess: -------------------------------------------------------------------------------- 1 | Order Allow,Deny 2 | Deny from all -------------------------------------------------------------------------------- /temp/.htaccess: -------------------------------------------------------------------------------- 1 | Order Allow,Deny 2 | Deny from all -------------------------------------------------------------------------------- /app/.htaccess: -------------------------------------------------------------------------------- 1 | Order Allow,Deny 2 | Deny from all 3 | -------------------------------------------------------------------------------- /reset.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf temp/cache/* 4 | echo "Cache cleaned" 5 | -------------------------------------------------------------------------------- /events.sql.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/events.sql.zip -------------------------------------------------------------------------------- /www/adminer.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/adminer.php -------------------------------------------------------------------------------- /www/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon.ico -------------------------------------------------------------------------------- /www/flags/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/flags/flags.png -------------------------------------------------------------------------------- /www/img/octopus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/img/octopus.png -------------------------------------------------------------------------------- /www/img/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/img/og-image.jpg -------------------------------------------------------------------------------- /www/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/img/spinner.gif -------------------------------------------------------------------------------- /resources/favicon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/resources/favicon.psd -------------------------------------------------------------------------------- /www/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/favicon.ico -------------------------------------------------------------------------------- /www/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /www/img/eventigo-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/img/eventigo-header.png -------------------------------------------------------------------------------- /app/config/decorator.neon: -------------------------------------------------------------------------------- 1 | decorator: 2 | App\Modules\Core\Components\AbstractBaseControl: 3 | inject: true -------------------------------------------------------------------------------- /resources/eventigo-header.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/resources/eventigo-header.psd -------------------------------------------------------------------------------- /www/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /www/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /www/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /www/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /www/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /www/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /www/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /www/img/header-background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/img/header-background.jpg -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /www/img/header-background-old.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/img/header-background-old.jpg -------------------------------------------------------------------------------- /app/Modules/Front/Components/Subscription/Subscription.latte: -------------------------------------------------------------------------------- 1 | {form form} 2 | {input email} 3 | {input subscribe} 4 | {/form} -------------------------------------------------------------------------------- /www/favicon/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/android-chrome-144x144.png -------------------------------------------------------------------------------- /www/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /www/favicon/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/android-chrome-36x36.png -------------------------------------------------------------------------------- /www/favicon/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/android-chrome-48x48.png -------------------------------------------------------------------------------- /www/favicon/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/android-chrome-72x72.png -------------------------------------------------------------------------------- /www/favicon/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/android-chrome-96x96.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /www/favicon/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filipsuk/eventigo-web/HEAD/www/favicon/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /app/lang/tags.cs_CZ.neon: -------------------------------------------------------------------------------- 1 | tagGroup.business: Podnikání 2 | tagGroup.development: Programování 3 | tagGroup.marketing: Marketing 4 | tagGroup.others: Další -------------------------------------------------------------------------------- /app/config/extensions/translation.neon: -------------------------------------------------------------------------------- 1 | extensions: 2 | translation: Kdyby\Translation\DI\TranslationExtension 3 | 4 | translation: 5 | default: cs 6 | fallback: [cs_CZ, cs] -------------------------------------------------------------------------------- /app/config/database.neon: -------------------------------------------------------------------------------- 1 | database: 2 | dsn: 'mysql:host=%database.host%;dbname=%database.name%' 3 | user: %database.username% 4 | password: %database.password% 5 | options: 6 | lazy: yes 7 | -------------------------------------------------------------------------------- /app/Modules/Core/Presenters/templates/Error/4xx.latte: -------------------------------------------------------------------------------- 1 | {block content} 2 |

Oops...

3 | 4 |

Your browser sent a request that this server could not understand or process.

5 | -------------------------------------------------------------------------------- /app/Modules/Newsletter/Model/NoEventsFoundException.php: -------------------------------------------------------------------------------- 1 | Method Not Allowed 3 | 4 |

The requested method is not allowed for the URL.

5 | 6 |

error 405

7 | -------------------------------------------------------------------------------- /app/Modules/Newsletter/Model/Exception.php: -------------------------------------------------------------------------------- 1 | 5 | {$userNewsletter['content']|noescape} 6 | 7 | {/block} 8 | -------------------------------------------------------------------------------- /app/lang/newsletter.cs_CZ.neon: -------------------------------------------------------------------------------- 1 | unsubscribe.unsubscribed: "No dobrá. My si email %email% zatím škrtáme, ale brzy na viděnou (ツ)" 2 | email.events.nextWeek: "Příští týden" 3 | email.events.selectedNewEvents: "Vybrané nové akce" 4 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/Sign/SignInFactoryInterface.php: -------------------------------------------------------------------------------- 1 | Page Not Found 3 | 4 |

The page you requested has been taken off the site. We apologize for the inconvenience.

5 | 6 |

error 410

7 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/Settings/SettingsFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 5 | {_newsletter.unsubscribe.unsubscribed, [email => $email]|noescape} 6 |

7 | {/block} 8 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/Subscription/SubscriptionFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getByType('Nette\Application\Application')->run(); 9 | -------------------------------------------------------------------------------- /www/js/admin.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $('select').select2({ 3 | width: '100%' 4 | }); 5 | 6 | $('.datetime').datetimepicker({ 7 | locale: 'cs', 8 | format: "D. M. YYYY LT" 9 | }); 10 | 11 | $('.date').datetimepicker({ 12 | locale: 'cs', 13 | format: "D. M. YYYY" 14 | }); 15 | }); -------------------------------------------------------------------------------- /app/Modules/Front/Components/Sign/SignIn.latte: -------------------------------------------------------------------------------- 1 | {snippet form} 2 |

3 | 4 | {="front.signIn.form.title"|translate} 5 |

6 | {form form, class => ajax} 7 | {input email} 8 | {input submit, class => 'btn btn-success'} 9 | {/form} 10 | {/snippet} -------------------------------------------------------------------------------- /app/Modules/Core/Model/EventSeriesModel.php: -------------------------------------------------------------------------------- 1 | Access Denied 3 | 4 |

You do not have permission to view this page. Please try contact the web 5 | site administrator if you believe you should be able to view this page.

6 | 7 |

error 403

8 | -------------------------------------------------------------------------------- /app/lang/email.cs_CZ.neon: -------------------------------------------------------------------------------- 1 | login.loginButton: "Přihlásit se" 2 | login.text: "Pro přihlášení klikni na tlačítko níže. Nevedeme uživatelská hesla, protože si je stejně nikdo nepamatuje." 3 | login.footerText: "Tento e-mail jsi dostal na základě pokusu o přihlášení na eventigo.cz." 4 | login.subject: Přihlášení na eventigo.cz 5 | -------------------------------------------------------------------------------- /tests/config/config.test.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | meetup: 3 | apiKey: '' 4 | 5 | SendGrid: 6 | ApiKey: '' 7 | 8 | facebook: 9 | appSecret: "default_secret_to_confirm_format" 10 | 11 | database: 12 | dsn: 'mysql:host=127.0.0.1:1234;dbname=events' 13 | user: 14 | password: 15 | options: 16 | lazy: yes 17 | -------------------------------------------------------------------------------- /app/Modules/Admin/Components/SourcesTable/SourcesTableFactoryInterface.php: -------------------------------------------------------------------------------- 1 | {="newsletter.email.events.selectedNewEvents"|translate} 2 | 5 | -------------------------------------------------------------------------------- /www/js/lib.js: -------------------------------------------------------------------------------- 1 | Array.prototype.prefix = function (prefix) { 2 | var arr = []; 3 | for (i = 0; i < this.length; i++) { 4 | arr[i] = prefix + this[i]; 5 | } 6 | return arr; 7 | }; 8 | 9 | Array.prototype.diff = function (a) { 10 | return this.filter(function (i) { 11 | return a.indexOf(i) < 0; 12 | }); 13 | }; -------------------------------------------------------------------------------- /db/migrations/20160329220253_user_newsletter_html.php: -------------------------------------------------------------------------------- 1 | table('users_newsletters') 10 | ->addColumn('html', 'text', ['after' => 'variables']) 11 | ->update(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /db/migrations/20160413064329_user_token.php: -------------------------------------------------------------------------------- 1 | table('users') 10 | ->addColumn('token', 'string', ['limit' => 64, 'after' => 'password', 'null' => true]) 11 | ->update(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/Modules/Core/Presenters/templates/Error/404.latte: -------------------------------------------------------------------------------- 1 | {block content} 2 |

Page Not Found

3 | 4 |

The page you requested could not be found. It is possible that the address is 5 | incorrect, or that the page no longer exists. Please use a search engine to find 6 | what you are looking for.

7 | 8 |

error 404

9 | -------------------------------------------------------------------------------- /app/config/parameters.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | baseUrl: 'https://eventigo.cz' 3 | 4 | newsletter: 5 | sendNewsletterWithNoEvents: true 6 | defaultSubject: Akce na příští týden 7 | defaultAuthor: 8 | name: Filip 9 | email: filip@eventigo.cz 10 | 11 | database: 12 | host: 127.0.0.1:8889 13 | name: events 14 | username: 15 | password: 16 | -------------------------------------------------------------------------------- /db/migrations/20160320055050_bigger_code.php: -------------------------------------------------------------------------------- 1 | table('tags') 10 | ->changeColumn('code', 'string', ['limit' => 32, 'null' => false]) 11 | ->update(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/config/extensions/restful.neon: -------------------------------------------------------------------------------- 1 | extensions: 2 | restful: Drahak\Restful\DI\RestfulExtension 3 | 4 | restful: 5 | convention: 'snake_case' 6 | cacheDir: '%tempDir%/cache' 7 | jsonpKey: FALSE 8 | prettyPrintKey: 'pretty' 9 | routes: 10 | generateAtStart: FALSE 11 | autoGenerated: FALSE 12 | panel: TRUE 13 | 14 | security: 15 | requestTimeout: 300 16 | -------------------------------------------------------------------------------- /app/Modules/Admin/Components/EventsTable/NotApprovedEventsTableFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getByType(Symfony\Component\Console\Application::class); 9 | $consoleApplication->run(); 10 | -------------------------------------------------------------------------------- /db/migrations/20160428174045_user_last_access.php: -------------------------------------------------------------------------------- 1 | table('users') 10 | ->addColumn('last_access', 'datetime', ['null' => true, 'after' => 'newsletter']) 11 | ->update(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /db/migrations/20160317185357_password_column.php: -------------------------------------------------------------------------------- 1 | table('users') 10 | ->addColumn('password', 'string', ['limit' => 152, 'after' => 'email', 'null' => true]) 11 | ->update(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /db/migrations/20160406194807_tag_group_icon.php: -------------------------------------------------------------------------------- 1 | table('tags_groups') 10 | ->addColumn('icon', 'string', ['limit' => 255, 'null' => true, 'after' => 'name']) 11 | ->update(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /db/migrations/20160514211953_event_approved.php: -------------------------------------------------------------------------------- 1 | table('events') 10 | ->addColumn('approved', 'datetime', ['after' => 'state', 'null' => true]) 11 | ->update(); 12 | 13 | $this->execute("UPDATE events SET approved = created"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /db/migrations/20170205232804_demo_user.php: -------------------------------------------------------------------------------- 1 | 'demo@gmail.com', 11 | 'password' => '$2y$10$00l9Qn.Ou7xaiHgT0fJpHuwbC3mbjYqQF4SdV3H616W4nKoagwhdy', // demo 12 | 'newsletter' => true 13 | ]; 14 | 15 | $this->insert('users', $demoUser); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/config/extensions/console.neon: -------------------------------------------------------------------------------- 1 | extensions: 2 | console: Contributte\Console\DI\ConsoleExtension 3 | 4 | console: 5 | url: %baseUrl% 6 | 7 | services: 8 | - App\Modules\Newsletter\Console\CreateNewsletterCommand 9 | - App\Modules\Newsletter\Console\RenderNewslettersCommand 10 | - App\Modules\Newsletter\Console\SendNewslettersCommand 11 | - App\Modules\Admin\Console\GeneratePasswordCommand 12 | - App\Modules\Admin\Console\CrawlSourcesCommand 13 | -------------------------------------------------------------------------------- /db/migrations/20160514061722_event_state.php: -------------------------------------------------------------------------------- 1 | table('events') 10 | ->addColumn('state', 'enum', ['values' => ['approved', 'not-approved', 'skip'], 11 | 'default' => 'not-approved', 'after' => 'rate']) 12 | ->update(); 13 | 14 | $this->execute('UPDATE events SET state = "approved"'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /www/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #da532c 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | tests 10 | 11 | 12 | 13 | 14 | 15 | app 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /db/migrations/20170627065214_abroad_events.php: -------------------------------------------------------------------------------- 1 | table('users') 10 | ->addColumn( 11 | 'abroad_events', 12 | Phinx\Db\Adapter\MysqlAdapter::PHINX_TYPE_BOOLEAN, 13 | ['after' => 'newsletter', 'default' => true] 14 | ) 15 | ->update(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | 6 | install: 7 | # install composer dependencies 8 | - composer install 9 | 10 | script: 11 | # check coding standard (defined in composer.json "scripts" section) 12 | - composer cs 13 | # check with phpstan (defined in composer.json "scripts" section) 14 | - composer phpstan 15 | # run tests 16 | - vendor/bin/phpunit 17 | 18 | # do not send success notifications, they have no value 19 | notifications: 20 | email: 21 | on_success: never 22 | -------------------------------------------------------------------------------- /www/css/palette.less: -------------------------------------------------------------------------------- 1 | /* Palette generated by Material Palette - materialpalette.com/blue/light-green */ 2 | 3 | @darkPrimaryColor: #1976D2; 4 | @darkishPrimaryColor: #2783DE; 5 | @primaryColor: #2196F3; 6 | @lightPrimaryColor: #BBDEFB; 7 | @textPrimaryColor: #FFFFFF; 8 | @accentColor: #8BC34A; 9 | @primaryTextColor: #212121; 10 | @secondaryTextColor: #727272; 11 | @dividerColor: #B6B6B6; 12 | 13 | @footerBgColor: #3e464c; 14 | @footerColor: #99979c; 15 | 16 | @redColor: #f44336; -------------------------------------------------------------------------------- /db/migrations/20160309173103_nullable_columns.php: -------------------------------------------------------------------------------- 1 | table('events') 10 | ->changeColumn('description', 'text', ['null' => true]) 11 | ->changeColumn('end', 'datetime', ['null' => true]) 12 | ->changeColumn('image', 'string', ['limit' => 2083, 'comment' => 'url', 'null' => true]) 13 | ->update(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Modules/Admin/Components/EventsTable/NotApprovedEventsTable.latte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
{="admin.notApprovedEventsTable.eventSeries"|translate}{="admin.notApprovedEventsTable.name"|translate}{="admin.notApprovedEventsTable.date"|translate}{="admin.notApprovedEventsTable.created"|translate}
-------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/AbstractBasePresenter.php: -------------------------------------------------------------------------------- 1 | user->isLoggedIn() || ! $this->user->isInRole('admin')) { 15 | $this->redirect('Sign:in'); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/templates/Sources/create.latte: -------------------------------------------------------------------------------- 1 | {layout '../@layout.latte'} 2 | 3 | {block header} 4 | {if $presenter->getAction() === 'create'} 5 | {="admin.sources.create.header"|translate} 6 | {else} 7 | {="admin.sources.update.header"|translate} 8 | {/if} 9 | {/block} 10 | 11 | {block content} 12 |
13 |
14 |
15 |
16 | {control sourceForm} 17 |
18 |
19 |
20 |
21 | {/block} 22 | -------------------------------------------------------------------------------- /app/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /log/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /temp/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /app/Modules/Core/Components/Form/Form.php: -------------------------------------------------------------------------------- 1 | getRenderer(); 16 | $renderer->wrappers['label']['container'] = 'th class="th-label"'; 17 | $renderer->wrappers['control']['container'] = 'td class="td-control"'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/config/config.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - database.neon 3 | - factories.neon 4 | - services.neon 5 | - decorator.neon 6 | - parameters.neon 7 | - extensions/translation.neon 8 | - extensions/console.neon 9 | - extensions/facebook.neon 10 | - extensions/restful.neon 11 | 12 | php: 13 | date.timezone: Europe/Prague 14 | 15 | 16 | application: 17 | errorPresenter: Core:Error 18 | scanComposer: false 19 | scanDirs: false 20 | mapping: 21 | *: App\Modules\*\Presenters\*Presenter 22 | 23 | session: 24 | expiration: 365 days 25 | savePath: "%tempDir%/sessions" 26 | 27 | extensions: 28 | slackLogger: greeny\NetteSlackLogger\DI\SlackLoggerExtension 29 | -------------------------------------------------------------------------------- /app/Modules/Admin/Model/SourceModel.php: -------------------------------------------------------------------------------- 1 | 1, 14 | 'twiceAWeek' => 3, 15 | 'weekly' => 7, 16 | 'fortnightly' => 14, 17 | 'monthly' => 30, 18 | 'quarterly' => 90, 19 | 'half-yearly' => 183, 20 | 'yearly' => 365, 21 | ]; 22 | 23 | /** 24 | * @var string 25 | */ 26 | protected const TABLE_NAME = 'sources'; 27 | } 28 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/ExceptionPresenter.php: -------------------------------------------------------------------------------- 1 | sendResponse(new FileResponse($file, $filename, 'text/html', false)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /db/migrations/20160505142316_newsletter_table.php: -------------------------------------------------------------------------------- 1 | table('newsletters'); 10 | $newsletter->addColumn('subject', 'string', array('limit' => 100)) 11 | ->addColumn('from', 'string', array('limit' => 100)) 12 | ->addColumn('intro_text', 'text') 13 | ->addColumn('outro_text', 'text') 14 | ->addColumn('author', 'string', array('limit' => 20)) 15 | ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) 16 | ->create(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/Modules/Front/Presenters/templates/Homepage/discover.latte: -------------------------------------------------------------------------------- 1 | {layout '../@layout.latte'} 2 | 3 | {block content} 4 |
5 |
6 |
7 | {control subscriptionTags} 8 |
9 |
10 |
11 | {="front.homepage.events.header"|translate} 12 |
13 | {control eventsList} 14 |
15 |
16 |
17 |
18 | {/block} 19 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/EventSources/AbstractEventSource.php: -------------------------------------------------------------------------------- 1 | getHost(); 15 | 16 | return in_array($host, static::URLS, true); 17 | } catch (Throwable $throwable) { 18 | return false; 19 | } 20 | } 21 | 22 | /** 23 | * @return Event[] 24 | */ 25 | abstract public function getEvents(string $source): array; 26 | } 27 | -------------------------------------------------------------------------------- /app/config/templates/config.local.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | 3 | baseUrl: 'https://eventigo.cz' 4 | 5 | lastAccess: 6 | syncToDb: 5 minutes 7 | 8 | newsletter: 9 | sendNewsletterWithNoEvents: true 10 | defaultSubject: Akce na příští týden 11 | defaultAuthor: 12 | name: Filip 13 | email: filip@eventigo.cz 14 | 15 | # googleAnalyticsKey: UA-XXXX 16 | 17 | SendGrid: 18 | ApiKey: '' 19 | 20 | meetup: 21 | apiKey: '' 22 | 23 | database: 24 | host: 25 | name: 26 | username: 27 | password: 28 | 29 | slackLogger: 30 | enabled: false 31 | timeout: 60 32 | slackUrl: https://hooks.slack.com/services/XXXX 33 | logUrl: '%baseUrl%/admin/exception/?filename=__FILE__' 34 | defaults: 35 | name: EventigoApp 36 | -------------------------------------------------------------------------------- /db/migrations/20170626060205_venue.php: -------------------------------------------------------------------------------- 1 | table('events') 10 | ->addColumn( 11 | 'venue', 12 | Phinx\Db\Adapter\MysqlAdapter::PHINX_TYPE_STRING, 13 | ['after' => 'end', 'null' => true]) 14 | ->addColumn( 15 | 'country_id', 16 | Phinx\Db\Adapter\MysqlAdapter::PHINX_TYPE_STRING, 17 | ['after' => 'venue', 'limit' => 2, 'null' => true] 18 | ) 19 | ->addForeignKey('country_id', 'countries', 'id') 20 | ->update(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/EventTagModel.php: -------------------------------------------------------------------------------- 1 | getAll()->where('event_id', $eventId)->order('rate DESC')->fetchAll() as $eventTag) { 19 | $hashtags .= '#' . $eventTag->ref('tags', 'tag_id')->code . ' '; 20 | } 21 | 22 | return $hashtags; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /www/.maintenance.php: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 17 | 18 | Site is temporarily down for maintenance 19 | 20 |

We're Sorry

21 | 22 |

The site is temporarily down for maintenance. Please try again in a few minutes.

23 | 24 | table('events_redirects') 10 | ->addColumn('event_id', 'integer', ['signed' => false]) 11 | ->addColumn('user_id', 'integer', ['signed' => false, 'null' => true]) 12 | ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) 13 | ->addForeignKey('event_id', 'events', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 14 | ->addForeignKey('user_id', 'users', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 15 | ->create(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/EventsList/EventsList.php: -------------------------------------------------------------------------------- 1 | events = $events; 21 | } 22 | 23 | public function render(): void 24 | { 25 | $this->template->events = new EventsIterator($this->events); 26 | $this->template->render(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/templates/Sign/in.latte: -------------------------------------------------------------------------------- 1 | {layout '@layout.latte'} 2 | 3 | {block content} 4 | {form signInForm-form} 5 |
6 | {input email, class => form-control} 7 | 8 |
9 |
10 | {input password, class => form-control} 11 | 12 |
13 |
14 |
15 | {input signIn, class => 'btn btn-primary btn-block btn-flat'} 16 |
17 |
18 | {/form} 19 | {/block} 20 | -------------------------------------------------------------------------------- /db/migrations/20160512205838_events_unique_index.php: -------------------------------------------------------------------------------- 1 | table('events'); 15 | $events->changeColumn('name', 'string', array('limit' => 128)) 16 | ->addIndex(array('name', 'start'), array('unique' => true)) 17 | ->save(); 18 | } 19 | 20 | public function down() 21 | { 22 | $events = $this->table('events'); 23 | $events->changeColumn('name', 'string', array('limit' => 1024)) 24 | ->removeIndex(array('name', 'start')) 25 | ->save(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /db/migrations/20160504063746_facebook.php: -------------------------------------------------------------------------------- 1 | table('users') 10 | ->addColumn('facebook_id', 'string', ['null' => true, 'after' => 'email', 'limit' => 17]) 11 | ->addColumn('facebook_token', 'string', ['null' => true, 'after' => 'token', 'limit' => 168]) 12 | ->addColumn('firstname', 'string', ['null' => true, 'after' => 'email', 'limit' => 32]) 13 | ->addColumn('fullname', 'string', ['null' => true, 'after' => 'firstname', 'limit' => 122]) 14 | ->changeColumn('email', 'string', ['limit' => 254, 'null' => true]) 15 | ->update(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /db/migrations/20160324192410_users_events.php: -------------------------------------------------------------------------------- 1 | table('users_events', ['id' => false, 'primary_key' => ['user_id', 'event_id']]) 10 | ->addColumn('user_id', 'integer', ['signed' => false]) 11 | ->addColumn('event_id', 'integer', ['signed' => false]) 12 | ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) 13 | ->addForeignKey('user_id', 'users', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 14 | ->addForeignKey('event_id', 'events', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 15 | ->create(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Modules/Admin/Components/EventsTable/js.latte: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /db/migrations/20160512150334_newsletter_content_limit.php: -------------------------------------------------------------------------------- 1 | table('users_newsletters'); 13 | $users->changeColumn('content', 'text', array('limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_MEDIUM)) 14 | ->save(); 15 | } 16 | 17 | /** 18 | * Migrate Down. 19 | */ 20 | public function down() 21 | { 22 | $users = $this->table('users_newsletters'); 23 | $users->changeColumn('content', 'text', array('limit' => \Phinx\Db\Adapter\MysqlAdapter::TEXT_REGULAR)) 24 | ->save(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/EventSources/Utils/EventSource.php: -------------------------------------------------------------------------------- 1 | getHost(); 25 | 26 | return in_array($host, self::SOURCES, true); 27 | } catch (Throwable $throwable) { 28 | return false; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/Modules/Api/V1/Presenters/EventsPresenter.php: -------------------------------------------------------------------------------- 1 | eventApiService = $eventApiService; 20 | } 21 | 22 | public function actionRead(): void 23 | { 24 | $this->resource = $this->eventApiService->getEvents(); 25 | $this->sendResource(IResource::JSON); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/ContainerFactory.php: -------------------------------------------------------------------------------- 1 | setDebugMode(true); 14 | $configurator->setTempDirectory(__DIR__ . '/../temp'); 15 | 16 | $configurator->addConfig(__DIR__ . '/../app/config/config.neon'); 17 | $localConfig = __DIR__ . '/../app/config/config.local.neon'; 18 | if (file_exists($localConfig)) { 19 | $configurator->addConfig($localConfig); 20 | } 21 | $configurator->addConfig(__DIR__ . '/config/config.test.neon'); 22 | 23 | return $configurator->createContainer(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/config/factories.neon: -------------------------------------------------------------------------------- 1 | services: 2 | - App\Modules\Front\Components\Subscription\SubscriptionFactoryInterface 3 | - App\Modules\Front\Components\SubscriptionTags\SubscriptionTagsFactoryInterface 4 | - App\Modules\Front\Components\EventsList\EventsListFactoryInterface 5 | - App\Modules\Admin\Components\EventForm\EventFormFactoryInterface 6 | - App\Modules\Admin\Components\SignIn\SignInFormFactoryInterface 7 | - App\Modules\Front\Components\Tags\TagsFactoryInterface 8 | - App\Modules\Front\Components\Settings\SettingsFactoryInterface 9 | - App\Modules\Admin\Components\SourceForm\SourceFormFactoryInterface 10 | - App\Modules\Admin\Components\SourcesTable\SourcesTableFactoryInterface 11 | - App\Modules\Admin\Components\EventsTable\NotApprovedEventsTableFactoryInterface 12 | - App\Modules\Front\Components\Sign\SignInFactoryInterface 13 | -------------------------------------------------------------------------------- /app/Modules/Core/Presenters/templates/Error/500.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Server Error 5 | 6 | 12 | 13 |
14 |

Server Error

15 | 16 |

We're sorry! The server encountered an internal error and 17 | was unable to complete your request. Please try again later.

18 | 19 |

error 500

20 |
21 | -------------------------------------------------------------------------------- /app/Modules/Core/Presenters/Error4xxPresenter.php: -------------------------------------------------------------------------------- 1 | getRequest()->isMethod(Request::FORWARD)) { 14 | $this->error(); 15 | } 16 | } 17 | 18 | public function renderDefault(BadRequestException $exception): void 19 | { 20 | // load template 403.latte or 404.latte or ... 4xx.latte 21 | $file = __DIR__ . "/templates/Error/{$exception->getCode()}.latte"; 22 | $this->template->setFile(is_file($file) ? $file : __DIR__ . '/templates/Error/4xx.latte'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /db/migrations/20160327201056_sources.php: -------------------------------------------------------------------------------- 1 | table('sources') 10 | ->addColumn('name', 'string', ['limit' => 1024, 'null' => true]) 11 | ->addColumn('url', 'string', ['limit' => 2083, 'null' => true]) 12 | ->addColumn('event_series_id', 'integer', ['signed' => false, 'null' => true]) 13 | ->addColumn('check_frequency', 'integer', ['default' => 1]) 14 | ->addColumn('next_check', 'date') 15 | ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) 16 | ->addForeignKey('event_series_id', 'events_series', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 17 | ->create(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /www/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /www/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 15px; 3 | line-height: 1.6; 4 | color: #333; 5 | background: white; 6 | } 7 | 8 | h1 { 9 | color: #3484D2; 10 | } 11 | 12 | #ajax-spinner { 13 | margin: 15px 0 0 15px; 14 | padding: 13px; 15 | background: white url('../img/spinner.gif') no-repeat 50% 50%; 16 | font-size: 0; 17 | z-index: 123456; 18 | display: none; 19 | } 20 | 21 | div.flash { 22 | color: black; 23 | background: #FFF9D7; 24 | border: 1px solid #E2C822; 25 | padding: 1em; 26 | margin: 1em 0; 27 | } 28 | 29 | a[href^="#error:"] { 30 | background: red; 31 | color: white; 32 | } 33 | 34 | form th, form td { 35 | vertical-align: top; 36 | font-weight: normal; 37 | } 38 | 39 | form th { 40 | text-align: right; 41 | } 42 | 43 | form .required label { 44 | font-weight: bold; 45 | } 46 | 47 | form .error { 48 | color: #D00; 49 | font-weight: bold; 50 | } 51 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/templates/Events/default.latte: -------------------------------------------------------------------------------- 1 | {layout '../@layout.latte'} 2 | 3 | {block header} 4 | 5 | {/block} 6 | 7 | {block content} 8 |
9 | {="admin.events.create.header"|translate} 10 | 11 | 12 | {="admin.events.crawlSources.link"|translate} 13 | 14 |
15 | 16 |
17 |
18 | {="admin.events.default.notApprovedEventsTable.title"|translate} 19 |
20 |
21 | {control notApprovedEventsTable} 22 |
23 |
24 | {/block} 25 | 26 | 27 | {block scripts} 28 | {control notApprovedEventsTable:js} 29 | {/block} -------------------------------------------------------------------------------- /www/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Eventigo.cz", 3 | "icons": [ 4 | { 5 | "src": "\/favicon\/android-chrome-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": 0.75 9 | }, 10 | { 11 | "src": "\/favicon\/android-chrome-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": 1 15 | }, 16 | { 17 | "src": "\/favicon\/android-chrome-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": 1.5 21 | }, 22 | { 23 | "src": "\/favicon\/android-chrome-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": 2 27 | }, 28 | { 29 | "src": "\/favicon\/android-chrome-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": 3 33 | }, 34 | { 35 | "src": "\/favicon\/android-chrome-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": 4 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /app/bootstrap.php: -------------------------------------------------------------------------------- 1 | setDebugMode(in_array( 9 | $_SERVER['HTTP_HOST'], 10 | ['localhost:8000', 'kuba.eventigo.cz', 'filip.eventigo.cz', 'eventigo.local', 'eventigo.local.cz'], 11 | true 12 | )); 13 | } else { 14 | $configurator->setDebugMode(true); 15 | } 16 | 17 | // Fix redirects to port 80 (https://forum.nette.org/cs/13896-openshift-redirect-z-https-pridava-port-80) 18 | if (! empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { 19 | $_SERVER['SERVER_PORT'] = 443; 20 | } 21 | 22 | $configurator->enableDebugger(__DIR__ . '/../log'); 23 | 24 | $configurator->setTempDirectory(__DIR__ . '/../temp'); 25 | 26 | $configurator->addConfig(__DIR__ . '/config/config.neon'); 27 | $configurator->addConfig(__DIR__ . '/config/config.local.neon'); 28 | 29 | return $configurator->createContainer(); 30 | -------------------------------------------------------------------------------- /www/.htaccess: -------------------------------------------------------------------------------- 1 | # Apache configuration file (see httpd.apache.org/docs/current/mod/quickreference.html) 2 | 3 | # disable directory listing 4 | 5 | Options -Indexes 6 | 7 | 8 | # enable cool URL 9 | 10 | RewriteEngine On 11 | # RewriteBase / 12 | 13 | RewriteCond %{HTTP_HOST} ^www\.eventigo\.cz$ [NC] 14 | RewriteRule ^(.*)$ https://eventigo.cz/$1 [L,R=301] 15 | 16 | # prevents files starting with dot to be viewed by browser 17 | RewriteRule /\.|^\.(?!well-known/) - [F] 18 | 19 | # front controller 20 | RewriteCond %{REQUEST_FILENAME} !-f 21 | RewriteCond %{REQUEST_FILENAME} !-d 22 | RewriteRule !\.(pdf|js|ico|gif|jpg|png|css|rar|zip|tar\.gz|map)$ index.php [L] 23 | 24 | 25 | # enable gzip compression 26 | 27 | 28 | AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css application/javascript application/json application/xml image/svg+xml 29 | 30 | 31 | -------------------------------------------------------------------------------- /phinx.yml.template: -------------------------------------------------------------------------------- 1 | paths: 2 | migrations: %%PHINX_CONFIG_DIR%%/db/migrations 3 | seeds: %%PHINX_CONFIG_DIR%%/db/seeds 4 | 5 | environments: 6 | default_migration_table: phinxlog 7 | default_database: development 8 | 9 | local: 10 | adapter: mysql 11 | host: localhost 12 | name: production_db 13 | user: root 14 | pass: '' 15 | port: 3306 16 | charset: utf8 17 | 18 | development: 19 | adapter: mysql 20 | host: localhost 21 | name: development_db 22 | user: root 23 | pass: '' 24 | port: 3306 25 | charset: utf8 26 | 27 | testing: 28 | adapter: mysql 29 | host: localhost 30 | name: testing_db 31 | user: root 32 | pass: '' 33 | port: 3306 34 | charset: utf8 35 | 36 | production: 37 | adapter: mysql 38 | host: localhost 39 | name: production_db 40 | user: root 41 | pass: '' 42 | port: 3306 43 | charset: utf8 44 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/UserTagModel.php: -------------------------------------------------------------------------------- 1 | getAll() 18 | ->select('tag.tag_group.name AS tagGroupName') 19 | ->select('tag.code') 20 | ->where('user_id', $userId) 21 | ->group('tag.tag_group_id, tag.id') 22 | ->fetchAssoc('tagGroupName[]=code'); 23 | } 24 | 25 | /** 26 | * Get ids of user's subscribed tags. 27 | * 28 | * @return mixed[] 29 | */ 30 | public function getUserTagIds(int $userId): array 31 | { 32 | return $this->getAll() 33 | ->select('tag_id') 34 | ->where('user_id', $userId) 35 | ->fetchAssoc('[]=tag_id'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Modules/Core/Utils/Collection.php: -------------------------------------------------------------------------------- 1 | id = $id; 27 | $this->name = $name; 28 | $this->code = $code; 29 | } 30 | 31 | public static function createFromRow(IRow $tagRow): Tag 32 | { 33 | return new self($tagRow['id'], $tagRow['name'], $tagRow['code']); 34 | } 35 | 36 | public function getId(): int 37 | { 38 | return $this->id; 39 | } 40 | 41 | public function getName(): string 42 | { 43 | return $this->name; 44 | } 45 | 46 | public function getCode(): string 47 | { 48 | return $this->code; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Modules/Front/Presenters/templates/Profile/settings.latte: -------------------------------------------------------------------------------- 1 | {layout '../@layout.latte'} 2 | 3 | {block content} 4 |
5 | {* subscription *} 6 |
7 |
8 |
9 |

{="front.profile.settings.main.title"|translate}

10 |
11 |
12 |
13 |
14 | {control settings} 15 |
16 |
17 |
18 | 19 | {* tags *} 20 |
21 |
22 |
23 |

{="front.profile.settings.tags.title"|translate}

24 |
25 |
26 |
27 | {control tags} 28 |
29 |
30 |
31 | {/block} 32 | -------------------------------------------------------------------------------- /db/migrations/20160402220653_tag_group.php: -------------------------------------------------------------------------------- 1 | table('tags_groups', ['id' => false, 'primary_key' => 'id']) 10 | ->addColumn('id', 'integer', ['signed' => false, 'identity' => true]) 11 | ->addColumn('name', 'string', ['limit' => 32]) 12 | ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) 13 | ->create(); 14 | 15 | $this->table('tags') 16 | ->addColumn('tag_group_id', 'integer', ['signed' => false, 'null' => true, 'after' => 'code']) 17 | ->addForeignKey('tag_group_id', 'tags_groups', 'id', ['update' => 'CASCADE', 'delete' => 'SET_NULL']) 18 | ->update(); 19 | 20 | $this->table('tags_groups')->insert([ 21 | ['name' => 'development'], 22 | ['name' => 'business'], 23 | ['name' => 'marketing'], 24 | ['name' => 'others'], 25 | ])->update(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Modules/Newsletter/Model/UserNewsletterModel.php: -------------------------------------------------------------------------------- 1 | insert([ 21 | 'user_id' => $userId, 22 | 'from' => $from, 23 | 'subject' => $subject, 24 | 'content' => $content, 25 | 'hash' => $hash ?? $this->generateUniqueHash(), 26 | ]); 27 | } 28 | 29 | public function generateUniqueHash(): string 30 | { 31 | do { 32 | $hash = Random::generate(32); 33 | } while ($this->getAll()->where(['hash' => $hash])->fetch()); 34 | 35 | return $hash; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/Modules/Admin/Components/DataTable/AbstractDataTable.php: -------------------------------------------------------------------------------- 1 | dataSource = $dataSource; 25 | } 26 | 27 | /** 28 | * @return mixed[] 29 | */ 30 | abstract public function generateJson(): array; 31 | 32 | public function handleJson(): void 33 | { 34 | $json = $this->generateJson(); 35 | $this->presenter->sendResponse(new JsonResponse($json)); 36 | } 37 | 38 | public function getLang(): string 39 | { 40 | return '//cdn.datatables.net/plug-ins/1.10.11/i18n/Czech.json'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Modules/Core/Utils/Helper.php: -------------------------------------------------------------------------------- 1 | tag. 14 | */ 15 | public static function utfToHtmlEntities(string $input): string 16 | { 17 | $emogrifier = new Emogrifier; 18 | 19 | try { 20 | $emogrifier->setHtml($input); 21 | 22 | return $emogrifier->emogrifyBodyContent(); 23 | } catch (BadMethodCallException $e) { 24 | Debugger::log($e->getMessage()); 25 | 26 | return $input; 27 | } 28 | } 29 | 30 | /** 31 | * Returns utm params from array of parameters. 32 | * 33 | * @param string[] $params 34 | * @return string[] 35 | */ 36 | public static function extractUtmParameters(array $params): array 37 | { 38 | $utmParams = ['utm_source', 'utm_campaign', 'utm_medium']; 39 | 40 | return array_intersect_key($params, array_flip($utmParams)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - '#::__construct\(\) does not call parent constructor from Nette\\Application\\UI\\Presenter#' 4 | - '#::__construct\(\) does not call parent constructor from Nette\\ComponentModel\\Component#' 5 | - '#Parameter \#2 \$count of method Kdyby\\Translation\\Translator::translate\(\) expects int\|null, Nette\\Utils\\Html|mixed|string|string\[\] given#' 6 | - '#Call to an undefined method SendGrid\\Client::mail\(\)#' 7 | - '#Access to undefined constant App\\Modules\\Core\\Model\\EventSources\\AbstractEventSource::URLS#' 8 | - '#Parameter \#1 \$eventTagRow of static method App\\Modules\\Core\\Model\\Entity\\EventTag::createFromRow\(\) expects Nette\\Database\\Table\\ActiveRow, Nette\\Database\\Table\\IRow given#' 9 | - '#Property Drahak\\Restful\\Application\\UI\\ResourcePresenter::\$resource \(Drahak\\Restful\\IResource\) does not accept App\\Modules\\Core\\Model\\Entity\\Event\[\]#' 10 | - '#Access to an undefined property Nette\\Database\\Table\\Selection::\$id#' 11 | 12 | includes: 13 | - vendor/phpstan/phpstan-nette/extension.neon 14 | - vendor/phpstan/phpstan-nette/rules.neon 15 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/AbstractIterator.php: -------------------------------------------------------------------------------- 1 | data = array_values($data); 26 | } 27 | 28 | public function current(): ?IRow 29 | { 30 | return $this->valid() ? $this->data[$this->index] : null; 31 | } 32 | 33 | public function next(): ?IRow 34 | { 35 | ++$this->index; 36 | 37 | return $this->current(); 38 | } 39 | 40 | public function key(): int 41 | { 42 | return $this->index; 43 | } 44 | 45 | public function valid(): bool 46 | { 47 | return array_key_exists($this->index, $this->data); 48 | } 49 | 50 | public function rewind(): void 51 | { 52 | $this->index = 0; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /db/migrations/20160322091004_users_events_series_and_organisers.php: -------------------------------------------------------------------------------- 1 | table('users_events_series', ['id' => false, 'primary_key' => ['user_id', 'event_series_id']]) 11 | ->addColumn('user_id', 'integer', ['signed' => false]) 12 | ->addColumn('event_series_id', 'integer', ['signed' => false]) 13 | ->addForeignKey('user_id', 'users', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 14 | ->addForeignKey('event_series_id', 'events_series', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 15 | ->create(); 16 | 17 | // Users follow organisers 18 | $this->table('users_organisers', ['id' => false, 'primary_key' => ['user_id', 'organiser_id']]) 19 | ->addColumn('user_id', 'integer', ['signed' => false]) 20 | ->addColumn('organiser_id', 'integer', ['signed' => false]) 21 | ->addForeignKey('user_id', 'users', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 22 | ->addForeignKey('organiser_id', 'organisers', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 23 | ->create(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Modules/Front/Presenters/SignPresenter.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 33 | } 34 | 35 | public function actionOut(): void 36 | { 37 | if (! $this->getUser()->isLoggedIn()) { 38 | $this->redirect('Homepage:'); 39 | } 40 | 41 | $this->getUser()->logout(true); 42 | 43 | $this->flashMessage($this->translator->translate('front.sign.out', [ 44 | 'bye' => self::BYE[array_rand(self::BYE)], 45 | ])); 46 | $this->redirect('Homepage:'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /db/migrations/20160421173837_newsletter_refactoring.php: -------------------------------------------------------------------------------- 1 | table('users_newsletters')->drop(); 10 | $this->table('newsletters')->drop(); 11 | $this->table('newsletters_contents')->drop(); 12 | $this->table('newsletters_layouts')->drop(); 13 | 14 | $this->table('users_newsletters', ['id' => false, 'primary_key' => 'id']) 15 | ->addColumn('id', 'integer', ['signed' => false, 'identity' => true]) 16 | ->addColumn('from', 'string', ['length' => 254]) 17 | ->addColumn('subject', 'string', ['length' => 100]) 18 | ->addColumn('content', 'text') 19 | ->addColumn('hash', 'string', ['length' => 32, 'null' => true]) 20 | ->addColumn('user_id', 'integer', ['signed' => false]) 21 | ->addColumn('sent', 'datetime', ['null' => true]) 22 | ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) 23 | ->addForeignKey('user_id', 'users', 'id', ['update' => 'CASCADE', 'delete' => 'CASCADE']) 24 | ->create(); 25 | 26 | $this->table('users') 27 | ->addColumn('newsletter', 'boolean', ['after' => 'token', 'default' => true]) 28 | ->update(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/Entity/EventTag.php: -------------------------------------------------------------------------------- 1 | event = $event; 27 | $this->tag = $tag; 28 | $this->rate = $rate; 29 | } 30 | 31 | public static function createFromRow(ActiveRow $eventTagRow): EventTag 32 | { 33 | return new self( 34 | Event::createFromRow($eventTagRow->ref('events', 'event_id')), 35 | Tag::createFromRow($eventTagRow->ref('tags', 'tag_id')), 36 | $eventTagRow['rate'] 37 | ); 38 | } 39 | 40 | public function getEvent(): Event 41 | { 42 | return $this->event; 43 | } 44 | 45 | public function getTag(): Tag 46 | { 47 | return $this->tag; 48 | } 49 | 50 | public function getRate(): int 51 | { 52 | return $this->rate; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/config/services.neon: -------------------------------------------------------------------------------- 1 | services: 2 | router: App\Router\RouterFactory::createRouter 3 | - App\Modules\Core\Model\EventModel 4 | - App\Modules\Core\Model\TagModel 5 | - App\Modules\Core\Model\UserModel 6 | - App\Modules\Core\Model\UserTagModel 7 | - App\Modules\Newsletter\Model\NewsletterService 8 | - SendGrid(%SendGrid.ApiKey%) 9 | - App\Modules\Newsletter\Model\UserNewsletterModel 10 | - App\Modules\Newsletter\Model\NewsletterModel 11 | - App\Modules\Core\Model\EventTagModel 12 | - App\Modules\Admin\Model\EventService 13 | - App\Modules\Admin\Model\Authenticator 14 | - App\Modules\Core\Model\EventRedirectModel 15 | - App\Modules\Core\Model\TagGroupModel 16 | - App\Modules\Core\Model\EventSources\Facebook\FacebookEventSource 17 | - App\Modules\Core\Model\OrganiserModel 18 | - App\Modules\Core\Model\EventSeriesModel 19 | - App\Modules\Admin\Model\OrganiserService 20 | - App\Modules\Admin\Model\SourceModel 21 | - App\Modules\Admin\Model\SourceService 22 | - App\Modules\Email\Model\EmailService 23 | - App\Modules\Core\Model\EventSources\Srazy\SrazyEventSource 24 | - 25 | class: App\Modules\Core\Model\EventSources\Meetup\MeetupEventSource 26 | setup: 27 | - setApiKey(%meetup.apiKey%) 28 | - App\Modules\Api\V1\Model\EventApiService 29 | - App\Modules\Core\Model\CountryModel 30 | -------------------------------------------------------------------------------- /app/Modules/Admin/Console/CrawlSourcesCommand.php: -------------------------------------------------------------------------------- 1 | sourceService = $sourceService; 20 | parent::__construct(); 21 | } 22 | 23 | protected function configure(): void 24 | { 25 | $this->setName('admin:crawlSources') 26 | ->setDescription('Crawl events from sources'); 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output): int 30 | { 31 | $addedEvents = $this->sourceService->crawlSources(); 32 | 33 | if ($addedEvents) { 34 | $output->writeln($addedEvents . ' new events has been added to be approved'); 35 | } else { 36 | $output->writeln('No new events has been added'); 37 | } 38 | 39 | return 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Modules/Core/Presenters/ErrorPresenter.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 23 | } 24 | 25 | public function run(Request $request): IResponse 26 | { 27 | $e = $request->getParameter('exception'); 28 | 29 | if ($e instanceof BadRequestException) { 30 | // $this->logger->log("HTTP code {$e->getCode()}: 31 | // {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", 'access'); 32 | return new ForwardResponse($request->setPresenterName('Core:Error4xx')); 33 | } 34 | 35 | $this->logger->log($e, ILogger::EXCEPTION); 36 | 37 | return new CallbackResponse(function (): void { 38 | require __DIR__ . '/templates/Error/500.phtml'; 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /www/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 20 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/templates/Events/create.latte: -------------------------------------------------------------------------------- 1 | {layout '../@layout.latte'} 2 | 3 | {block header} 4 | {if $presenter->getAction() === 'create'} 5 | {="admin.events.create.header"|translate} 6 | {elseif $presenter->getAction() === 'approve'} 7 | {="admin.events.approve.header"|translate} 8 | {else} 9 | {="admin.events.update.header"|translate} 10 | {/if} 11 | {/block} 12 | 13 | {block content} 14 |
15 |
16 | {control eventForm} 17 |
18 |
19 | {/block} 20 | 21 | {block scripts} 22 | 43 | {/block} 44 | -------------------------------------------------------------------------------- /app/Modules/Newsletter/Console/CreateNewsletterCommand.php: -------------------------------------------------------------------------------- 1 | newsletterService = $newsletterService; 20 | parent::__construct(); 21 | } 22 | 23 | protected function configure(): void 24 | { 25 | $this->setName('newsletters:create') 26 | ->setDescription('Creates default newsletter'); 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output): int 30 | { 31 | $result = $this->newsletterService->createDefaultNewsletter(); 32 | if ($result) { 33 | $output->writeln('New newsletter id: ' . $result . ''); 34 | 35 | return 0; 36 | } 37 | 38 | $output->writeln('Could not create newsletter '); 39 | 40 | return 1; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Modules/Admin/Console/GeneratePasswordCommand.php: -------------------------------------------------------------------------------- 1 | userModel = $userModel; 21 | parent::__construct(); 22 | } 23 | 24 | protected function configure(): void 25 | { 26 | $this->setName('admin:generatePassword') 27 | ->setDescription('Generate admin password') 28 | ->addArgument( 29 | 'password', 30 | InputArgument::REQUIRED, 31 | 'Raw password' 32 | ); 33 | } 34 | 35 | protected function execute(InputInterface $input, OutputInterface $output): int 36 | { 37 | $password = (string) $input->getArgument('password'); 38 | 39 | $hash = $this->userModel->hashAndEncrypt($password); 40 | 41 | $output->writeln($hash); 42 | 43 | return 0; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /www/css/admin.css: -------------------------------------------------------------------------------- 1 | #main { 2 | display: block; 3 | } 4 | #main form th.th-label { 5 | padding: 0.6rem 1rem 1rem 0rem; 6 | text-align: right; 7 | } 8 | #main form td.td-control input, 9 | #main form td.td-control textarea { 10 | width: 100%; 11 | } 12 | #main form td.td-control input[type=checkbox] { 13 | width: inherit; 14 | margin-right: 1rem; 15 | } 16 | #main form td.td-control textarea { 17 | border: 1px solid #ccc; 18 | } 19 | #main form td.td-control .select2-container--default .select2-selection--single { 20 | border: 1px solid #ccc; 21 | } 22 | #main form fieldset { 23 | border: none; 24 | display: inline-block; 25 | vertical-align: top; 26 | } 27 | #main .pure-box { 28 | margin-bottom: 20px; 29 | } 30 | #frm-signInForm-form table { 31 | margin: auto; 32 | } 33 | .info-box-content .info-box-text.inline, 34 | .info-box-content .info-box-number.inline { 35 | text-transform: inherit; 36 | display: inline; 37 | } 38 | .login-logo, 39 | .logo-lg { 40 | font-weight: 400; 41 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 42 | -webkit-font-smoothing: antialiased; 43 | } 44 | table.table a.btn { 45 | margin-left: 0.2rem; 46 | margin-right: 0.2rem; 47 | } 48 | #image-preview { 49 | width: 360px; 50 | height: 130px; 51 | } 52 | #image-preview img { 53 | object-fit: cover; 54 | width: 100%; 55 | height: 100%; 56 | } 57 | -------------------------------------------------------------------------------- /app/Modules/Front/Presenters/RedirectPresenter.php: -------------------------------------------------------------------------------- 1 | eventRedirectModel = $eventRedirectModel; 25 | $this->eventModel = $eventModel; 26 | } 27 | 28 | public function renderDefault(string $url): void 29 | { 30 | // Find event with same url 31 | $events = $this->eventModel->getAll()->where('origin_url', $url)->fetchAll(); 32 | 33 | foreach ($events as $event) { 34 | // Count the redirect 35 | $this->eventRedirectModel->insert([ 36 | 'event_id' => $event->id, 37 | 'user_id' => $this->getUser()->isLoggedIn() ? $this->getUser()->getId() : null, 38 | ]); 39 | } 40 | 41 | $this->redirectUrl(new Url($url)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /www/css/admin.less: -------------------------------------------------------------------------------- 1 | @inputBorder: #ccc; 2 | 3 | #main { 4 | display: block; 5 | 6 | form { 7 | th.th-label { 8 | padding: 0.6rem 1rem 1rem 0rem; 9 | text-align: right; 10 | } 11 | td.td-control { 12 | input, textarea { 13 | width: 100%; 14 | } 15 | input[type=checkbox] { 16 | width: inherit; 17 | margin-right: 1rem; 18 | } 19 | textarea { 20 | border: 1px solid @inputBorder; 21 | } 22 | .select2-container--default .select2-selection--single { 23 | border: 1px solid @inputBorder; 24 | } 25 | } 26 | 27 | fieldset { 28 | border: none; 29 | display: inline-block; 30 | vertical-align: top; 31 | } 32 | } 33 | 34 | .pure-box { 35 | margin-bottom: 20px; 36 | } 37 | } 38 | 39 | #frm-signInForm-form table { 40 | margin: auto; 41 | } 42 | 43 | .info-box-content { 44 | .info-box-text.inline, 45 | .info-box-number.inline { 46 | text-transform: inherit; 47 | display: inline; 48 | } 49 | } 50 | 51 | .login-logo, 52 | .logo-lg { 53 | font-weight: 400; 54 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 55 | -webkit-font-smoothing: antialiased; 56 | } 57 | 58 | table.table { 59 | a.btn { 60 | margin-left: 0.2rem; 61 | margin-right: 0.2rem; 62 | } 63 | } 64 | 65 | #image-preview { 66 | width: 360px; 67 | height: 130px; 68 | img { 69 | object-fit: cover; 70 | width: 100%; 71 | height: 100%; 72 | } 73 | } -------------------------------------------------------------------------------- /app/Modules/Newsletter/Model/NewsletterModel.php: -------------------------------------------------------------------------------- 1 | getAll() 27 | ->order('created DESC') 28 | ->limit(1); 29 | 30 | if ($newsletter->count() !== 0) { 31 | return $newsletter->fetch()->toArray(); 32 | } 33 | 34 | throw new RuntimeException('No newsletters found'); 35 | } 36 | 37 | public function createNewsletter(Newsletter $newsletter): IRow 38 | { 39 | return $this->insert([ 40 | 'subject' => $newsletter->getSubject(), 41 | 'from' => $newsletter->getFrom(), 42 | 'intro_text' => $newsletter->getIntroText(), 43 | 'outro_text' => $newsletter->getOutroText(), 44 | 'author' => $newsletter->getAuthor(), 45 | ]); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/Modules/Core/Components/AbstractBaseControl.php: -------------------------------------------------------------------------------- 1 | template->render(); 21 | } 22 | 23 | protected function createTemplate(): Template 24 | { 25 | /** @var Template $template */ 26 | $template = parent::createTemplate(); 27 | 28 | if ($file = $this->getTemplateDefaultFile()) { 29 | $template->setFile($file); 30 | } 31 | 32 | $template->addFilter('datetime', function (NetteDateTime $a, ?NetteDateTime $b = null) { 33 | DateTime::setTranslator($this->translator); 34 | 35 | return DateTime::eventsDatetimeFilter($a, $b); 36 | }); 37 | 38 | return $template; 39 | } 40 | 41 | /** 42 | * Derives template path from class name. 43 | */ 44 | protected function getTemplateDefaultFile(): ?string 45 | { 46 | $refl = $this->getReflection(); 47 | $file = dirname($refl->getFileName()) . '/' . $refl->getShortName() . '.latte'; 48 | 49 | return file_exists($file) ? $file : null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/DashboardPresenter.php: -------------------------------------------------------------------------------- 1 | eventModel = $eventModel; 18 | } 19 | 20 | public function actionDefault(): void 21 | { 22 | $this->template->approvedEventsCounts = $this->eventModel->getAll() 23 | ->select('COUNT(*) AS all') 24 | ->select('COUNT(IF(MONTH(start) = MONTH(?), 1, NULL)) AS thisMonth', $datetime = new DateTime) 25 | ->select('COUNT(IF(MONTH(start) = MONTH(?), 1, NULL)) AS nextMonth', $datetime->modifyClone('+1 MONTH')) 26 | ->where('start >= ?', $datetime) 27 | ->where('state = ?', EventModel::STATE_APPROVED) 28 | ->fetch(); 29 | 30 | $this->template->notApprovedEventsCounts = $this->eventModel->getAll() 31 | ->select('COUNT(*) AS all') 32 | ->select('COUNT(IF(MONTH(start) = MONTH(?), 1, NULL)) AS thisMonth', $datetime = new DateTime) 33 | ->select('COUNT(IF(MONTH(start) = MONTH(?), 1, NULL)) AS nextMonth', $datetime->modifyClone('+1 MONTH')) 34 | ->where('start >= ?', $datetime) 35 | ->where('state = ?', EventModel::STATE_NOT_APPROVED) 36 | ->fetch(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Modules/Newsletter/Console/SendNewslettersCommand.php: -------------------------------------------------------------------------------- 1 | newsletterService = $newsletterService; 26 | $this->userNewsletterModel = $userNewsletterModel; 27 | parent::__construct(); 28 | } 29 | 30 | protected function configure(): void 31 | { 32 | $this->setName('newsletters:send') 33 | ->setDescription('Send newsletters'); 34 | } 35 | 36 | protected function execute(InputInterface $input, OutputInterface $output): int 37 | { 38 | $usersNewslettersIds = $this->userNewsletterModel->getAll() 39 | ->where('sent', null) 40 | ->fetchPairs(null, 'id'); 41 | 42 | $this->newsletterService->sendNewsletters($usersNewslettersIds); 43 | 44 | $output->writeln(count($usersNewslettersIds) . ' newsletters have been sent'); 45 | 46 | return 0; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Modules/Email/Model/Entity/Email.php: -------------------------------------------------------------------------------- 1 | from; 34 | } 35 | 36 | public function setFrom(string $from): self 37 | { 38 | $this->from = $from; 39 | 40 | return $this; 41 | } 42 | 43 | public function getTo(): string 44 | { 45 | return $this->to; 46 | } 47 | 48 | public function setTo(string $to): self 49 | { 50 | $this->to = $to; 51 | 52 | return $this; 53 | } 54 | 55 | public function getSubject(): string 56 | { 57 | return $this->subject; 58 | } 59 | 60 | public function setSubject(string $subject): self 61 | { 62 | $this->subject = $subject; 63 | 64 | return $this; 65 | } 66 | 67 | public function getBody(): string 68 | { 69 | return $this->body; 70 | } 71 | 72 | public function setBody(string $body): self 73 | { 74 | $this->body = $body; 75 | 76 | return $this; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/Modules/Email/Presenters/EmailPresenter.php: -------------------------------------------------------------------------------- 1 | emailService = $emailService; 34 | } 35 | 36 | /** 37 | * Renders login email with provided user token. 38 | * 39 | * @throws \InvalidArgumentException 40 | * @throws \Nette\Application\UI\InvalidLinkException 41 | * @throws \Nette\InvalidArgumentException 42 | */ 43 | public function renderLogin(string $token): void 44 | { 45 | /** @var ITemplate|Template $template */ 46 | $template = $this->getTemplate(); 47 | $template->getLatte()->setLoader(new StringLoader); // @todo: what is this for? 48 | $template->setFile($this->emailService->renderLoginEmail($token)); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/Modules/Newsletter/Console/RenderNewslettersCommand.php: -------------------------------------------------------------------------------- 1 | newsletterService = $newsletterService; 26 | $this->userModel = $userModel; 27 | parent::__construct(); 28 | } 29 | 30 | protected function configure(): void 31 | { 32 | $this->setName('newsletters:render'); 33 | $this->setDescription('Render users newsletters prepared to send'); 34 | } 35 | 36 | protected function execute(InputInterface $input, OutputInterface $output): int 37 | { 38 | $users = $this->userModel->getAll() 39 | ->where('newsletter', true) 40 | ->fetchAll(); 41 | 42 | $createdCount = 0; 43 | 44 | foreach ($users as $user) { 45 | $this->newsletterService->createUserNewsletter($user->getPrimary()); 46 | ++$createdCount; 47 | } 48 | 49 | $output->writeln($createdCount . ' newsletters have been created'); 50 | 51 | return 0; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/AbstractBaseModel.php: -------------------------------------------------------------------------------- 1 | database = $database; 24 | } 25 | 26 | public function getAll(): Selection 27 | { 28 | return $this->database->table($this::TABLE_NAME); 29 | } 30 | 31 | /** 32 | * Inserts row in a table. 33 | * @param mixed[]|\Traversable|Selection array($column => $value)|\Traversable|Selection for INSERT ... SELECT 34 | * @return IRow|int|bool Returns IRow or number of affected rows for Selection or table without primary key 35 | */ 36 | public function insert(iterable $data) 37 | { 38 | return $this->getAll() 39 | ->insert($data); 40 | } 41 | 42 | /** 43 | * Updates all rows in result set. 44 | * Joins in UPDATE are supported only in MySQL. 45 | * @param mixed[] ($column => $value) 46 | * @return int number of affected rows 47 | */ 48 | public function update(iterable $data): int 49 | { 50 | return $this->getAll() 51 | ->update($data); 52 | } 53 | 54 | /** 55 | * @param mixed[] $data 56 | */ 57 | public function delete(array $data): void 58 | { 59 | $this->getAll()->where($data)->delete(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/Modules/Core/Utils/Filters.php: -------------------------------------------------------------------------------- 1 | fullname ?: $identity->email ?: self::$translator->translate('front.nav.user'); 42 | } 43 | 44 | /** 45 | * Inline filter used for CSS inline and UTF8 to HTML entities conversion (email clients compatibility). 46 | */ 47 | public static function inline(string $s, bool $stripTags = true): string 48 | { 49 | $output = Helper::utfToHtmlEntities($s); 50 | if ($stripTags) { 51 | $output = strip_tags($output); 52 | } 53 | 54 | return $output; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/Modules/Email/Model/Entity/BasicEmail.php: -------------------------------------------------------------------------------- 1 | introText; 36 | } 37 | 38 | public function setIntroText(string $introText): BasicEmail 39 | { 40 | $this->introText = $introText; 41 | 42 | return $this; 43 | } 44 | 45 | public function getButtonText(): string 46 | { 47 | return $this->buttonText; 48 | } 49 | 50 | public function setButtonText(string $buttonText): self 51 | { 52 | $this->buttonText = $buttonText; 53 | 54 | return $this; 55 | } 56 | 57 | public function getButtonUrl(): Url 58 | { 59 | return $this->buttonUrl; 60 | } 61 | 62 | public function setButtonUrl(Url $buttonUrl): self 63 | { 64 | $this->buttonUrl = $buttonUrl; 65 | 66 | return $this; 67 | } 68 | 69 | public function getFooterText(): string 70 | { 71 | return $this->footerText; 72 | } 73 | 74 | public function setFooterText(string $footerText): BasicEmail 75 | { 76 | $this->footerText = $footerText; 77 | 78 | return $this; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /db/migrations/20160318171343_organisers.php: -------------------------------------------------------------------------------- 1 | table('organisers', ['id' => false, 'primary_key' => 'id']) 11 | ->addColumn('id', 'integer', ['signed' => false, 'identity' => true]) 12 | ->addColumn('name', 'string', ['limit' => 255]) 13 | ->addColumn('url', 'string', ['limit' => 2083, 'null' => true]) 14 | ->addColumn('icon', 'string', ['limit' => 2083, 'null' => true]) 15 | ->addColumn('image', 'string', ['limit' => 2083, 'null' => true]) 16 | ->addColumn('updated', 'timestamp', ['null' => true, 'default' => null, 'update' => 'CURRENT_TIMESTAMP']) 17 | ->addColumn('created', 'timestamp', ['default' => 'CURRENT_TIMESTAMP']) 18 | ->addIndex('name') 19 | ->create(); 20 | 21 | // Create series 22 | $this->table('events_series', ['id' => false, 'primary_key' => 'id']) 23 | ->addColumn('id', 'integer', ['signed' => false, 'identity' => true]) 24 | ->addColumn('name', 'string', ['limit' => 255, 'null' => true]) 25 | ->addColumn('url', 'string', ['limit' => 2083, 'null' => true]) 26 | ->addColumn('icon', 'string', ['limit' => 2083, 'null' => true]) 27 | ->addColumn('image', 'string', ['limit' => 2083, 'null' => true]) 28 | ->addColumn('organiser_id', 'integer', ['signed' => false]) 29 | ->addForeignKey('organiser_id', 'organisers', 'id', ['update' => 'CASCADE']) 30 | ->create(); 31 | 32 | // Add foreign key for events 33 | $this->table('events') 34 | ->addColumn('event_series_id', 'integer', ['after' => 'rate', 'null' => true, 'signed' => false]) 35 | ->addForeignKey('event_series_id', 'events_series', 'id', ['update' => 'CASCADE']) 36 | ->update(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Modules/Admin/Components/SignIn/SignInForm.php: -------------------------------------------------------------------------------- 1 | render(); 25 | } 26 | 27 | public function processForm(Form $form): void 28 | { 29 | $values = $form->getValues(); 30 | 31 | try { 32 | $this->getPresenter()->getUser()->login(UserModel::ADMIN_LOGIN, $values->email, $values->password); 33 | } catch (AuthenticationException $e) { 34 | $this->onIncorrectLogIn(); 35 | } 36 | 37 | $this->onLoggedIn(); 38 | } 39 | 40 | protected function createComponentForm(): Form 41 | { 42 | $form = new Form; 43 | $form->setTranslator($this->translator->domain('admin.signInForm')); 44 | 45 | $form->addText('email', 'email') 46 | ->setAttribute('placeholder', 'email.placeholder') 47 | ->setRequired('email.required'); 48 | $form->addPassword('password', 'password') 49 | ->setAttribute('placeholder', 'password.placeholder') 50 | ->setRequired('password.required'); 51 | 52 | $form->addSubmit('signIn', 'signIn')->setAttribute('class', 'btn btn-success'); 53 | $form->onSuccess[] = [$this, 'processForm']; 54 | 55 | return $form; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/templates/Sources/default.latte: -------------------------------------------------------------------------------- 1 | {layout '../@layout.latte'} 2 | 3 | {block header} 4 | 5 | {/block} 6 | 7 | {block content} 8 |
9 | {="admin.sources.create.header"|translate} 10 |
11 | 12 |
13 |
14 | {="admin.sources.default.table.title"|translate} 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
{="admin.sources.default.table.name"|translate}{="admin.sources.default.table.nextCheck"|translate}
25 |
26 |
27 | {/block} 28 | 29 | {block scripts} 30 | 31 | 32 | 48 | {/block} -------------------------------------------------------------------------------- /app/Router/RouterFactory.php: -------------------------------------------------------------------------------- 1 | /[/]', 'Dashboard:default'); 19 | $router[] = $adminRouter; 20 | 21 | // Nesletter 22 | $newsletterRouter = new RouteList('Newsletter'); 23 | $newsletterRouter[] = new Route('newsletter/', 'Newsletter:default'); 24 | $newsletterRouter[] = new Route('newsletter/dynamic/', 'Newsletter:dynamic'); 25 | $newsletterRouter[] = new Route('newsletter//', 'Newsletter:default'); 26 | $router[] = $newsletterRouter; 27 | 28 | // Email 29 | $emailRouter = new RouteList('Email'); 30 | $emailRouter[] = new Route('email/login', 'Email:login'); 31 | $router[] = $emailRouter; 32 | 33 | // Api 34 | $router[] = new CrudRoute('api/v1/events', [ 35 | 'module' => 'Api:V1', 36 | 'presenter' => 'Events', 37 | ]); 38 | 39 | // Front 40 | $router[] = new Route('profile/settings/', 'Front:Profile:settings'); 41 | $router[] = new Route('discover/?', 'Front:Homepage:discover'); 42 | $router[] = new Route('/[/]', [ 43 | 'module' => 'Front', 44 | 'presenter' => 'Homepage', 45 | 'action' => 'default', 46 | ]); 47 | 48 | return $router; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eventigo/eventigo-web", 3 | "require": { 4 | "php": "^7.1", 5 | "nette/application": "~2.4", 6 | "nette/bootstrap": "~2.4", 7 | "nette/caching": "~2.4", 8 | "nette/database": "~2.4", 9 | "nette/di": "~2.4", 10 | "nette/finder": "~2.4", 11 | "nette/forms": "~2.4", 12 | "nette/http": "~2.4", 13 | "nette/mail": "~2.4", 14 | "nette/robot-loader": "^2.4|^3.0", 15 | "nette/security": "~2.4", 16 | "nette/utils": "~2.4", 17 | "latte/latte": "~2.4", 18 | "tracy/tracy": "~2.4", 19 | "kdyby/translation": "~2.2", 20 | "sendgrid/sendgrid": "^5.5", 21 | "kdyby/facebook": "2.0.x-dev", 22 | "pelago/emogrifier": "@dev", 23 | "greeny/nette-slack-logger": "^1.2", 24 | "webuni/srazy-api-client": "1.0.x-dev", 25 | "dms/meetup-api-client": "2.0.x-dev", 26 | "drahak/restful": "@dev", 27 | "contributte/console": "^0.1" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": "^6.4", 31 | "robmorgan/phinx": "^0.7", 32 | "phpstan/phpstan": "^0.8", 33 | "phpstan/phpstan-nette": "^0.8", 34 | "symplify/easy-coding-standard": "^2.5.5" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "App\\": "app" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "App\\Tests\\": "tests", 44 | "PHP_CodeSniffer\\": "vendor/squizlabs/php_codesniffer/src" 45 | } 46 | }, 47 | "scripts": { 48 | "complete-check": ["phpunit", "@cs", "@phpstan"], 49 | "cs": "vendor/bin/ecs check app", 50 | "fs": "vendor/bin/ecs check app --fix", 51 | "phpstan": "vendor/bin/phpstan analyse app --level 5 -c phpstan.neon" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/EventSources/Srazy/SrazyEventSource.php: -------------------------------------------------------------------------------- 1 | series()->get($series)->getEvents(); 29 | 30 | $events = []; 31 | foreach ($srazyEvents as $event) { 32 | $start = $event->getStart() ? DateTime::from($event->getStart()) : null; 33 | if ($start && $start > new DateTime) { 34 | $events[] = new Event( 35 | null, 36 | $event->getName() ?? '', 37 | $event->getDescription() ?? '', 38 | $event->getUri(), 39 | $start, 40 | $event->getEnd() ? DateTime::from($event->getEnd()) : null, 41 | null, 42 | null, 43 | null, // TODO $e->setImage() Get image from community page 44 | Event::calculateRateByAttendeesCount( 45 | count($event->getConfirmedAttendees()) + count($event->getUnconfirmedAttendees()) 46 | ) 47 | ); 48 | } 49 | } 50 | 51 | return $events; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Modules/Newsletter/Model/NewsletterServiceTest.php: -------------------------------------------------------------------------------- 1 | create(); 29 | $this->newsletterService = $container->getByType(NewsletterService::class); 30 | $this->linkGenerator = $container->getByType(LinkGenerator::class); 31 | 32 | $this->baseUrl = $container->getParameters()['baseUrl']; 33 | } 34 | 35 | public function testRedirectLinks() 36 | { 37 | $this->assertSame( 38 | $this->baseUrl . '/redirect/?url=someUrl', 39 | $this->linkGenerator->link('Front:Redirect:', ['someUrl']) 40 | ); 41 | } 42 | 43 | public function testPrepareLinks() 44 | { 45 | $templateData = []; 46 | $templateData = $this->newsletterService->prepareLinks( 47 | $templateData, 'userToken', 'baseUrl', 'newsletterHash' 48 | ); 49 | 50 | $this->assertSame([ 51 | 'updatePreferencesUrl' => $this->baseUrl . '/profile/settings/userToken?utm_campaign=newsletterButton&utm_source=newsletter&utm_medium=email', 52 | 'feedUrl' => $this->baseUrl . '/?token=userToken&utm_campaign=newsletterButton&utm_source=newsletter&utm_medium=email', 53 | 'unsubscribeUrl' => 'baseUrl/newsletter/unsubscribe/newsletterHash' 54 | ], $templateData); 55 | } 56 | } -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/SignPresenter.php: -------------------------------------------------------------------------------- 1 | signInFormFactory = $signInFormFactory; 25 | $this->translator = $translator; 26 | } 27 | 28 | public function actionIn(): void 29 | { 30 | if ($this->user->isLoggedIn() && $this->user->isInRole('admin')) { 31 | $this->redirect('Dashboard:'); 32 | } 33 | } 34 | 35 | public function actionOut(): void 36 | { 37 | if (! $this->user->isLoggedIn()) { 38 | $this->redirect('in'); 39 | } 40 | 41 | if ($this->user->isInRole('admin')) { 42 | $this->user->logout(true); 43 | } 44 | 45 | $this->redirect('in'); 46 | } 47 | 48 | protected function createComponentSignInForm(): SignInForm 49 | { 50 | $control = $this->signInFormFactory->create(); 51 | 52 | $control->onLoggedIn[] = function (): void { 53 | $this->redirect('Dashboard:'); 54 | }; 55 | 56 | $control->onIncorrectLogIn[] = function (): void { 57 | $this->flashMessage($this->translator->translate('admin.signInForm.incorrectLogIn'), 'danger'); 58 | $this->redirect('this'); 59 | }; 60 | 61 | return $control; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Modules/Front/Model/EventsIterator.php: -------------------------------------------------------------------------------- 1 | thisWeek && $this->current()->thisWeek) { 33 | return $this->thisWeek = true; 34 | } 35 | 36 | return false; 37 | } 38 | 39 | public function drawThisMonthTitle(): bool 40 | { 41 | if (! $this->thisMonth && $this->current()->thisMonth && ! $this->current()->thisWeek) { 42 | return $this->thisMonth = true; 43 | } 44 | 45 | return false; 46 | } 47 | 48 | public function drawNextMonthTitle(): bool 49 | { 50 | if (! $this->nextMonth && $this->current()->nextMonth 51 | && ! $this->current()->thisWeek && ! $this->current()->thisMonth 52 | ) { 53 | return $this->nextMonth = true; 54 | } 55 | 56 | return false; 57 | } 58 | 59 | public function drawUpcomingTitle(): bool 60 | { 61 | if (! $this->upcoming && ! $this->current()->thisWeek 62 | && ! $this->current()->thisMonth && ! $this->current()->nextMonth 63 | ) { 64 | return $this->upcoming = true; 65 | } 66 | 67 | return false; 68 | } 69 | 70 | public function count(): int 71 | { 72 | return count($this->data); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/SubscriptionTags/SubscriptionTags.latte: -------------------------------------------------------------------------------- 1 | {form form, class => ajax} 2 |
3 | {="front.subscription.form.email.label"|translate|noescape} 4 | {input email} 5 | {input real_subscribe} 6 | {input subscribe, class => ['btn','btn-success']} 7 |
8 |
9 | {="front.homepage.tags.header"|translate|noescape} 10 |
11 |
12 | {* Top tags group *} 13 | {var $top = isset($form[tags]->getComponents()[top]) ? $form[tags]->getComponents()[top] : null} 14 | {if $top} 15 |
    16 |
  • 17 | 20 |
  • 21 |
22 | {/if} 23 | 24 | {* Tags groups *} 25 | {foreach $form[tags]->getComponents() as $tagGroupName => $tagsGroup} 26 | {php if ($tagGroupName === 'top') continue;} 27 |

28 | {ifset $tagsGroups[$tagGroupName]} 29 | 30 | {/ifset} 31 | {="tags.tagGroup." . $tagGroupName|translate}

32 |
    33 |
  • 34 | 37 |
  • 38 |
39 | {/foreach} 40 |
41 | {/form} 42 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/TagModel.php: -------------------------------------------------------------------------------- 1 | getAll() 20 | ->select('tags.code') 21 | ->select('tags.name') 22 | ->select('COUNT(:events_tags.event_id) AS eventsCount') 23 | ->select('tag_group_id') 24 | ->select('tag_group.name AS tagGroupName') 25 | ->select('tags.id') 26 | ->where(':events_tags.event.start >= NOW()'); 27 | 28 | if (! $showAbroad) { 29 | $selection 30 | ->where([':events_tags.event.country_id = ? OR :events_tags.event.country_id IS NULL' => 'CZ']); 31 | } 32 | 33 | return $selection->group('tag_group_id, tags.id') 34 | ->order('tag_group_id') 35 | ->order('eventsCount DESC') 36 | ->order('tags.name'); 37 | } 38 | 39 | /** 40 | * Get tags with the most upcoming events and all others. 41 | */ 42 | public function getAllByMostEvents(): Selection 43 | { 44 | return $this->getAll() 45 | ->select('tags.code') 46 | ->select('tags.name') 47 | ->select('COUNT(IF(:events_tags.event_id IS NOT NULL AND :events_tags.event.start >= NOW(),' 48 | . ' TRUE, NULL)) AS eventsCount') 49 | ->select('tag_group_id') 50 | ->select('tag_group.name AS tagGroupName') 51 | ->select('tags.id') 52 | ->group('tag_group_id, tags.id') 53 | ->order('tag_group_id') 54 | ->order('eventsCount DESC') 55 | ->order('tags.name'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/templates/Sign/@layout.latte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AdminLTE 2 | Log in 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 31 | 32 | 33 | 34 | 35 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/Sign/SignIn.php: -------------------------------------------------------------------------------- 1 | userModel = $userModel; 36 | $this->emailService = $emailService; 37 | } 38 | 39 | public function processForm(Form $form, ArrayHash $values): void 40 | { 41 | $user = $this->userModel->getUserByEmail($values->email); 42 | 43 | if (! $user) { 44 | $user = $this->userModel->subscribe($values->email); 45 | } 46 | 47 | $this->emailService->sendLogin($values->email, $user->token); 48 | $this->onSuccess($values->email); 49 | } 50 | 51 | protected function createComponentForm(): Form 52 | { 53 | $form = new Form; 54 | $form->setTranslator($this->translator->domain('front.signIn.form')); 55 | 56 | $form->addText('email', 'email') 57 | ->setAttribute('placeholder', 'email.placeholder') 58 | ->addRule(Form::EMAIL, 'email.invalid') 59 | ->setRequired('email.required'); 60 | $form->addSubmit('submit', 'submit') 61 | ->setAttribute('data-loading-text', $this->translator->translate('front.signIn.form.submit.sending')); 62 | 63 | $form->onSuccess[] = [$this, 'processForm']; 64 | 65 | return $form; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/Tags/Tags.latte: -------------------------------------------------------------------------------- 1 | {form form, class => ajax} 2 |
3 | {* Top tags group *} 4 | {var $top = isset($form[tags]->getComponents()[top]) ? $form[tags]->getComponents()[top] : null} 5 | {if $top} 6 |
    7 |
  • 8 | 11 |
  • 12 |
13 | {/if} 14 | 15 | {* Tags groups *} 16 | {foreach $form[tags]->getComponents() as $tagGroupName => $tagsGroup} 17 | {php if ($tagGroupName === 'top') continue;} 18 | 19 | {if $iterator->getCounter() % $columnsPerRow === 1 && $iterator->getCounter() > 1} 20 |
{* // div.row *} 21 | {/if} 22 | 23 |
24 |
25 |

26 | {ifset $tagsGroups[$tagGroupName]} 27 | 28 | {/ifset} 29 | {="tags.tagGroup." . $tagGroupName|translate}

30 |
    31 |
  • 32 | 36 |
  • 37 |
38 |
39 | {/foreach} 40 |
{* // div.row *} 41 | 42 | {/form} 43 | -------------------------------------------------------------------------------- /app/Modules/Api/V1/Model/EventApiService.php: -------------------------------------------------------------------------------- 1 | eventModel = $eventModel; 25 | $this->linkGenerator = $linkGenerator; 26 | } 27 | 28 | /** 29 | * @return Event[] 30 | */ 31 | public function getEvents(): array 32 | { 33 | $events = []; 34 | 35 | foreach ($this->eventModel->getAllWithDates([], new DateTime) as $eventRow) { 36 | $event = Event::createFromRow($eventRow); 37 | $eventTags = []; 38 | foreach ($this->eventModel->getEventTags($eventRow) as $eventTag) { 39 | $eventTags[] = [ 40 | 'code' => $eventTag->getTag()->getCode(), 41 | 'name' => $eventTag->getTag()->getName(), 42 | 'rate' => $eventTag->getRate(), 43 | ]; 44 | } 45 | 46 | $events[] = [ 47 | 'id' => $event->getHash(), 48 | 'name' => $event->getName(), 49 | 'description' => $event->getDescription(), 50 | 'url' => $this->linkGenerator->link('Front:Redirect:', [$event->getOriginUrl()]), 51 | 'start' => $event->getStart()->jsonSerialize(), 52 | 'end' => $event->getEnd() ? $event->getEnd()->jsonSerialize() : null, 53 | 'venue' => $event->getVenue(), 54 | 'country' => $event->getCountryCode(), 55 | 'image' => $event->getImage(), 56 | 'tags' => $eventTags, 57 | ]; 58 | } 59 | 60 | return $events; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Modules/Admin/Model/OrganiserService.php: -------------------------------------------------------------------------------- 1 | eventSeriesModel = $eventSeriesModel; 24 | $this->organiserModel = $organiserModel; 25 | } 26 | 27 | /** 28 | * @return array|\Nette\Database\Table\IRow[] 29 | */ 30 | public function getOrganisersSeries(): array 31 | { 32 | return $this->organiserModel->getAll() 33 | ->select('organisers.name AS organiser') 34 | ->select(':events_series.name AS series') 35 | ->select(':events_series.id AS seriesId') 36 | ->fetchAll(); 37 | } 38 | 39 | /** 40 | * @param array|\Nette\Database\Table\IRow[] $series 41 | * @return array|\Nette\Database\Table\IRow[] 42 | */ 43 | public static function formatSeriesForSelect(array $series): array 44 | { 45 | $result = []; 46 | 47 | foreach ($series as $item) { 48 | $result[$item->seriesId] = $item->organiser 49 | . ($item->series && $item->organiser !== $item->series 50 | ? ': ' . $item->series 51 | : ''); 52 | } 53 | 54 | return $result; 55 | } 56 | 57 | public function createOrganiser(string $name, string $url): IRow 58 | { 59 | $organiser = $this->organiserModel->insert([ 60 | 'name' => $name, 61 | ]); 62 | 63 | $this->eventSeriesModel->insert([ 64 | 'organiser_id' => $organiser->id, 65 | 'name' => $name, 66 | 'url' => $url, 67 | ]); 68 | 69 | return $organiser; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /www/flags/flags.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Generated with CSS Flag Sprite generator (https://www.flag-sprites.com/) 3 | */.flag{display:inline-block;width:16px;height:11px;background:url('flags.png') no-repeat}.flag.flag-es{background-position:-80px -11px}.flag.flag-it{background-position:-16px -33px}.flag.flag-al{background-position:0 0}.flag.flag-hu{background-position:-80px -22px}.flag.flag-cz{background-position:-16px -11px}.flag.flag-me{background-position:-16px -44px}.flag.flag-ie{background-position:-96px -22px}.flag.flag-md{background-position:0 -44px}.flag.flag-eu{background-position:-96px -11px}.flag.flag-be{background-position:-48px 0}.flag.flag-hr{background-position:-64px -22px}.flag.flag-lt{background-position:-48px -33px}.flag.flag-si{background-position:-80px -55px}.flag.flag-ro{background-position:-16px -55px}.flag.flag-lu{background-position:-64px -33px}.flag.flag-fi{background-position:0 -22px}.flag.flag-pt{background-position:0 -55px}.flag.flag-ba{background-position:-32px 0}.flag.flag-ru{background-position:-48px -55px}.flag.flag-mk{background-position:-32px -44px}.flag.flag-by{background-position:-96px 0}.flag.flag-de{background-position:-32px -11px}.flag.flag-at{background-position:-16px 0}.flag.flag-mt{background-position:-48px -44px}.flag.flag-nl{background-position:-64px -44px}.flag.flag-se{background-position:-64px -55px}.flag.flag-li{background-position:-32px -33px}.flag.flag-rs{background-position:-32px -55px}.flag.flag-xk{background-position:-32px -66px}.flag.flag-ua{background-position:0 -66px}.flag.flag-no{background-position:-80px -44px}.flag.flag-gb{background-position:-32px -22px}.flag.flag-ch{background-position:0 -11px}.flag.flag-pl{background-position:-96px -44px}.flag.flag-mc{background-position:-96px -33px}.flag.flag-is{background-position:0 -33px}.flag.flag-lv{background-position:-80px -33px}.flag.flag-ee{background-position:-64px -11px}.flag.flag-dk{background-position:-48px -11px}.flag.flag-sk{background-position:-96px -55px}.flag.flag-bg{background-position:-64px 0}.flag.flag-fr{background-position:-16px -22px}.flag.flag-br{background-position:-80px 0}.flag.flag-gr{background-position:-48px -22px}.flag.flag-us{background-position:-16px -66px} -------------------------------------------------------------------------------- /app/Modules/Core/Model/EventSources/Meetup/MeetupEventSource.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 29 | } 30 | 31 | /** 32 | * Get upcoming events of the page. 33 | * @return Event[] 34 | */ 35 | public function getEvents(string $group): array 36 | { 37 | /** @var MultiResultResponse|MeetupKeyAuthClient $client */ 38 | $client = MeetupKeyAuthClient::factory(['key' => $this->apiKey]); 39 | $groupEvents = $client->getGroupEvents(['urlname' => $group])->getData(); 40 | 41 | $groupData = $client->getGroup(['urlname' => $group])->getData(); 42 | $groupPhoto = isset($groupData['group_photo']) ? $groupData['group_photo']['photo_link'] : null; 43 | 44 | $events = []; 45 | foreach ($groupEvents as $event) { 46 | if ($event['status'] === 'upcoming') { 47 | $events[] = new Event( 48 | null, 49 | $event['name'], 50 | $event['description'] ?? '', 51 | $event['link'], 52 | DateTime::from($event['time'] / 1000), 53 | null, 54 | null, 55 | null, 56 | $groupPhoto ?? null, 57 | Event::calculateRateByAttendeesCount($event['yes_rsvp_count'] + $event['waitlist_count']) 58 | ); 59 | } 60 | } 61 | 62 | return $events; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Modules/Admin/Model/Authenticator.php: -------------------------------------------------------------------------------- 1 | userModel = $userModel; 22 | } 23 | 24 | /** 25 | * @param string[] $credentials 26 | * 27 | * @throws AuthenticationException 28 | */ 29 | public function authenticate(array $credentials): Identity 30 | { 31 | @[$type, $email, $password] = $credentials; // Password is optional, suppress the notice 32 | 33 | // Get user with same email 34 | $user = $this->userModel->getAll()->where([ 35 | 'email' => $email, 36 | ])->fetch(); 37 | 38 | if ($user !== false) { 39 | switch ($type) { 40 | case UserModel::SUBSCRIPTION_LOGIN: 41 | return new Identity($user->id, null, $user->toArray()); 42 | case UserModel::ADMIN_LOGIN: 43 | return $this->logToAdmin($user, $password); 44 | default: 45 | throw new AuthenticationException('Unsupported login type', IAuthenticator::FAILURE); 46 | } 47 | } else { 48 | throw new AuthenticationException( 49 | 'User with this email and password has not been found', 50 | IAuthenticator::IDENTITY_NOT_FOUND 51 | ); 52 | } 53 | } 54 | 55 | private function logToAdmin(ActiveRow $user, string $password): Identity 56 | { 57 | if (Passwords::verify($password, $user['password'])) { 58 | return new Identity($user['id'], ['admin'], $user->toArray()); 59 | } 60 | 61 | throw new AuthenticationException('Incorrect credentials', IAuthenticator::INVALID_CREDENTIAL); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Modules/Newsletter/Model/Entity/Newsletter.php: -------------------------------------------------------------------------------- 1 | id; 47 | } 48 | 49 | public function getSubject(): string 50 | { 51 | return $this->subject; 52 | } 53 | 54 | public function getFrom(): string 55 | { 56 | return $this->from; 57 | } 58 | 59 | public function getIntroText(): string 60 | { 61 | return $this->introText; 62 | } 63 | 64 | public function getOutroText(): string 65 | { 66 | return $this->outroText; 67 | } 68 | 69 | public function getAuthor(): string 70 | { 71 | return $this->author; 72 | } 73 | 74 | public function getCreated(): DateTime 75 | { 76 | return $this->created; 77 | } 78 | 79 | public function setSubject(string $subject): void 80 | { 81 | $this->subject = $subject; 82 | } 83 | 84 | public function setFrom(string $from): void 85 | { 86 | $this->from = $from; 87 | } 88 | 89 | public function setIntroText(string $introText): void 90 | { 91 | $this->introText = $introText; 92 | } 93 | 94 | public function setOutroText(string $outroText): void 95 | { 96 | $this->outroText = $outroText; 97 | } 98 | 99 | public function setAuthor(string $author): void 100 | { 101 | $this->author = $author; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/Modules/Core/Presenters/AbstractBasePresenter.php: -------------------------------------------------------------------------------- 1 | translator); 35 | $template->addFilter(null, [Filters::class, 'loader']); 36 | 37 | return $template; 38 | } 39 | 40 | protected function beforeRender(): void 41 | { 42 | parent::beforeRender(); 43 | 44 | $this->template->parameters = $this->context->getParameters(); 45 | } 46 | 47 | /** 48 | * Log in the user by token (usually provided in url). 49 | * 50 | * @throws BadRequestException 51 | */ 52 | protected function loginWithToken(?string $token = null): void 53 | { 54 | if (! $this->getUser()->isLoggedIn()) { 55 | if ($token === null || ($user = $this->userModel->getAll()->where('token', $token)->fetch()) === false 56 | ) { 57 | Debugger::log(sprintf( 58 | 'Invalid user token "%s". Can not login.', 59 | $token 60 | )); 61 | 62 | throw new BadRequestException; 63 | } 64 | 65 | try { 66 | $this->getUser()->login(new Identity($user->id, null, $user->toArray())); 67 | } catch (AuthenticationException $e) { 68 | Debugger::log($e->getMessage(), ILogger::EXCEPTION); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /easy-coding-standard.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/symplify/easy-coding-standard/config/symfony-checkers.neon 3 | - vendor/symplify/easy-coding-standard/config/symfony-risky-checkers.neon 4 | - vendor/symplify/easy-coding-standard/config/php54-checkers.neon 5 | - vendor/symplify/easy-coding-standard/config/php70-checkers.neon 6 | - vendor/symplify/easy-coding-standard/config/php71-checkers.neon 7 | - vendor/symplify/easy-coding-standard/config/symplify-checkers.neon 8 | - vendor/symplify/easy-coding-standard/config/common.neon 9 | - vendor/symplify/easy-coding-standard/config/spaces.neon 10 | 11 | checkers: 12 | # Metrics 13 | PHP_CodeSniffer\Standards\Generic\Sniffs\Files\LineLengthSniff: 14 | absoluteLineLimit: 120 15 | PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\CyclomaticComplexitySniff: 16 | absoluteComplexity: 7 17 | PHP_CodeSniffer\Standards\Generic\Sniffs\Metrics\NestingLevelSniff: 18 | absoluteNestingLevel: 4 19 | 20 | # Type Hints 21 | SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff: 22 | enableEachParameterAndReturnInspection: true 23 | 24 | # Namespaces 25 | SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff: 26 | allowPartialUses: false 27 | 28 | parameters: 29 | exclude_checkers: 30 | - PHP_CodeSniffer\Standards\PSR2\Sniffs\Methods\FunctionCallSignatureSniff 31 | - PhpCsFixer\Fixer\Operator\UnaryOperatorSpacesFixer 32 | - PhpCsFixer\Fixer\Phpdoc\PhpdocAlignFixer 33 | - PhpCsFixer\Fixer\PhpTag\BlankLineAfterOpeningTagFixer 34 | - PhpCsFixer\Fixer\Operator\NewWithBracesFixer 35 | # strict rule on this code 36 | - Symplify\CodingStandard\Fixer\Naming\PropertyNameMatchingTypeFixer 37 | - Symplify\CodingStandard\Sniffs\DependencyInjection\NoClassInstantiationSniff 38 | - Symplify\CodingStandard\Sniffs\Property\DynamicPropertySniff 39 | 40 | skip: 41 | SlevomatCodingStandard\Sniffs\Namespaces\ReferenceUsedNamesOnlySniff: 42 | - app/bootstrap.php 43 | 44 | skip_codes: 45 | - SlevomatCodingStandard\Sniffs\TypeHints\TypeHintDeclarationSniff.UselessDocComment 46 | # many todos 47 | - PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis\EmptyStatementSniff.DetectedCATCH 48 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/Settings/Settings.php: -------------------------------------------------------------------------------- 1 | user = $user; 32 | $this->userModel = $userModel; 33 | } 34 | 35 | public function render(): void 36 | { 37 | $this['form']->render(); 38 | } 39 | 40 | public function processForm(Form $form): void 41 | { 42 | $values = $form->getValues(); 43 | 44 | $this->userModel->getAll()->wherePrimary($this->user->getId())->update([ 45 | 'newsletter' => (bool) $values->newsletter, 46 | 'abroad_events' => (bool) $values->abroadEvents, 47 | ]); 48 | 49 | $this->onChange(); 50 | } 51 | 52 | protected function createComponentForm(): Form 53 | { 54 | $form = new Form; 55 | $form->setTranslator($this->translator->domain('front.profile.settings.main')); 56 | $form->getElementPrototype()->addClass('ajax'); 57 | 58 | // fa-envelope fa-envelope-o 59 | $form->addCheckbox( 60 | 'newsletter', 61 | Html::el('span class="label-icon-end"') 62 | ->addText($this->translator->translate('front.profile.settings.main.newsletter')) 63 | ->addHtml(Html::el('i class="fa fa-envelope"')) 64 | ); 65 | // fa-globe 66 | $form->addCheckbox('abroadEvents', 67 | Html::el('span class="label-icon-end"') 68 | ->addText($this->translator->translate('front.profile.settings.main.abroad')) 69 | ->addHtml(Html::el('i class="fa fa-globe"')) 70 | ); 71 | 72 | $form->onSuccess[] = [$this, 'processForm']; 73 | 74 | return $form; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/Subscription/Subscription.php: -------------------------------------------------------------------------------- 1 | userModel = $userModel; 31 | } 32 | 33 | /** 34 | * @throws EmailExistsException 35 | */ 36 | public function processForm(Form $form): void 37 | { 38 | $values = $form->getValues(); 39 | $user = $this->subscribe($values->email); 40 | $this->onSuccess($user->email); 41 | } 42 | 43 | protected function createComponentForm(): Form 44 | { 45 | $form = new Form; 46 | $form->setTranslator($this->translator->domain('front.subscription.form')); 47 | 48 | $form->addText('email', null, null, 254) 49 | ->setAttribute('placeholder', 'email.placeholder') 50 | ->setAttribute('type', 'email') 51 | ->addCondition(Form::FILLED) 52 | ->addRule(Form::EMAIL, 'email.validation'); 53 | 54 | $form->addSubmit('subscribe', 'subscribe.label'); 55 | 56 | $form->onSuccess[] = [$this, 'processForm']; 57 | 58 | return $form; 59 | } 60 | 61 | /** 62 | * @throws EmailExistsException 63 | * @throws \PDOException 64 | */ 65 | protected function subscribe(string $email): ?IRow 66 | { 67 | try { 68 | $user = $this->userModel->subscribe($email); 69 | 70 | if ($this->getReflection()->getName() === __CLASS__) { 71 | $this->onSuccess($user->email); 72 | } else { 73 | return $user; 74 | } 75 | } catch (EmailExistsException $e) { 76 | $this->onEmailExists($email); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/templates/Dashboard/default.latte: -------------------------------------------------------------------------------- 1 | {layout '../@layout.latte'} 2 | 3 | {block content} 4 | 5 |
6 |
7 |
8 | 9 |
10 | {="admin.dashboard.events.upcoming"|translate: $approvedEventsCounts->all} 11 | {="admin.dashboard.events.thisMonth"|translate}: 12 | {$approvedEventsCounts->thisMonth} 13 |
14 | {="admin.dashboard.events.nextMonth"|translate}: 15 | {$approvedEventsCounts->nextMonth} 16 |
17 |
18 |
19 |
20 |
21 | 26 | 27 | 28 |
29 | {="admin.dashboard.events.notApproved"|translate} 30 | {="admin.dashboard.events.thisMonth"|translate}: 31 | {$notApprovedEventsCounts->thisMonth} 32 |
33 | {="admin.dashboard.events.nextMonth"|translate}: 34 | {$notApprovedEventsCounts->nextMonth} 35 |
36 |
37 |
38 |
39 |
40 | {/block} -------------------------------------------------------------------------------- /app/Modules/Newsletter/Presenters/NewsletterPresenter.php: -------------------------------------------------------------------------------- 1 | newsletterService = $newsletterService; 31 | $this->userNewsletterModel = $userNewsletterModel; 32 | } 33 | 34 | public function actionDefault(string $hash): void 35 | { 36 | $this->userNewsletter = $this->userNewsletterModel->getAll()->where([ 37 | 'hash' => $hash, 38 | ])->fetch(); 39 | 40 | if (! $this->userNewsletter) { 41 | $this->error(null, IResponse::S404_NOT_FOUND); 42 | } 43 | } 44 | 45 | public function renderDefault(): void 46 | { 47 | $newsletter = $this->userNewsletter; 48 | $this->template->userNewsletter = NewsletterService::inlineCss($newsletter->toArray()); 49 | } 50 | 51 | public function actionUnsubscribe(string $hash): void 52 | { 53 | $userNewsletter = $this->userNewsletterModel->getAll()->where(['hash' => $hash])->fetch(); 54 | if ($userNewsletter) { 55 | $this->userModel->getAll()->wherePrimary($userNewsletter->user_id)->update([ 56 | 'newsletter' => false, 57 | ]); 58 | 59 | $this->template->email = $userNewsletter->user->email; 60 | } 61 | } 62 | 63 | public function actionDynamic(): void 64 | { 65 | // Allow newsletter preview only for admins 66 | if (! $this->getUser()->isLoggedIn() || ! $this->getUser()->isInRole('admin')) { 67 | $this->redirect(':Admin:Sign:in'); 68 | } 69 | } 70 | 71 | public function renderDynamic(int $userId): void 72 | { 73 | $newsletter = $this->newsletterService->buildArrayForTemplate($userId); 74 | $this->template->newsletter = NewsletterService::inlineCss($newsletter); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Eventigo 2 | 3 | [![Build Status](https://img.shields.io/travis/eventigo/eventigo-web/master.svg?style=flat-square)](https://travis-ci.org/eventigo/eventigo-web) 4 | 5 | 6 | ## Requirements 7 | 8 | - PHP 7.1+ 9 | - MySQL 5.6.5+, [5.7 not supported](http://stackoverflow.com/questions/34691059/select-distinct-and-order-by-in-mysql) 10 | - Composer, Bower 11 | 12 | ## First run 13 | 14 | 1. Vytvořit novou MySQL databázi a importovat `events.sql.zip` 15 | 2. Zkopírovat `app/config/templates/config.local.neon` do adresáře `app/config` a upravit konfiguraci 16 | 3. Zkopírovat `phinx.yml.template` jako nový soubor `phinx.yml` a nastavit přístupy do databáze (viz [Phinx docs](http://docs.phinx.org/en/latest/configuration.html)) 17 | 4. Nainstalovat závislosti 18 | ``` 19 | composer install 20 | bower install 21 | ``` 22 | 5. Spustit databázové migrace `vendor/bin/phinx migrate` 23 | 6. Vygenerovat heslo příkazem `php bin/console admin:generatePassword ` 24 | 7. Vytvořit admin uživatele v tabulce `users` s vygenerovaným heslem nebo použít demo admin účet: demo@gmail.com, heslo: demo 25 | 8. Přihlásit se na url `/admin` 26 | 27 | 28 | ## Newsletters 29 | 30 | Před vytvořením emailů je možný dynamický preview na adrese `/newsletter/dynamic/` 31 | 32 | 1. Vytvořit záznam v tabulce newsletters - `$ php bin/console newsletters:create`. Použije se poslední podle parametru created. Obsahuje texty, předmět mailu atd. 33 | 34 | 2. Do nového záznamu doplnit `intro_text` a `outro_text` (HTML formát) 35 | 36 | 3. Kontrola možná na adrese `/newsletter/dynamic/` 37 | 38 | 4. Vyrenderování (přípravení) newsletterů pro všechny, kdo má nastavený flag users.newsletter _(true)_ 39 | ` 40 | $ php bin/console newsletters:render 41 | ` 42 | 43 | 5. Preview konkrétního newsletteru na adrese `/newsletter/` 44 | Unsubscribe newsletterů přes link `/newsletter/unsubscribe/` 45 | 46 | 6. Odeslání připravených newsletterů _(nemá nastavené datum odeslání user_newsletter.sent)_ 47 | ` 48 | $ php bin/console newsletters:send 49 | ` 50 | 51 | ## API 52 | 53 | 📚 [Apiary documentation](http://docs.eventigo.apiary.io) 54 | 55 | ## Code style check & fix 56 | 57 | ✅ Check by running: 58 | 59 | ```bash 60 | composer cs 61 | ``` 62 | 63 | ✨ Auto-fix by running: 64 | 65 | ```bash 66 | composer fs 67 | ``` 68 | 69 | We use [Symplify/EasyCodingStandard](https://github.com/Symplify/EasyCodingStandard) (PHP_CodeSniffer and PHP-CS-Fixer). Thanks to @TomasVotruba! 70 | 71 | ## Exceptions 72 | 73 | Html exceptions lze číst jako admin na url `/admin/exception/[exception-file.html]` 74 | -------------------------------------------------------------------------------- /app/Modules/Core/Utils/DateTime.php: -------------------------------------------------------------------------------- 1 | translate('front.datetime.' . strtolower(strftime('%A', $a->getTimestamp()))); 80 | 81 | if ($b && ($a->format('dmy') !== $b->format('dmy'))) { 82 | // Two day event 83 | $result = $aDayName . $a->format(' j. n. ') . ' – ' . $b->format('j. n. Y'); 84 | } else { 85 | // One day event 86 | $result = $aDayName . $a->format(' j. n. Y'); 87 | // Add Hour:minute time if its not 00:00 88 | if ((int) $a->format('G') > 0) { 89 | $result .= $a->format(' G:i'); 90 | } 91 | } 92 | 93 | return $result; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/Modules/Front/Presenters/AbstractBasePresenter.php: -------------------------------------------------------------------------------- 1 | updateAccess(); 42 | } 43 | 44 | private function updateAccess(): void 45 | { 46 | $access = $this->getSession('access'); 47 | $now = new NetteDateTime; 48 | 49 | // Get last access stored in DB 50 | if ($this->getUser()->isLoggedIn()) { 51 | $user = $this->userModel->getAll() 52 | ->wherePrimary($this->getUser()->getId()) 53 | ->fetch(); 54 | $lastInDb = $user->last_access; 55 | } 56 | 57 | // Store the newest access 58 | $this->lastAccess = DateTime::max($access->last ?? null, $lastInDb ?? null); 59 | 60 | // Update last access 61 | $access->last = clone $now; 62 | 63 | // Update last access in DB if it has been updated few minutes ago or earlier 64 | $syncToDb = $this->context->getParameters()['lastAccess']['syncToDb'] ?? '5 minutes'; 65 | if ($this->getUser()->isLoggedIn() 66 | && $this->shouldSyncToDb($access->lastInDb ?? null, $lastInDb ?? null, $syncToDb) 67 | ) { 68 | $this->userModel->getAll() 69 | ->wherePrimary($this->getUser()->getId()) 70 | ->update([ 71 | 'last_access' => $now, 72 | ]); 73 | 74 | // Update last in DB access 75 | $access->lastInDb = clone $now; 76 | } 77 | } 78 | 79 | private function shouldSyncToDb( 80 | ?NetteDateTime $sessionLastInDb = null, 81 | ?NetteDateTime $lastInDb = null, 82 | string $syncToDb 83 | ): bool { 84 | return ! $sessionLastInDb 85 | || ($lastInDb && $lastInDb > $sessionLastInDb) 86 | || $sessionLastInDb < new NetteDateTime('-' . $syncToDb); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/Modules/Front/Presenters/templates/Homepage/default.latte: -------------------------------------------------------------------------------- 1 | {layout '../@layout.latte'} 2 | 3 | {** 4 | * @param array $events 5 | * @param EventModel $eventModel 6 | * @param int $eventsMaxCount 7 | *} 8 | 9 | {block header} 10 |
11 |
12 | 44 | 45 | 46 | 47 | 48 | {*
49 |
50 |

{="front.homepage.header.logo"|translate}

51 |
52 |
*} 53 |
54 |
55 |

{="front.homepage.header.mainTitle"|translate}

56 |

{="front.homepage.header.howItWorks"|translate|noescape}

57 | 58 | {= 59 | 60 |

{="front.homepage.header.subTitle"|translate}

61 | {="front.homepage.header.button"|translate} 62 |
63 |
64 |
65 |
66 | {/block} 67 | 68 | {block content} 69 |
70 |
71 |
72 | {control subscriptionTags} 73 |
74 |
75 |
76 | {="front.homepage.events.header"|translate} 77 |
78 | {control eventsList} 79 |
80 |
81 |
82 |
83 | {/block} 84 | -------------------------------------------------------------------------------- /app/Modules/Front/Presenters/ProfilePresenter.php: -------------------------------------------------------------------------------- 1 | userTagModel = $userTagModel; 35 | $this->settingsFactory = $settingsFactory; 36 | $this->tagsFactory = $tagsFactory; 37 | } 38 | 39 | public function actionSettings(?string $token = null): void 40 | { 41 | // Try to log in the user with provided token 42 | if ($token) { 43 | $this->loginWithToken($token); 44 | } 45 | } 46 | 47 | public function renderSettings(): void 48 | { 49 | $this->template->userData = $this->userModel->getAll() 50 | ->wherePrimary($this->getUser()->getId())->fetch(); 51 | 52 | $userTags = $this->userTagModel->getUsersTags($this->user->getId()); 53 | $this['tags']['form']->setDefaults(['tags' => $userTags]); 54 | 55 | $user = $this->userModel->getAll()->wherePrimary($this->getUser()->getId())->fetch(); 56 | $this['settings-form']->setDefaults([ 57 | 'newsletter' => $user->newsletter, 58 | 'abroadEvents' => $user->abroad_events, 59 | ]); 60 | } 61 | 62 | protected function startup(): void 63 | { 64 | parent::startup(); 65 | 66 | if (! $this->getUser()->isLoggedIn()) { 67 | $this->flashMessage($this->translator->translate('front.profile.settings.notLoggedIn')); 68 | $this->redirect('Homepage:default'); 69 | } 70 | } 71 | 72 | protected function createComponentTags(): Tags 73 | { 74 | $control = $this->tagsFactory->create(); 75 | 76 | $control->onChange[] = function (): void { 77 | $this['tags']->redrawControl(); 78 | $this->redrawControl('flash-messages'); 79 | }; 80 | 81 | return $control; 82 | } 83 | 84 | protected function createComponentSettings(): Settings 85 | { 86 | return $this->settingsFactory->create(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /www/js/plugins/eventFilter.js: -------------------------------------------------------------------------------- 1 | (function ($, allTags) { 2 | 3 | const showTime = 300; 4 | 5 | 6 | /** 7 | * Plugin initialization 8 | * @returns {jQuery} 9 | */ 10 | $.fn.eventFilter = function () { 11 | filter(); 12 | this.change(filter); 13 | return this; 14 | }; 15 | 16 | 17 | /** 18 | * Filter events by checked tags 19 | */ 20 | function filter() { 21 | var tags = getCheckedTags(), 22 | tagsClasses = tags.prefix('.tag-'), 23 | events = $('#events-list'); 24 | 25 | if (tagsClasses.length) { 26 | // Hide unchecked tags 27 | events 28 | .find('.event-box:not(' + tagsClasses.join(',') + ')') 29 | .fadeOut(showTime); 30 | 31 | // Show up hidden 32 | var filteredEvents = events.find('.event-box' + tagsClasses.join(',')); 33 | filteredEvents.fadeIn(showTime); 34 | 35 | } else { 36 | // Show all up 37 | events 38 | .children() 39 | .fadeIn(showTime); 40 | 41 | filteredEvents = events.children(); 42 | tags = allTags; 43 | } 44 | 45 | // Order events by rate 46 | orderByRate(filteredEvents, tags); 47 | } 48 | 49 | 50 | /** 51 | * Return array with checked tags 52 | * @returns {Array} 53 | */ 54 | function getCheckedTags() { 55 | var checked = $('#tags').find('input:checked'); 56 | 57 | var tags = []; 58 | checked.each(function (index, el) { 59 | tags.push($(el).parent().data('tag')); 60 | }); 61 | 62 | return tags; 63 | } 64 | 65 | 66 | /** 67 | * Order events by calculated rate 68 | * @param events 69 | * @param tags 70 | */ 71 | function orderByRate(events, tags) { 72 | var rates = calculateRates(events, tags); 73 | events.sort(function (a, b) { 74 | return rates[a.id] > rates[b.id] ? -1 : 1; 75 | }).appendTo('#events-list'); 76 | } 77 | 78 | 79 | /** 80 | * Order events by calculated rate 81 | * @param events 82 | * @param tags 83 | */ 84 | function calculateRates(events, tags) { 85 | var eventsRates = []; 86 | 87 | events.each(function (i, el) { 88 | eventsRates[el.id] = 0; 89 | 90 | var rates = $(el).data('rates'); 91 | 92 | // Event rate 93 | var eventRate = rates.event; 94 | 95 | // Tag rates 96 | tags.forEach(function (tag) { 97 | var tagRate = rates[tag] || 0; 98 | eventsRates[el.id] += eventRate * tagRate; 99 | }); 100 | }); 101 | 102 | return eventsRates; 103 | } 104 | 105 | }(jQuery, allTags)); -------------------------------------------------------------------------------- /app/Modules/Email/Model/EmailService.php: -------------------------------------------------------------------------------- 1 | sendGrid = $sendGrid; 48 | $this->templateFactory = $templateFactory; 49 | $this->linkGenerator = $linkGenerator; 50 | $this->translator = $translator; 51 | } 52 | 53 | public function sendLogin(string $emailTo, string $token): void 54 | { 55 | $to = new Email(null, $emailTo); 56 | $from = new Email('Eventigo.cz', 'prihlaseni@eventigo.cz'); 57 | $subject = $this->translator->translate('email.login.subject'); 58 | $content = new Content('text/html', $this->renderLoginEmail($token)); 59 | 60 | $mail = new Mail($from, $subject, $to, $content); 61 | $mail->addCategory('emailLogin'); 62 | 63 | try { 64 | $this->sendGrid->client->mail()->send()->post($mail); 65 | } catch (Throwable $throwable) { 66 | // TODO log unsuccessful email send 67 | } 68 | } 69 | 70 | public function renderLoginEmail(string $token): string 71 | { 72 | $email = new BasicEmail; 73 | $email->setIntroText($this->translator->translate('email.login.text')); 74 | $email->setButtonText($this->translator->translate('email.login.loginButton')); 75 | $email->setButtonUrl(new Url($this->linkGenerator->link('Front:Homepage:default', ['token' => $token]))); 76 | $email->setFooterText($this->translator->translate('email.login.footerText')); 77 | 78 | /** @var Template $template */ 79 | $template = $this->templateFactory->createTemplate(); 80 | Filters::setTranslator($this->translator); 81 | $template->addFilter(null, [Filters::class, 'loader']); 82 | 83 | return $template->getLatte()->renderToString(EmailPresenter::BASIC_EMAIL_TEMPLATE_FILE, [ 84 | 'email' => $email, 85 | ]); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/Modules/Admin/Presenters/SourcesPresenter.php: -------------------------------------------------------------------------------- 1 | sourceService = $sourceService; 44 | $this->sourceModel = $sourceModel; 45 | $this->sourcesTableFactory = $sourcesTableFactory; 46 | $this->sourceFormFactory = $sourceFormFactory; 47 | } 48 | 49 | public function actionCreate(): void 50 | { 51 | $defaults = [ 52 | 'frequencyNumber' => 1, 53 | 'nextCheck' => (new NetteDateTime('+1 days'))->format(DateTime::DATE_FORMAT), 54 | 'createOrganiser' => true, 55 | ]; 56 | $this['sourceForm-form']->setDefaults($defaults); 57 | } 58 | 59 | protected function createComponentSourceForm(): SourceForm 60 | { 61 | $control = $this->sourceFormFactory->create(); 62 | 63 | $control->onCreate[] = function (ActiveRow $source): void { 64 | $this->flashMessage($this->translator->translate('admin.sourceForm.success'), 'success'); 65 | 66 | // Crawl recently added source 67 | $addedEvents = $this->sourceService->crawlSource($source); 68 | if ($addedEvents > 0) { 69 | $this->flashMessage($this->translator->translate('admin.events.crawlSources.success', 70 | $addedEvents, ['events' => $addedEvents]), 'success'); 71 | } else { 72 | $this->flashMessage($this->translator->translate('admin.events.crawlSources.noEvents')); 73 | } 74 | 75 | $this->redirect('Sources:default'); 76 | }; 77 | 78 | $control->onUpdate[] = function (ActiveRow $source): void { 79 | $this->flashMessage($this->translator->translate('admin.sourceForm.success'), 'success'); 80 | $this->redirect('Sources:update', ['id' => $source->id]); 81 | }; 82 | 83 | return $control; 84 | } 85 | 86 | protected function createComponentSourcesTable(): SourcesTable 87 | { 88 | return $this->sourcesTableFactory->create($this->sourceModel->getAll()->select('id, name, url')); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/lang/front.cs_CZ.neon: -------------------------------------------------------------------------------- 1 | homepage.fbLogin.failed: Přihlášení přes facebook neproběhlo v pořádku 2 | homepage.header.logo: eventigo 3 | homepage.header.title: Každý týden výběr akcí přesně pro tebe 4 | homepage.header.mainTitle: "Eventy, které tě zajímají" 5 | homepage.header.howItWorks: "Každý čtvrtek ti pošleme přehled zajímavých akcí na následující týden.
Události za tebe hledáme napříč několika zdroji." 6 | homepage.header.subTitle: "Email každý týden s výběrem akcí přesně pro tebe" 7 | homepage.header.button: To chci vyzkoušet 8 | homepage.tags.header: "1 Jaké akce chceš dostávat?" 9 | homepage.events.header: Ukázka akcí z newsletteru 10 | subscription.form.email.placeholder: tvuj@email.cz 11 | subscription.form.email.validation: Email je špatně. Zadej správně svou emailovou adresu 12 | subscription.form.email.required: Zadej svou emailovou adresu 13 | subscription.form.subscribe.label: Chci dostávat týdenní výběr akcí 14 | subscription.form.email.label: "2 Kam chceš akce posílat?" 15 | subscription.message.emailExists: "Email %email% je již zaregistrován. Poslali jsme ti odkaz pro přihlášení" 16 | subscription.message.success: "Jsme rádi, že tě Eventigo zaujalo. Na adresu %email% ti bude pravidelně chodit přehled akcí." 17 | events.thisWeek.title: Tento týden 18 | events.thisMonth.title: Tento měsíc 19 | events.nextMonth.title: Příští měsíc 20 | events.upcoming.title: Další akce 21 | eventList.label.new: nové 22 | eventList.label.today: dnes 23 | eventList.label.recentlyAdded: nedávno přidané 24 | eventList.noEvents.title: Pro tvoje sledované tagy nejsou aktuálně žádné akce 25 | eventList.noEvents.settingsInfo: "Mrkni do nastavení tagů, zda tě nezaujme nějaké další téma, které sledovat" 26 | eventList.noEvents.linkToSettings: Nastavení sledovaných tagů 27 | eventList.noEvents.discoverInfo: Projdi si ostatní nadcházející akce 28 | eventList.noEvents.linkToDiscover: Procházet akce 29 | newsletter.header.title: Výběr nadcházejících akcí 30 | datetime.monday: Pondělí 31 | datetime.tuesday: Úterý 32 | datetime.wednesday: Středa 33 | datetime.thursday: Čtvrtek 34 | datetime.friday: Pátek 35 | datetime.saturday: Sobota 36 | datetime.sunday: Neděle 37 | footer.text: "© 2016 eventigo
Made with in Prague" 38 | meta.description: "Každý týden výběr akcí přesně pro tebe. Konference, meetupy, přednášky, workshopy a další." 39 | meta.title: "Eventy na tento týden" 40 | nav.myFeed: Můj feed 41 | nav.discover: Procházet akce 42 | nav.settings: Nastavení 43 | nav.signOut: Odhlásit se 44 | nav.login: Přihlásit se 45 | nav.login.facebook: Přihlásit se přes Facebook 46 | nav.login.email: Přihlásit se přes email 47 | nav.user: Uživatel 48 | profile.settings.main.newsletter: Chci dostávat email s výběrem akcí 49 | profile.settings.main.abroad: Zobrazovat akce v zahraničí 50 | profile.settings.main.title: Hlavní nastavení 51 | profile.settings.tags.title: Sledování tagů 52 | profile.settings.afterLogin: "Nastav si sledování tagů, které tě zajímají" 53 | profile.settings.notLoggedIn: Pro nastavení tagů se prosím přihlaš tlačítkem vpravo nahoře. 54 | sign.out: Byl jsi odhlášen. %bye% 55 | signIn.form.email: email 56 | signIn.form.email.placeholder: tvuj@email.cz 57 | signIn.form.email.invalid: "Zadej platný email, ať ti tam pošleme odkaz pro přihlášení" 58 | signIn.form.email.required: "Zadej email, ať ti tam pošleme odkaz pro přihlášení" 59 | signIn.form.submit: Získat přihlašovací odkaz 60 | signIn.form.submit.sending: Zasílám email 61 | signIn.form.success: Na email %email% jsme ti poslali odkaz pro přihlášení 62 | signIn.form.title: Přihlášení přes email 63 | -------------------------------------------------------------------------------- /app/Modules/Admin/Components/SourceForm/SourceForm.php: -------------------------------------------------------------------------------- 1 | organiserService = $organiserService; 38 | $this->sourceModel = $sourceModel; 39 | } 40 | 41 | public function render(): void 42 | { 43 | $this['form']->render(); 44 | } 45 | 46 | public function processForm(Form $form): void 47 | { 48 | $values = $form->getValues(); 49 | 50 | if (! $values->id) { 51 | if ($values->createOrganiser) { 52 | $organiser = $this->organiserService->createOrganiser($values->name, $values->url); 53 | } 54 | 55 | $source = $this->sourceModel->insert([ 56 | 'name' => $values->name ?: null, 57 | 'url' => $values->url ?: null, 58 | 'check_frequency' => $checkFrequency = SourceModel::FREQUENCY_TYPES[$values->frequency], 59 | 'next_check' => $values->nextCheck 60 | ? NetteDateTime::createFromFormat(DateTime::DATE_FORMAT, $values->nextCheck) 61 | : new NetteDateTime('+' . $checkFrequency . ' days'), 62 | 'event_series_id' => isset($organiser) 63 | ? $organiser->related('events_series')->fetch()->id 64 | : null, 65 | ]); 66 | 67 | $this->onCreate($source); 68 | } else { 69 | $source = $this->sourceModel->update($values); 70 | $this->onUpdate($source); 71 | } 72 | } 73 | 74 | protected function createComponentForm(): Form 75 | { 76 | $form = new Form; 77 | $form->setTranslator($this->translator->domain('admin.sourceForm')); 78 | 79 | // Event 80 | $form->addGroup(); 81 | $form->addText('name', 'name') 82 | ->setAttribute('autofocus', true); 83 | $form->addText('url', 'url') 84 | ->addCondition(Form::FILLED) 85 | ->addRule(Form::URL, 'url.wrong'); 86 | $form->addSelect('frequency', 'frequency') 87 | ->setItems(array_combine( 88 | $frequencyTypes = array_keys(SourceModel::FREQUENCY_TYPES), 89 | Collection::prefix($frequencyTypes, 'frequency.') 90 | )); 91 | $form->addText('nextCheck', 'nextCheck') 92 | ->setAttribute('class', 'date'); 93 | 94 | $form->addCheckbox('createOrganiser', 'createOrganiser'); 95 | 96 | $form->addHidden('id'); 97 | 98 | $form->addSubmit('save', 'save') 99 | ->setAttribute('class', 'btn btn-success'); 100 | $form->onSubmit[] = [$this, 'processForm']; 101 | 102 | return $form; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/Modules/Admin/Model/SourceService.php: -------------------------------------------------------------------------------- 1 | eventModel = $eventModel; 48 | $this->meetupSource = $meetupSource; 49 | $this->srazySource = $srazySource; 50 | $this->fbSource = $fbSource; 51 | $this->sourceModel = $sourceModel; 52 | } 53 | 54 | public function crawlSources(): int 55 | { 56 | $addedEvents = 0; 57 | 58 | // TODO Check sources by frequency 59 | $sources = $this->sourceModel->getAll(); 60 | foreach ($sources as $source) { 61 | $addedEvents += $this->crawlSource($source); 62 | } 63 | 64 | return $addedEvents; 65 | } 66 | 67 | public function crawlSource(IRow $source): int 68 | { 69 | $addedEvents = 0; 70 | 71 | $events = []; 72 | $sourceUrl = new Url($source['url']); 73 | if (FacebookEventSource::isSource($source['url'])) { 74 | $pageId = trim($sourceUrl->getPath(), '/'); 75 | $events = $this->fbSource->getEvents($pageId); 76 | } elseif (SrazyEventSource::isSource($source['url'])) { 77 | $pathParts = array_filter(explode('/', $sourceUrl->getPath())); 78 | $series = reset($pathParts); 79 | $events = $this->srazySource->getEvents($series); 80 | } elseif (MeetupEventSource::isSource($source['url'])) { 81 | $pathParts = array_filter(explode('/', $sourceUrl->getPath())); 82 | $group = reset($pathParts); 83 | $events = $this->meetupSource->getEvents($group); 84 | } 85 | 86 | foreach ($events as $event) { 87 | $existingEvent = $this->eventModel->findExistingEvent($event); 88 | 89 | if (! $existingEvent) { 90 | try { 91 | $this->eventModel->insert([ 92 | 'name' => $event->getName(), 93 | 'description' => $event->getDescription(), 94 | 'start' => $event->getStart(), 95 | 'end' => $event->getEnd(), 96 | 'origin_url' => $event->getOriginUrl(), 97 | 'image' => $event->getImage(), 98 | 'rate' => $event->getRate(), 99 | 'event_series_id' => $source['event_series_id'], 100 | ]); 101 | } catch (UniqueConstraintViolationException $e) { 102 | continue; 103 | } 104 | 105 | ++$addedEvents; 106 | } 107 | } 108 | 109 | return $addedEvents; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /app/Modules/Core/Model/EventSources/Facebook/FacebookEventSource.php: -------------------------------------------------------------------------------- 1 | facebook = $facebook; 35 | } 36 | 37 | public function getEventById(string $id): Event 38 | { 39 | try { 40 | $response = $this->facebook->api( 41 | '/' . $id, 42 | 'GET', 43 | ['fields' => self::EVENT_FIELDS] 44 | ); 45 | 46 | return new Event( 47 | null, 48 | $response->name, 49 | $response->description ?? '', 50 | 'https://facebook.com/events/' . $id . '/', 51 | isset($response->start_time) ? DateTime::createFromFormat(DATE_ISO8601, $response->start_time) : null, 52 | isset($response->end_time) ? DateTime::createFromFormat(DATE_ISO8601, $response->end_time) : null, 53 | $response->place->location->city ?? null, 54 | null, 55 | $response->cover->source ?? null, 56 | Event::calculateRateByAttendeesCount($response->interested_count + $response->attending_count) 57 | ); 58 | } catch (FacebookApiException $e) { 59 | Debugger::log($e, Debugger::EXCEPTION); 60 | 61 | throw $e; 62 | } 63 | } 64 | 65 | /** 66 | * Get upcoming events of the page. 67 | * @return Event[] 68 | */ 69 | public function getEvents(string $pageId): array 70 | { 71 | $response = $this->facebook->api( 72 | '/' . $pageId, 'GET', [ 73 | 'fields' => 'events{' . self::EVENT_FIELDS . '}', 74 | ] 75 | ); 76 | 77 | $events = []; 78 | if (isset($response->events)) { 79 | $fbEvents = $response->events->data; 80 | foreach ($fbEvents as $event) { 81 | $start = isset($event->start_time) ? $this->createIsoDateTime($event->start_time) : null; 82 | if ($start && $start > new DateTime) { 83 | $events[] = new Event( 84 | null, 85 | $event->name, 86 | $event->description ?? '', 87 | 'https://facebook.com/events/' . $event->id . '/', 88 | $start, 89 | isset($event->end_time) ? DateTime::createFromFormat(DATE_ISO8601, $event->end_time) : null, 90 | $event->place->location->city ?? null, 91 | null, 92 | $event->cover->source ?? null, 93 | Event::calculateRateByAttendeesCount($event->interested_count + $event->attending_count) 94 | ); 95 | } 96 | } 97 | } 98 | 99 | return $events; 100 | } 101 | 102 | private function createIsoDateTime(string $time): DateTime 103 | { 104 | return DateTime::createFromFormat(DATE_ISO8601, $time); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/Modules/Front/Components/Tags/Tags.php: -------------------------------------------------------------------------------- 1 | tagModel = $tagModel; 56 | $this->userTagModel = $userTagModel; 57 | $this->tagGroupModel = $tagGroupModel; 58 | $this->tags = $this->tagModel->getAllByMostEvents(); 59 | $this->user = $user; 60 | } 61 | 62 | public function render(): void 63 | { 64 | $this->template->tags = $this->tags->fetchPairs('code'); 65 | $this->template->tagsGroups = $this->tagGroupModel->getAll() 66 | ->where('icon IS NOT NULL') 67 | ->fetchPairs('name', 'icon'); 68 | $this->template->columnsPerRow = 4; 69 | parent::render(); 70 | } 71 | 72 | public function processForm(Form $form): void 73 | { 74 | $values = $form->getValues(); 75 | 76 | // Store user's selected tags 77 | $chosenTags = Collection::getNestedValues((array) $values->tags); 78 | $tags = $this->tagModel->getAll()->where('code IN (?)', $chosenTags)->fetchAll(); 79 | 80 | foreach ($tags as $tag) { 81 | try { 82 | // Add newly checked tags 83 | $this->userTagModel->insert([ 84 | 'tag_id' => $tag->id, 85 | 'user_id' => $this->user->getId(), 86 | ]); 87 | } catch (UniqueConstraintViolationException $e) { 88 | // Tag has been already inserted 89 | } 90 | } 91 | 92 | // Remove not checked tags 93 | if ($chosenTags) { 94 | $this->userTagModel->delete([ 95 | 'user_id' => $this->user->getId(), 96 | 'tag_id NOT IN (?)' => $this->tagModel->getAll() 97 | ->where('code IN (?)', $chosenTags) 98 | ->fetchPairs(null, 'id'), 99 | ]); 100 | } else { 101 | $this->userTagModel->delete([ 102 | 'user_id' => $this->user->getId(), 103 | ]); 104 | } 105 | 106 | $this->onChange(); 107 | } 108 | 109 | protected function createComponentForm(): Form 110 | { 111 | $form = new Form; 112 | 113 | $tagsGroups = $this->tags->fetchAssoc('tagGroupName|id'); 114 | $tagsContainer = $form->addContainer('tags'); 115 | foreach ($tagsGroups as $tagGroupName => $tagsGroup) { 116 | $tagsContainer->addCheckboxList($tagGroupName) 117 | ->setItems(Helpers::toPairs($tagsGroups[$tagGroupName], 'code', 'name')) 118 | ->setTranslator(null); 119 | } 120 | 121 | $form->onSuccess[] = [$this, 'processForm']; 122 | 123 | return $form; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/Modules/Newsletter/Presenters/templates/@layout.latte: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {ifset title}{include title|striptags} | {/ifset}Eventigo.cz 7 | 8 | {* favicon *} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {* Open Graph Markup *} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {* Styles *} 44 | 45 | 46 | 47 | {block head}{/block} 48 | 49 | 50 | 51 | {include content} 52 | 53 | {block scripts}{/block} 54 | 55 | {* Google Analytics if key is set in local config *} 56 | {* TODO: put in separate file *} 57 | {if !empty($parameters['googleAnalyticsKey'])} 58 | 67 | {/if} 68 | 69 | 70 | 80 | 81 | 82 | --------------------------------------------------------------------------------