diff --git a/docs/.dockerignore b/docs/.dockerignore new file mode 100644 index 00000000000..d8d37532fe2 --- /dev/null +++ b/docs/.dockerignore @@ -0,0 +1,28 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +dist +tmp + +# dependencies +node_modules +bower_components +Dockerfile + +# misc +/.sass-cache +/connect.lock +/coverage/* +/libpeerconnection.log +npm-debug.log* +testem.log +/public/assets/locales/ +*~ +/kubernetes/images/frontend/deploy-dist/ +/kubernetes/images/frontend/node_Modules/ +.idea +.git +docs +kubernetes +tests +/yarn-error.log diff --git a/docs/.editorconfig b/docs/.editorconfig new file mode 100644 index 00000000000..219985c2289 --- /dev/null +++ b/docs/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.hbs] +insert_final_newline = false + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/docs/.ember-cli b/docs/.ember-cli new file mode 100644 index 00000000000..ee64cfed2a8 --- /dev/null +++ b/docs/.ember-cli @@ -0,0 +1,9 @@ +{ + /** + Ember CLI sends analytics information by default. The data is completely + anonymous, but there are times when you might want to disable this behavior. + + Setting `disableAnalytics` to true will prevent any data from being sent. + */ + "disableAnalytics": false +} diff --git a/docs/.env.example b/docs/.env.example new file mode 100644 index 00000000000..71359c8c78f --- /dev/null +++ b/docs/.env.example @@ -0,0 +1,5 @@ +#MAPBOX_ACCESS_TOKEN = "Access Token" +API_HOST=https://test-api.eventyay.com +MAP_DISPLAY=embed +FASTBOOT_DISABLED=true +#HCAPTCHA_SITE_KEY="" diff --git a/docs/.eslintignore b/docs/.eslintignore new file mode 100644 index 00000000000..61ae85e526a --- /dev/null +++ b/docs/.eslintignore @@ -0,0 +1,2 @@ +vendor +app/utils/l10n-fingerprint-map.js diff --git a/docs/.eslintrc.js b/docs/.eslintrc.js new file mode 100644 index 00000000000..79be531258b --- /dev/null +++ b/docs/.eslintrc.js @@ -0,0 +1,146 @@ +const config = { + root: true, + parser: 'babel-eslint', + parserOptions: { + ecmaVersion: 2017, + sourceType: 'module', + ecmaFeatures: { legacyDecorators: true }, + }, + plugins: [ + 'ember', + 'ember-suave' + ], + extends: [ + 'eslint:recommended', + 'plugin:ember-suave/recommended' + ], + env: { + es6: true, + browser: true + }, + rules: { + 'arrow-spacing': 'error', + 'no-var': 'error', + 'no-useless-escape': 'off', + 'space-before-blocks': 'error', + 'comma-dangle': ['error', 'never'], + 'space-in-parens': ['error', 'never'], + 'space-before-function-paren': ['error', 'never'], + 'comma-spacing': ['error', { 'before': false, 'after': true }], + 'semi': ['error', 'always', {'omitLastInOneLineBlock': true}], + 'semi-spacing': ['error', {'before': false, 'after': true}], + 'keyword-spacing': ['error', { 'before': true, 'after': true }], + 'spaced-comment': ['error', 'always'], + 'object-shorthand': ['error', 'always'], + 'dot-notation': 'error', + 'arrow-parens': ['error', 'as-needed'], + 'object-curly-spacing': ['error', 'always'], + 'space-infix-ops': 'error', + 'no-multiple-empty-lines': ['error', { 'max': 2, 'maxEOF': 1 }], + 'key-spacing': [ + 'error', { + 'align': { + 'beforeColon': true, + 'afterColon': true, + 'on': 'colon' + } + } + ], + 'operator-linebreak': ['error', 'before'], + 'array-bracket-spacing': ['error', 'never'], + 'no-trailing-spaces': 'error', + 'brace-style': ['error', '1tbs', { 'allowSingleLine': true }], + 'max-statements-per-line': ['error', { 'max': 2 }], + 'quotes': ['error', 'single'], + 'indent': [ + 'error', 2, { + "FunctionExpression": {"parameters": "first"}, + "FunctionDeclaration": {"parameters": "first"}, + "MemberExpression": 1, + "SwitchCase": 1, + "outerIIFEBody": 0, + "VariableDeclarator": { "var": 2, "let": 2, "const": 3 } + } + ], + 'no-console': ["error", { allow: ["warn", "error"] }], + 'prefer-template': 'off', + 'prefer-rest-params': 'off', + 'camelcase': 'off', + 'eqeqeq': ['error', 'smart'], + "prefer-const": ["error", { + "destructuring": "any", + "ignoreReadBeforeAssign": false + }], + 'padding-line-between-statements': 'off', + 'lines-between-class-members': ['error', "always", { exceptAfterSingleLine: true }], + 'ember-suave/no-const-outside-module-scope': 'off', + 'ember-suave/require-access-in-comments': 'off', + 'ember-suave/lines-between-object-properties': 'off', + }, + globals: { + module : true, + process : true, + wysihtml5 : true, + palette : true, + Uint8Array : true, + require : true, + Promise : true + }, + overrides: [ + // node files + { + files: [ + '.eslintrc.js', + '.template-lintrc.js', + 'ember-cli-build.js', + 'testem.js', + 'blueprints/*/index.js', + 'config/**/*.js', + 'lib/*/index.js', + 'server/**/*.js' + ], + parserOptions: { + sourceType: 'script', + ecmaVersion: 2015 + }, + env: { + browser: false, + node: true + }, + plugins: ['node'], + rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { + // add your custom rules and overrides for node files here + + // this can be removed once the following is fixed + // https://github.com/mysticatea/eslint-plugin-node/issues/77 + 'node/no-unpublished-require': 'off', + 'node/no-extraneous-require': 'off' + }) + } + ] +}; + +config.overrides.push({ + files: ['**/*.ts'], + parser: '@typescript-eslint/parser', + parserOptions: config.parserOptions, + plugins: [ + ...config.plugins, + '@typescript-eslint' + ], + extends: [ + ...config.extends, + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + env: config.env, + rules: { + ...config.rules, + "@typescript-eslint/no-unused-vars": ["error"], + '@typescript-eslint/no-explicit-any': 'off', + 'semi': 'off' + }, + globals: config.globals +}) + +module.exports = config; diff --git a/docs/.github/CONTRIBUTING.md b/docs/.github/CONTRIBUTING.md new file mode 100644 index 00000000000..73b6c3e01ba --- /dev/null +++ b/docs/.github/CONTRIBUTING.md @@ -0,0 +1,16 @@ +## Contributions Best Practices + +**Commits** +* Write clear meaningful git commit messages (Do read https://chris.beams.io/posts/git-commit/) +* Make sure your PR's description contains GitHub's special keyword references that automatically close the related issue when the PR is merged. (More info at https://github.com/blog/1506-closing-issues-via-pull-requests ) +* When you make very very minor changes to a PR of yours (like for example fixing a failing travis build or some small style corrections or minor changes requested by reviewers) make sure you squash your commits afterwards so that you don't have an absurd number of commits for a very small fix. (Learn how to squash at https://davidwalsh.name/squash-commits-git ) +* When you're submitting a PR for a UI-related issue, it would be really awesome if you add a screenshot of your change or a link to a deployment where it can be tested out along with your PR. It makes it very easy for the reviewers and you'll also get reviews quicker. + +**Feature Requests and Bug Reports** +* When you file a feature request or when you are submitting a bug report to the [issue tracker](https://github.com/fossasia/open-event-frontend/issues), make sure you add steps to reproduce it. Especially if that bug is some weird/rare one. + +**Join the development** +* Before you join development, please [set up the project on your local machine](/docs/installation/local.md), run it and go through the application completely. Press on any button you can find and see where it leads to. Explore. (Don't worry ... Nothing will happen to the app or to you due to the exploring :wink: Only thing that will happen is, you'll be more familiar with what is where and might even get some cool ideas on how to improve various aspects of the app.) +* If you would like to work on an issue, drop in a comment at the issue. If it is already assigned to someone, but there is no sign of any work being done, please free to drop in a comment so that the issue can be assigned to you if the previous assignee has dropped it entirely. + +Do read the [Open Source Developer Guide and Best Practices at FOSSASIA](https://blog.fossasia.org/open-source-developer-guide-and-best-practices-at-fossasia). diff --git a/docs/.github/ISSUE_TEMPLATE/bug_report.md b/docs/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000000..cdba359c1d5 --- /dev/null +++ b/docs/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Describe the bug** + + +**To Reproduce** +Steps to reproduce the behaviour: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behaviour** + + +**Screenshots** + + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** + diff --git a/docs/.github/ISSUE_TEMPLATE/feature_request.md b/docs/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000000..ad9a01dd8ae --- /dev/null +++ b/docs/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** + + +**Describe the solution you'd like** + + +**Describe alternatives you've considered** + + +**Additional context** + diff --git a/docs/.github/PULL_REQUEST_TEMPLATE.md b/docs/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..6cecac1c38f --- /dev/null +++ b/docs/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ + + + + +Fixes # + +#### Short description of what this resolves: + + +#### Changes proposed in this pull request: + +- +- +- + +#### Checklist + +- [ ] I have read the [Contribution & Best practices Guide](https://blog.fossasia.org/open-source-developer-guide-and-best-practices-at-fossasia). +- [ ] My branch is up-to-date with the Upstream `development` branch. +- [ ] The acceptance, integration, unit tests and linter pass locally with my changes +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] I have added necessary documentation (if appropriate) diff --git a/docs/.github/dependabot.yml b/docs/.github/dependabot.yml new file mode 100644 index 00000000000..e57b468012e --- /dev/null +++ b/docs/.github/dependabot.yml @@ -0,0 +1,104 @@ +version: 2 +updates: +- package-ecosystem: npm + directory: "/" + schedule: + interval: daily + time: "21:00" + open-pull-requests-limit: 10 + ignore: + - dependency-name: ember-data + versions: + - "> 3.14.1, < 4" + - dependency-name: ember-data + versions: + - ">= 3.15.a, < 3.16" + - dependency-name: ember-data + versions: + - ">= 3.16.a, < 3.17" + - dependency-name: ember-models-table + versions: + - "> 3.0.0, < 4" + - dependency-name: ember-source + versions: + - "> 3.16.1, < 3.17" + - dependency-name: ember-source + versions: + - ">= 3.17.a, < 3.18" + - dependency-name: ember-source + versions: + - "> 3.20.3, < 4" + - dependency-name: object-to-formdata + versions: + - "> 3.0.4, < 3.1" + - dependency-name: "@sentry/browser" + versions: + - 6.2.4 + - 6.3.1 + - 6.3.2 + - 6.3.3 + - dependency-name: "@sentry/tracing" + versions: + - 6.3.0 + - 6.3.3 + - dependency-name: "@sentry/integrations" + versions: + - 6.3.1 + - 6.3.2 + - 6.3.3 + - dependency-name: mini-css-extract-plugin + versions: + - 1.5.0 + - dependency-name: ember-h-captcha + versions: + - 2.0.13 + - 2.0.22 + - 2.0.31 + - 2.0.33 + - dependency-name: eslint-plugin-ember + versions: + - 10.4.0 + - dependency-name: ember-qunit + versions: + - 5.1.3 + - 5.1.4 + - dependency-name: ember-router-scroll + versions: + - 4.0.2 + - 4.0.3 + - dependency-name: ember-auto-import + versions: + - 1.11.0 + - dependency-name: ember-template-lint + versions: + - 3.0.0 + - 3.0.1 + - 3.1.0 + - 3.1.1 + - 3.2.0 + - dependency-name: ember-cli-babel + versions: + - 7.24.0 + - 7.25.0 + - 7.26.0 + - dependency-name: ember-table + versions: + - 3.0.0 + - 3.0.1 + - dependency-name: moment-timezone + versions: + - 0.5.33 + - dependency-name: ember-fullcalendar + versions: + - 2.0.0 + - dependency-name: paypal-checkout + versions: + - 4.0.324 + - 4.0.325 + - dependency-name: "@typescript-eslint/parser" + versions: + - 4.16.0 + - dependency-name: "@babel/core" + versions: + - 7.13.0 + - 7.13.1 \ No newline at end of file diff --git a/docs/.github/release-drafter.yml b/docs/.github/release-drafter.yml new file mode 100644 index 00000000000..f3b015dcd60 --- /dev/null +++ b/docs/.github/release-drafter.yml @@ -0,0 +1,23 @@ +name-template: Release v$NEXT_MINOR_VERSION 🌈 +tag-template: v$NEXT_MINOR_VERSION +categories: + - title: 🚀 Features + label: feature + - title: 🐛 Bug Fixes + label: fix + - title: 🧰 Maintenance + label: chore + - title: 🕮 Documentation + label: docs + - title: ⚙ Dependencies and Libraries + label: dependencies +change-template: '- $TITLE (#$NUMBER) - @$AUTHOR' +template: | + ## Changes + + $CHANGES + + ## Contributors + + Thanks a lot to our contributors for making this release possible: + $CONTRIBUTORS diff --git a/docs/.github/workflows/ci.yml b/docs/.github/workflows/ci.yml new file mode 100644 index 00000000000..fbd14dac726 --- /dev/null +++ b/docs/.github/workflows/ci.yml @@ -0,0 +1,106 @@ +name: CI + +on: + push: + branches: [ development, master, testing ] + pull_request: + branches: [ development, master, testing ] + +jobs: + install: + name: Install Dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + id: cache + with: + path: node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn + - name: Setup Node.js + uses: actions/setup-node@v1 + if: steps.cache.outputs.cache-hit != 'true' + with: + node-version: 14.x + - run: yarn + if: steps.cache.outputs.cache-hit != 'true' + + + lint: + name: Lint + needs: install + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + - uses: actions/cache@v2 + id: cache + with: + path: node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + # no restore-keys here, so we only accept this exact version + - run: yarn lint + + + test: + name: Test + needs: install + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Copy .env + run: cp '.env.example' '.env' + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + - uses: actions/cache@v2 + id: cache + with: + path: node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + # no restore-keys here, so we only accept this exact version + - run: yarn l10n:generate + - run: COVERAGE=true yarn test + - name: Code Coverage + uses: codecov/codecov-action@v1 + + + build: + name: Build + needs: install + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 14.x + - uses: actions/cache@v2 + id: cache + with: + path: node_modules + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + # no restore-keys here, so we only accept this exact version + - run: yarn l10n:extract + - run: yarn l10n:update + - run: yarn l10n:generate + - run: ROOT_URL=open-event-frontend yarn build -prod + env: + API_HOST: https://open-event.dokku.fossasia.org + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + if: github.ref == 'refs/heads/development' && github.event_name == 'push' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dist + force_orphan: true diff --git a/docs/.github/workflows/docker-hub.yml b/docs/.github/workflows/docker-hub.yml new file mode 100644 index 00000000000..27d9fd0a75a --- /dev/null +++ b/docs/.github/workflows/docker-hub.yml @@ -0,0 +1,49 @@ +# Ref: https://github.com/docker/build-push-action#usage +name: Build Docker container + +on: + push: + branches: + - development + - master + +jobs: + docker: + if: ${{ github.repository_owner == 'fossasia' }} + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Docker meta + uses: docker/metadata-action@v4 + id: meta + with: + images: | + eventyay/open-event-frontend + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - + name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - + name: Set up Buildx + uses: docker/setup-buildx-action@v2 + - + name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/docs/.github/workflows/issue_scripts.yml b/docs/.github/workflows/issue_scripts.yml new file mode 100644 index 00000000000..22eb1af2853 --- /dev/null +++ b/docs/.github/workflows/issue_scripts.yml @@ -0,0 +1,23 @@ +name: "Issue Scripts" + +on: + issue_comment: + types: [created] + +jobs: + assign-check: + runs-on: ubuntu-latest + + if: contains(github.event.comment.body, 'please assign') || contains(github.event.comment.body, 'assign me') + steps: + - uses: actions/github-script@v3 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + await github.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "We don't assign issues to external contributors. Please comment that you're working on the issue after checking that no one else is, and then send a pull request for it. Thank You!" + }) + diff --git a/docs/.github/workflows/scripts.yml b/docs/.github/workflows/scripts.yml new file mode 100644 index 00000000000..3d7c4cc0a7b --- /dev/null +++ b/docs/.github/workflows/scripts.yml @@ -0,0 +1,44 @@ +name: "Scripts" + +on: + pull_request_target: + +jobs: + dependabot-approve: + runs-on: ubuntu-latest + + if: github.actor == 'dependabot-preview[bot]' + steps: + - uses: actions/github-script@v3 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + github.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + event: 'APPROVE' + }) + + branch-check: + runs-on: ubuntu-latest + + if: (github.base_ref == 'development' && github.head_ref == 'development') || (github.base_ref == 'testing' && github.head_ref == 'testing') + steps: + - uses: actions/github-script@v3 + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + await github.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: "We don't accept pull requests from `development` or `testing` branch. Please send a pull request from some other branch. Thank You!" + }) + await github.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + state: "closed" + }) + diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000000..b56b831280d --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,32 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp + +# dependencies +/node_modules +/bower_components + +# misc +/.sass-cache +/connect.lock +/coverage/* +/libpeerconnection.log +npm-debug.log* +testem.log +/public/assets/locales/ +*~ +/kubernetes/images/frontend/deploy-dist/ +/kubernetes/images/frontend/node_Modules/ +.env +.idea +.vscode +.eslintcache +.yarnrc +.yarn + +/yarn-error.log + +# OS-X Trash +.DS_Store diff --git a/docs/.gitpod.dockerfile b/docs/.gitpod.dockerfile new file mode 100644 index 00000000000..4d477153a41 --- /dev/null +++ b/docs/.gitpod.dockerfile @@ -0,0 +1,3 @@ +FROM gitpod/workspace-full + +RUN sudo apt-get install -y gettext \ No newline at end of file diff --git a/docs/.gitpod.yml b/docs/.gitpod.yml new file mode 100644 index 00000000000..15114936c35 --- /dev/null +++ b/docs/.gitpod.yml @@ -0,0 +1,16 @@ +image: + file: .gitpod.dockerfile + +tasks: + - name: setup + init: | + nvm install 14 + nvm use 14 + yarn + cp .env.example .env + yarn l10n:generate + command: yarn start + +ports: + - port: 4200 + onOpen: open-browser diff --git a/docs/.sass-lint.yml b/docs/.sass-lint.yml new file mode 100644 index 00000000000..e0389d54513 --- /dev/null +++ b/docs/.sass-lint.yml @@ -0,0 +1,34 @@ +files: + include: 'app/styles/**/*.scss' + +options: + formatter: stylish + merge-default-rules: true + +rules: + no-important: 0 + force-element-nesting: 0 + hex-length: 2 + no-color-literals: 0 + leading-zero: 2 + space-after-comma: 2 + hex-notation: 2 + force-pseudo-nesting: 2 + quotes: 2 + empty-line-between-blocks: 2 + space-before-brace: 2 + no-ids: 2 + no-duplicate-properties: 2 + shorthand-values: 2 + variable-name-format: 2 + no-vendor-prefixes: 2 + nesting-depth: + - 2 + - + max-depth: 4 + no-qualifying-elements: + - 1 + - + allow-element-with-attribute: true + allow-element-with-class: true + property-sort-order: 0 diff --git a/docs/.template-lintrc.js b/docs/.template-lintrc.js new file mode 100644 index 00000000000..b46508b6b9b --- /dev/null +++ b/docs/.template-lintrc.js @@ -0,0 +1,20 @@ +/* jshint node:true */ +'use strict'; + +module.exports = { + extends: 'recommended', + rules: { + 'no-nested-interactive': { + ignoredTags: ['label'] // Allow label tag inside a or any other interactive element + }, + 'link-rel-noopener': true, + 'no-curly-component-invocation': true, + // TODO: Remove and fix + 'require-button-type': false, + 'no-partial': false, + 'require-valid-alt-text': false, + 'no-inline-styles': false, + 'no-negated-condition': false, + 'no-invalid-meta': false, // Crashing the linter https://github.com/ember-template-lint/ember-template-lint/pull/1087 + } +}; diff --git a/docs/.travis.yml b/docs/.travis.yml new file mode 100644 index 00000000000..9d3f5cb203b --- /dev/null +++ b/docs/.travis.yml @@ -0,0 +1,50 @@ +language: node_js + +node_js: + - "14" + +env: + - CXX=g++-4.8 JOBS=1 PATH=$PATH:${HOME}/google-cloud-sdk/bin CLOUDSDK_CORE_DISABLE_PROMPTS=1 + +cache: + yarn: true + +addons: + chrome: stable + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 + +before_install: + - export PATH="$(yarn global bin):$PATH" + +install: yarn + +script: + - touch .env + - yarn l10n:generate + - yarn lint + - COVERAGE=true yarn test + - ROOT_URL=open-event-frontend ember build -prod + # - bash scripts/test_fastboot.sh # FastBoot is disabled for now + +after_success: + - 'bash <(curl -s https://codecov.io/bash)' + +deploy: + provider: pages + skip-cleanup: true + github-token: $GH_TOKEN # Set in the settings page of your repository, as a secure variable + keep-history: false + local-dir: dist/ + on: + branch: development + +branches: + only: + - master + - development + - testing + diff --git a/docs/.watchmanconfig b/docs/.watchmanconfig new file mode 100644 index 00000000000..e7834e3e4f3 --- /dev/null +++ b/docs/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp", "dist"] +} diff --git a/docs/Architecture.md b/docs/Architecture.md new file mode 100644 index 00000000000..6dfa82b48ef --- /dev/null +++ b/docs/Architecture.md @@ -0,0 +1 @@ +![Alt text](arc.png) \ No newline at end of file diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000000..d01a133fe25 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,45 @@ +FROM node:14-alpine as builder + +WORKDIR /app + +RUN apk add git python3-dev make g++ gettext + +COPY package.json yarn.lock ./ + +RUN yarn install + +COPY . . +ARG api_host +ARG google_api_key +ENV API_HOST=$api_host +ENV GOOGLE_API_KEY=$google_api_key +RUN yarn l10n:generate && \ + touch .env && \ + JOBS=1 yarn build -prod + +## + +FROM node:14-alpine + + +WORKDIR /fastboot + +COPY --from=builder /app/dist/ dist/ + +RUN apk add --no-cache ca-certificates nginx && \ + cp dist/package.json . && \ + yarn install && \ + yarn add fastboot-app-server dotenv lodash && \ + rm -rf yarn.lock && \ + yarn cache clean + +COPY scripts/* ./scripts/ +COPY config/environment.js ./config/ + +RUN mkdir -p /etc/nginx/http.d/ +COPY config/nginx.conf /etc/nginx/http.d/ +RUN mkdir -p /run/nginx + +EXPOSE 4000 + +CMD ["sh", "scripts/container_start.sh"] diff --git a/docs/LICENSE b/docs/LICENSE new file mode 100644 index 00000000000..8dada3edaf5 --- /dev/null +++ b/docs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..cddb03dad07 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,131 @@ +# Open Event Frontend + +![Open Event Frontend](docs/images/Frontend_Branding.png) + +[![Build Status](https://github.com/fossasia/open-event-frontend/workflows/CI/badge.svg?branch=development)](https://github.com/fossasia/open-event-frontend/actions?query=workflow%3Aci) +[![Netlify](https://img.shields.io/netlify/89d57fdc-826c-400b-af13-c542e9513f62)](https://app.netlify.com/sites/open-event/deploys) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/71f37fe496e94175a5735d23e10591e8)](https://www.codacy.com/gh/fossasia/open-event-frontend/dashboard?utm_source=github.com&utm_medium=referral&utm_content=fossasia/open-event-frontend&utm_campaign=Badge_Grade) +[![codecov](https://codecov.io/gh/fossasia/open-event-frontend/branch/development/graph/badge.svg)](https://codecov.io/gh/fossasia/open-event-frontend) +[![Known Vulnerabilities](https://snyk.io/test/github/fossasia/open-event-frontend/badge.svg)](https://snyk.io/test/github/fossasia/open-event-frontend) +[![Weblate](https://hosted.weblate.org/widgets/open-event/-/frontend/svg-badge.svg)](https://hosted.weblate.org/projects/open-event/frontend/) +[![Gitter](https://img.shields.io/badge/chat-on%20gitter-ff006f.svg?style=flat-square)](https://gitter.im/fossasia/open-event-frontend) +[![Mailing](https://img.shields.io/badge/Mailing-List-red.svg)](https://groups.google.com/forum/#!forum/open-event) +[![Twitter Follow](https://img.shields.io/twitter/follow/eventyay.svg?style=social&label=Follow&maxAge=2592000?style=flat-square)](https://twitter.com/eventyay) + +The front end for the **Open Event Server** + +**API Documentation:** + +- Every installation of the **Open Event Server** project includes API docs +- A hosted version of the API docs is available in the `gh-pages` branch of the **Open Event Server** repository at [http://api.eventyay.com](http://api.eventyay.com) + +## Communication + +Please join our [Mailing list](https://groups.google.com/forum/#!forum/open-event) or [chat channel](https://gitter.im/fossasia/open-event-frontend) to get in touch with the developers. + +## Installation + +The Open Event Frontend can be easily deployed on a variety of platforms. Detailed platform specific instructions have been provided below. + +1. [Local Installation](/docs/installation/local.md) +2. [Publish to GitHub Pages](/docs/installation/Publish-to-GitHub-Pages.md) +3. [Running in Docker](/docs/installation/docker.md) + +## Running / Development + +[Click to see installation video](https://youtu.be/BNi492mJyD4) + +**Note**: Please follow [installation steps](/docs/installation/local.md#steps) listed above carefully before running + +Unfortunately, no one reads the note above, so please just run the following commands when setting up for the first time: + +- `yarn` +- `cp .env.example .env` +- `yarn l10n:generate` + +Running: + +- `yarn start` +- Visit your app at [http://localhost:4200](http://localhost:4200). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details. + +### Running Tests + +This project has acceptance, integration and unit tests located inside the `tests/` folder. + +- `yarn test` - CLI output +- `yarn test --server` - Live browser preview and console access + +### Building + +- `yarn build` (development) +- `yarn build -prod` (production) + +## Deployments, Docker images and Releases + +### Deployments + +**Master branch** + +The master branch of open-event-frontend gets deployed in a production environment at [https://eventyay.com](https://eventyay.com) +It consumes the API exposed by master branch deployment of open event server, hosted at [https://api.eventyay.com](https://api.eventyay.com) + +#### Development branch + +The **development** branch of open-event-frontend gets deployed at [https://open-event-frontend.now.sh/](https://open-event-frontend.now.sh/) +It consumes the API exposed by development branch of open event server, hosted at [https://test.eventyay.com](https://test.eventyay.com) + +### Release Cycle + +Stable versions will be released periodically, starting from version 1.0.0 when open-event-frontend went into production. Version names will follow [semantic versioning](https://semver.org/) + +### Docker Hub Images + +Docker images hosted on [open-event-frontend repository](https://cloud.docker.com/u/eventyay/repository/docker/eventyay/open-event-frontend) under eventyay organisation on docker hub are updated for each push on master and development branch. Separate tags for each version release are also maintained. They are as follows: + +| Branch/Release | Image | +| --------------- | ---------------------------------------- | +| Master | eventyay/open-event-frontend:latest | +| Development | eventyay/open-event-frontend:development | +| Version(vx.y.z) | eventyay/open-event-frontend:vx.y.z | + +## Further Reading / Useful Links + +- [ember.js](https://emberjs.com/) + +- [ember-cli](https://ember-cli.com/) + +- [Semantic UI](https://semantic-ui.com/) + +- [Semantic-UI-Ember](https://semantic-org.github.io/Semantic-UI-Ember/) + +- Development Browser Extensions + - [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + - [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) + +## Contributions Best Practices + +### Commits + +- Write clear meaningful git commit messages (Do read [https://chris.beams.io/posts/git-commit/](https://chris.beams.io/posts/git-commit/)) +- Make sure your PR's description contains GitHub's special keyword references that automatically close the related issue when the PR is merged. (More info at [https://github.com/blog/1506-closing-issues-via-pull-requests](https://github.com/blog/1506-closing-issues-via-pull-requests) ) +- When you make very minor changes to a PR of yours (like for example fixing a failing Travis build or some small style corrections or minor changes requested by reviewers) make sure you squash your commits afterward so that you don't have an absurd number of commits for a very small fix. (Learn how to squash at [https://davidwalsh.name/squash-commits-git](https://davidwalsh.name/squash-commits-git) ) +- When you're submitting a PR for a UI-related issue, it would be really awesome if you add a screenshot of your change or a link to a deployment where it can be tested out along with your PR. It makes it very easy for the reviewers and you'll also get reviews quicker. + +### Feature Requests and Bug Reports + +When you file a feature request or when you are submitting a bug report to the [issue tracker](https://github.com/fossasia/open-event-frontend/issues), make sure you add steps to reproduce it. Especially if that bug is some weird/rare one. + +### Join the development + +- Before you join development, please set up the project on your local machine, run it and go through the application completely. Press on any button you can find and see where it leads to. Explore. (Don't worry ... Nothing will happen to the app or to you due to the exploring :wink: Only thing that will happen is, you'll be more familiar with what is where and might even get some cool ideas on how to improve various aspects of the app.) +- If you would like to work on an issue, drop in a comment with the estimated completion date at the issue. If it is already assigned to someone, but there is no sign of any work being done, please feel free to drop in a comment. + +## License + +This project is currently licensed under the [Apache License version 2.0](LICENSE). + +To obtain the software under a different license, Please contact **[FOSSASIA](https://blog.fossasia.org/contact/)**. diff --git a/docs/app.json b/docs/app.json new file mode 100644 index 00000000000..f6595fecc36 --- /dev/null +++ b/docs/app.json @@ -0,0 +1,10 @@ +{ + "name": "Open Event Frontend", + "description": "Frontend for Open Event Server", + "repository": "https://github.com/fossasia/open-event-frontend", + "logo": "https://heroku-www-files.s3.amazonaws.com/getting-started/ember.svg", + "keywords": ["ember.js", "static", "open event"], + "buildpacks": [ + { "url": "https://codon-buildpacks.s3.amazonaws.com/buildpacks/heroku/emberjs.tgz" } + ] +} diff --git a/docs/app/adapters/access-code.js b/docs/app/adapters/access-code.js new file mode 100644 index 00000000000..06e2f16dc2d --- /dev/null +++ b/docs/app/adapters/access-code.js @@ -0,0 +1,13 @@ +import ApplicationAdapter from './application'; +import ENV from 'open-event-frontend/config/environment'; + +export default ApplicationAdapter.extend({ + + urlForQueryRecord(query) { + if (query && query.code && query.eventIdentifier) { + return `${ENV.APP.apiHost}/v1/events/${query.eventIdentifier}/access-codes/${query.code}`; + } else { + return this._super(...arguments); + } + } +}); diff --git a/docs/app/adapters/admin-sales-by-event.js b/docs/app/adapters/admin-sales-by-event.js new file mode 100644 index 00000000000..b2e3f58e07a --- /dev/null +++ b/docs/app/adapters/admin-sales-by-event.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-sales-by-event', 'admin/sales/by-event'); + return url; + } +}); diff --git a/docs/app/adapters/admin-sales-by-location.js b/docs/app/adapters/admin-sales-by-location.js new file mode 100644 index 00000000000..562a661171d --- /dev/null +++ b/docs/app/adapters/admin-sales-by-location.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-sales-by-locations', 'admin/sales/by-location'); + return url; + } +}); diff --git a/docs/app/adapters/admin-sales-by-marketer.js b/docs/app/adapters/admin-sales-by-marketer.js new file mode 100644 index 00000000000..cef4144d5f2 --- /dev/null +++ b/docs/app/adapters/admin-sales-by-marketer.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-sales-by-marketers', 'admin/sales/by-marketer'); + return url; + } +}); diff --git a/docs/app/adapters/admin-sales-by-organizer.js b/docs/app/adapters/admin-sales-by-organizer.js new file mode 100644 index 00000000000..0fdeb183f59 --- /dev/null +++ b/docs/app/adapters/admin-sales-by-organizer.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-sales-by-organizer', 'admin/sales/by-organizer'); + return url; + } +}); diff --git a/docs/app/adapters/admin-sales-discounted.js b/docs/app/adapters/admin-sales-discounted.js new file mode 100644 index 00000000000..bfc28d590a5 --- /dev/null +++ b/docs/app/adapters/admin-sales-discounted.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-sales-discounteds', 'admin/sales/discounted'); + return url; + } +}); diff --git a/docs/app/adapters/admin-sales-fee.js b/docs/app/adapters/admin-sales-fee.js new file mode 100644 index 00000000000..ba4d9b9be73 --- /dev/null +++ b/docs/app/adapters/admin-sales-fee.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-sales-fee', 'admin/sales/fee'); + return url; + } +}); diff --git a/docs/app/adapters/admin-sales-invoice.js b/docs/app/adapters/admin-sales-invoice.js new file mode 100644 index 00000000000..0d063a3df66 --- /dev/null +++ b/docs/app/adapters/admin-sales-invoice.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-sales-by-invoice', 'admin/sales/invoice'); + return url; + } +}); diff --git a/docs/app/adapters/admin-statistics-event.js b/docs/app/adapters/admin-statistics-event.js new file mode 100644 index 00000000000..f4821a41300 --- /dev/null +++ b/docs/app/adapters/admin-statistics-event.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-statistics-event', 'admin/statistics/event'); + return url; + } +}); diff --git a/docs/app/adapters/admin-statistics-group.js b/docs/app/adapters/admin-statistics-group.js new file mode 100644 index 00000000000..15f279b2c28 --- /dev/null +++ b/docs/app/adapters/admin-statistics-group.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-statistics-group', 'admin/statistics/group'); + return url; + } +}); diff --git a/docs/app/adapters/admin-statistics-mail.js b/docs/app/adapters/admin-statistics-mail.js new file mode 100644 index 00000000000..8f15a5ddc71 --- /dev/null +++ b/docs/app/adapters/admin-statistics-mail.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-statistics-mail', 'admin/statistics/mail'); + return url; + } +}); diff --git a/docs/app/adapters/admin-statistics-session.js b/docs/app/adapters/admin-statistics-session.js new file mode 100644 index 00000000000..c5fc6206bf2 --- /dev/null +++ b/docs/app/adapters/admin-statistics-session.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-statistics-session', 'admin/statistics/session'); + return url; + } +}); diff --git a/docs/app/adapters/admin-statistics-user.js b/docs/app/adapters/admin-statistics-user.js new file mode 100644 index 00000000000..aaa496f1b52 --- /dev/null +++ b/docs/app/adapters/admin-statistics-user.js @@ -0,0 +1,9 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + url = url.replace('admin-statistics-user', 'admin/statistics/user'); + return url; + } +}); diff --git a/docs/app/adapters/application.js b/docs/app/adapters/application.js new file mode 100644 index 00000000000..6268770e569 --- /dev/null +++ b/docs/app/adapters/application.js @@ -0,0 +1,107 @@ +import { inject as service } from '@ember/service'; +import { computed } from '@ember/object'; +import ENV from 'open-event-frontend/config/environment'; +import JSONAPIAdapter from 'ember-data/adapters/json-api'; +import HasManyQueryAdapterMixin from 'ember-data-has-many-query/mixins/rest-adapter'; +import FastbootAdapter from 'ember-data-storefront/mixins/fastboot-adapter'; + +/** + * The backend server expects the filter in a serialized string format. + * + * @param query + * @return {*} + */ +export const fixFilterQuery = query => { + if (Object.prototype.hasOwnProperty.call(query, 'filter')) { + query.filter = JSON.stringify(query.filter); + } + + return query; +}; + +export default JSONAPIAdapter.extend(HasManyQueryAdapterMixin, FastbootAdapter, { + host : ENV.APP.apiHost, + namespace : ENV.APP.apiNamespace, + + notify : service(), + session : service(), + l10n : service(), + + headers: computed('session.data.authenticated', function() { + const headers = { + 'Content-Type': 'application/vnd.api+json' + }; + const { access_token } = this.session.data.authenticated; + if (access_token) { + headers[ENV['ember-simple-auth-token'].authorizationHeaderName] = ENV['ember-simple-auth-token'].authorizationPrefix + access_token; + } + + return headers; + }), + + isInvalid(statusCode) { + if (statusCode !== 404 && statusCode !== 422 && statusCode !== 403 && statusCode !== 409) { + this.notify.error(this.l10n.t('An unexpected error has occurred.'), { + closeAfter : 5000, + id : 'serve_error' + }); + } + }, + + query(store, type, query) { + query = fixFilterQuery(query); + return this._super(store, type, query); + }, + + queryRecord(store, type, query) { + query = fixFilterQuery(query); + return this._super(store, type, query); + }, + + ajaxOptions(url, type) { + const request = this._super(...arguments); + + // The requests with public=true will not be authorized + if (type === 'GET') { + if (request.data.public) {delete request.headers[ENV['ember-simple-auth-token'].authorizationHeaderName]} + + if (ENV.noCache === 'true') { + request.data.nocache = true; + } + } + + return request; + }, + + /** + This method is called for every response that the adapter receives from the + API. If the response has a 401 status code it invalidates the session (see + {{#crossLink "SessionService/invalidate:method"}}{{/crossLink}}). + + @method handleResponse + @param {Number} status The response status as received from the API + @param {Object} headers HTTP headers as received from the API + @param {any} payload The response body as received from the API + @param {Object} requestData the original request information + @protected + */ + handleResponse(status, headers, payload, requestData) { + this.ensureResponseAuthorized(status, headers, payload, requestData); + return this._super(...arguments); + }, + + /** + The default implementation for handleResponse. + If the response has a 401 status code it invalidates the session (see + {{#crossLink "SessionService/invalidate:method"}}{{/crossLink}}). + + Override this method if you want custom invalidation logic for incoming responses. + @method ensureResponseAuthorized + @param {Number} status The response status as received from the API + */ + ensureResponseAuthorized(status) { + if (status === 401 && this.session.isAuthenticated) { + this.session.invalidate(); + } + } +}); diff --git a/docs/app/adapters/discount-code.js b/docs/app/adapters/discount-code.js new file mode 100644 index 00000000000..86e15b7d811 --- /dev/null +++ b/docs/app/adapters/discount-code.js @@ -0,0 +1,13 @@ +import ApplicationAdapter from './application'; +import ENV from 'open-event-frontend/config/environment'; + +export default ApplicationAdapter.extend({ + + urlForQueryRecord(query) { + if (query && query.code && query.eventIdentifier) { + return `${ENV.APP.apiHost}/v1/events/${query.eventIdentifier}/discount-codes/${query.code}`; + } else { + return this._super(...arguments); + } + } +}); diff --git a/docs/app/adapters/event.js b/docs/app/adapters/event.js new file mode 100644 index 00000000000..dfe33d41a76 --- /dev/null +++ b/docs/app/adapters/event.js @@ -0,0 +1,14 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + const url = this._super(modelName, id, snapshot, requestType, query); + if (requestType === 'updateRecord' && snapshot.adapterOptions?.getTrashed) { + return url + '?get_trashed=true'; + } + if (query?.upcoming) { + return url + '/upcoming'; + } + return url; + } +}); diff --git a/docs/app/adapters/group.js b/docs/app/adapters/group.js new file mode 100644 index 00000000000..47f55683449 --- /dev/null +++ b/docs/app/adapters/group.js @@ -0,0 +1,11 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + if (requestType === 'updateRecord' && snapshot.adapterOptions && snapshot.adapterOptions.getTrashed) { + url = `${url}?get_trashed=true`; + } + return url; + } +}); diff --git a/docs/app/adapters/user.js b/docs/app/adapters/user.js new file mode 100644 index 00000000000..47f55683449 --- /dev/null +++ b/docs/app/adapters/user.js @@ -0,0 +1,11 @@ +import ApplicationAdapter from './application'; + +export default ApplicationAdapter.extend({ + buildURL(modelName, id, snapshot, requestType, query) { + let url = this._super(modelName, id, snapshot, requestType, query); + if (requestType === 'updateRecord' && snapshot.adapterOptions && snapshot.adapterOptions.getTrashed) { + url = `${url}?get_trashed=true`; + } + return url; + } +}); diff --git a/docs/app/app.js b/docs/app/app.js new file mode 100644 index 00000000000..ca475dcbb0c --- /dev/null +++ b/docs/app/app.js @@ -0,0 +1,15 @@ +import Application from '@ember/application'; +import Resolver from './resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from 'open-event-frontend/config/environment'; +import './sentry'; + +const App = Application.extend({ + modulePrefix : config.modulePrefix, + podModulePrefix : config.podModulePrefix, + Resolver +}); + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/docs/app/authenticators/custom-jwt.js b/docs/app/authenticators/custom-jwt.js new file mode 100644 index 00000000000..5baf55e03a2 --- /dev/null +++ b/docs/app/authenticators/custom-jwt.js @@ -0,0 +1,66 @@ +import JWT from 'ember-simple-auth-token/authenticators/jwt'; +import { get } from '@ember/object'; +import { assign } from '@ember/polyfills'; +import { Promise } from 'rsvp'; +import { isEmpty } from '@ember/utils'; +import ENV from 'open-event-frontend/config/environment'; +import { inject as service } from '@ember/service'; + +export default JWT.extend({ + init() { + this._super(...arguments); + }, + + session: service(), + + handleAuthResponse(response, oldRefreshToken = null) { + const token = get(response, this.tokenPropertyName); + + if (isEmpty(token)) { + throw new Error('Token is empty. Please check your backend response.'); + } + + const tokenData = this.getTokenData(token); + const expiresAt = get(tokenData, this.tokenExpireName); + const tokenExpireData = {}; + + tokenExpireData[this.tokenExpireName] = expiresAt; + + if (this.tokenExpirationInvalidateSession) { + this.scheduleAccessTokenExpiration(expiresAt); + } + if (this.refreshAccessTokens) { + let refreshToken = null; + + if (oldRefreshToken) { + refreshToken = oldRefreshToken; + } else { + refreshToken = get(response, this.refreshTokenPropertyName); + } + + if (isEmpty(refreshToken)) { + throw new Error('Refresh token is empty. Please check your backend response.'); + } + + this.scheduleAccessTokenRefresh(expiresAt, refreshToken); + } + + return assign(response, tokenExpireData, { tokenData }); + }, + + refreshAccessToken(token) { + this.headers[ENV['ember-simple-auth-token'].authorizationHeaderName] = ENV['ember-simple-auth-token'].authorizationPrefix + token; + this.headers['X-CSRF-Token'] = this.session.data.authenticated.tokenData?.csrf; + + const data = this.makeRefreshData(token); + return this.makeRequest(this.serverTokenRefreshEndpoint, data, this.headers).then(response => { + const sessionData = this.handleAuthResponse(response.json, token); + sessionData.refresh_token = token; + this.trigger('sessionDataUpdated', sessionData); + return sessionData; + }).catch(error => { + this.handleTokenRefreshFail(error.status); + return Promise.reject(error); + }); + } +}); diff --git a/docs/app/components/account/application-section.js b/docs/app/components/account/application-section.js new file mode 100644 index 00000000000..95a337be883 --- /dev/null +++ b/docs/app/components/account/application-section.js @@ -0,0 +1,50 @@ +import classic from 'ember-classic-decorator'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import Component from '@ember/component'; + +@classic +export default class ApplicationSection extends Component { + @service + torii; + + isLoading = false; + + @action + auth(provider) { + try { + if (provider === 'facebook') { + this.torii.open('facebook').then(authData => { + this.set('isLoading', true); + this.loader.load(`/auth/oauth/login/${ provider }/${ authData.authorizationCode }/?redirect_uri=${ authData.redirectUri}`) + .then(async response => { + const credentials = { + 'username' : response.email, + 'password' : response.facebook_login_hash + }; + + const authenticator = 'authenticator:jwt'; + this.session + .authenticate(authenticator, credentials) + .then(async() => { + const tokenPayload = this.authManager.getTokenPayload(); + if (tokenPayload) { + this.authManager.persistCurrentUser( + await this.store.findRecord('user', tokenPayload.identity) + ); + this.set('data', this.authManager.currentUser); + } + + this.set('isLoading', false); + }); + }); + }); + } + } catch (error) { + this.notify.error(error.message, { + id: 'error_message' + }); + this.set('isLoading', false); + } + } +} diff --git a/docs/app/components/account/contact-info-section.js b/docs/app/components/account/contact-info-section.js new file mode 100644 index 00000000000..a07eb3d700d --- /dev/null +++ b/docs/app/components/account/contact-info-section.js @@ -0,0 +1,55 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { validPhoneNumber } from 'open-event-frontend/utils/validators'; + +export default Component.extend(FormMixin, { + + email : '', + phone : '', + isLoading : false, + + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + email: { + identifier : 'email', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter your email ID') + }, + { + type : 'email', + prompt : this.l10n.t('Please enter a valid email address') + } + ] + }, + phone: { + identifier : 'phone', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a phone number.') + }, + { + type : 'regExp', + value : validPhoneNumber, + prompt : this.l10n.t('Please enter a valid phone number.') + } + ] + } + } + }; + }, + + actions: { + submit() { + this.onValid(() => { + this.sendAction('submit'); + }); + } + } +}); diff --git a/docs/app/components/account/danger-zone.js b/docs/app/components/account/danger-zone.js new file mode 100644 index 00000000000..56b3437943e --- /dev/null +++ b/docs/app/components/account/danger-zone.js @@ -0,0 +1,60 @@ +import classic from 'ember-classic-decorator'; +import { action, computed } from '@ember/object'; +import Component from '@ember/component'; + +@classic +export default class DangerZone extends Component { + @computed('data.events', 'data.orders') + get isUserDeletable() { + if (this.data.events.length || this.data.orders.length) { + return false; + } + return true; + } + + @action + openDeleteUserModal(id, email) { + this.setProperties({ + 'isUserDeleteModalOpen' : true, + 'confirmEmail' : '', + 'userEmail' : email, + 'userId' : id + }); + } + + @action + openConfirmDeleteUserModal() { + this.setProperties({ + 'isUserDeleteModalOpen' : false, + 'confirmEmail' : '', + 'isConfirmUserDeleteModalOpen' : true, + 'checked' : false + }); + } + + @action + deleteUser(user) { + this.set('isLoading', true); + user.destroyRecord() + .then(() => { + this.authManager.logout(); + this.routing.transitionTo('index'); + this.notify.success(this.l10n.t('Your account has been deleted successfully.'), { + id: 'account_Delete' + }); + }) + .catch(e => { + console.error('Error while deleting account', e); + this.notify.error(this.l10n.t('An unexpected error has occurred.'), { + id: 'account_del_error' + }); + }) + .finally(() => { + this.setProperties({ + 'isLoading' : false, + 'isConfirmUserDeleteModalOpen' : false, + 'checked' : false + }); + }); + } +} diff --git a/docs/app/components/account/email-preferences-section.js b/docs/app/components/account/email-preferences-section.js new file mode 100644 index 00000000000..621119de9dc --- /dev/null +++ b/docs/app/components/account/email-preferences-section.js @@ -0,0 +1,23 @@ +import classic from 'ember-classic-decorator'; +import { action } from '@ember/object'; +import Component from '@ember/component'; + +@classic +export default class EmailPreferencesSection extends Component { + @action + savePreference(emailPreference) { + emailPreference.save() + .then(() => { + this.notify.success(this.l10n.t('Email notifications updated successfully'), { + id: 'email_notif' + }); + }) + .catch(e => { + console.error('Error while updating email notifications.', e); + emailPreference.rollbackAttributes(); + this.notify.error(this.l10n.t('An unexpected error has occurred.'), { + id: 'email_error' + }); + }); + } +} diff --git a/docs/app/components/account/password-section.js b/docs/app/components/account/password-section.js new file mode 100644 index 00000000000..a4fa2f3f708 --- /dev/null +++ b/docs/app/components/account/password-section.js @@ -0,0 +1,56 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; + +export default Component.extend(FormMixin, { + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + currentPassword: { + identifier : 'password_current', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the current password') + } + ] + }, + newPassword: { + identifier : 'password_new', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a new password') + }, + { + type : 'minLength[8]', + prompt : this.l10n.t('Your password must have at least {ruleValue} characters') + } + ] + }, + repeatPassword: { + identifier : 'password_repeat', + rules : [ + { + type : 'match[password_new]', + prompt : this.l10n.t('Passwords do not match') + } + ] + } + } + }; + }, + + actions: { + showPassword(fieldName) { + this.toggleProperty(`showPass${fieldName}`); + }, + submit() { + this.onValid(() => { + this.changePassword({ passwordCurrent: this.passwordCurrent, passwordNew: this.passwordNew }); + }); + } + } +}); diff --git a/docs/app/components/country-dropdown.ts b/docs/app/components/country-dropdown.ts new file mode 100644 index 00000000000..e043c52a2ab --- /dev/null +++ b/docs/app/components/country-dropdown.ts @@ -0,0 +1,14 @@ +import Component from '@glimmer/component'; +import { orderBy, filter } from 'lodash-es'; +import { paymentCountries } from 'open-event-frontend/utils/dictionary/payment'; +import { countries, Country } from 'open-event-frontend/utils/dictionary/demography'; + +export default class CountryDropdown extends Component { + get countries(): Country[] { + return orderBy(countries, 'name'); + } + + get paymentCountries(): Country[] { + return orderBy(filter(countries, country => paymentCountries.includes(country.code)), 'name'); + } +} diff --git a/docs/app/components/create-session-message.js b/docs/app/components/create-session-message.js new file mode 100644 index 00000000000..90cf9feb47d --- /dev/null +++ b/docs/app/components/create-session-message.js @@ -0,0 +1,24 @@ +import { tracked } from '@glimmer/tracking'; +import classic from 'ember-classic-decorator'; +import { computed } from '@ember/object'; +import Component from '@ember/component'; + +@classic +export default class CreateSessionMessage extends Component { + @tracked isMessageVisible = true; + + @computed( + 'session.isAuthenticated', + 'isMessageVisible', + 'isNewSpeaker', + 'isNewSession' + ) + get shouldShowMessage() { + const speakerIDlength = this.data.userSpeaker ? this.data.userSpeaker.toArray().length : 0; + return this.session.isAuthenticated + && this.isMessageVisible + && !this.isNewSpeaker + && this.isNewSession + && (speakerIDlength > 0); + } +} diff --git a/docs/app/components/currency-amount.js b/docs/app/components/currency-amount.js new file mode 100644 index 00000000000..173ffd98960 --- /dev/null +++ b/docs/app/components/currency-amount.js @@ -0,0 +1,25 @@ +import { inject as service } from '@ember/service'; +import Component from '@glimmer/component'; + +const locales = { + 'de' : 'de-DE', + 'zh_Hans' : 'zh-CN', + 'zh_Hant' : 'zh-TW' +}; + +export default class CurrencyAmount extends Component { + + @service l10n; + + get modifiedAmount() { + const resolvedLocale = new Intl.NumberFormat().resolvedOptions().locale; + let currentLocale = 'en'; + if (resolvedLocale === 'en-US') { + currentLocale = this.l10n.getLocale(); + } + if (!this.args.amount || !this.args.currency || this.args.amount <= 0) { + return '0.00'; + } + return (this.args.amount)?.toLocaleString(locales[currentLocale] ?? resolvedLocale, { style: 'currency', currency: this.args.currency, minimumFractionDigits: 2 }); + } +} diff --git a/docs/app/components/errors/forbidden-error.js b/docs/app/components/errors/forbidden-error.js new file mode 100644 index 00000000000..7f500991af5 --- /dev/null +++ b/docs/app/components/errors/forbidden-error.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class ForbiddenError extends Component {} diff --git a/docs/app/components/errors/generic-error.js b/docs/app/components/errors/generic-error.js new file mode 100644 index 00000000000..1756faff982 --- /dev/null +++ b/docs/app/components/errors/generic-error.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class GenericError extends Component {} diff --git a/docs/app/components/errors/not-found.js b/docs/app/components/errors/not-found.js new file mode 100644 index 00000000000..6e5cf3552a3 --- /dev/null +++ b/docs/app/components/errors/not-found.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class NotFound extends Component {} diff --git a/docs/app/components/errors/server-error.js b/docs/app/components/errors/server-error.js new file mode 100644 index 00000000000..a0d0595e1d2 --- /dev/null +++ b/docs/app/components/errors/server-error.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class ServerError extends Component {} diff --git a/docs/app/components/event-card.js b/docs/app/components/event-card.js new file mode 100644 index 00000000000..e1d76a72867 --- /dev/null +++ b/docs/app/components/event-card.js @@ -0,0 +1,78 @@ +import classic from 'ember-classic-decorator'; +import { classNames } from '@ember-decorators/component'; +import { action, computed } from '@ember/object'; +import Component from '@ember/component'; +import { forOwn } from 'lodash-es'; +import moment from 'moment-timezone'; +import { pascalCase } from 'open-event-frontend/utils/string'; + +@classic +@classNames('column') +export default class EventCard extends Component { + @computed('event.type', 'event.topic', 'event.subTopic') + get tags() { + // Unfortunately, due to the extremely dynamic and stringy nature + // of ember, the next line crashes on using object destructuring + // and we don't have time and resources to chase down issues originating + // from ember core, hence disabling the lint + const tagsOriginal = this.getProperties('event.topic.name', 'event.type.name', 'event.subTopic.name'); // eslint-disable-line ember/no-get + const tags = []; + forOwn(tagsOriginal, value => { + if (value && value.trim() !== '') { + tags.push(`#${pascalCase(value)}`); + } + }); + return tags; + } + + @computed + get eventClass() { + return this.isWide ? 'thirteen wide computer ten wide tablet sixteen wide mobile column ' + (!this.device.isMobile && 'rounded-l-none') : 'event fluid'; + } + + @computed + get isPastEvent() { + const currentTime = moment.tz(this.event.timezone); + return this.event.endsAt < currentTime; + } + + @action + selectCategory(category, subCategory) { + this.set('category', (category === this.category && !subCategory) ? null : category); + this.set('subCategory', (!subCategory || subCategory === this.subCategory) ? null : subCategory); + this.set('is_online', null); + this.setProperties({ + eventName : null, + eventType : null, + startDate : null, + endDate : null, + location : null, + ticketType : null, + cfs : null, + isOnline : null, + hasImage : null, + hasLogo : null, + isPast : null + }); + } + + @action + selectEventType(eventType) { + this.set('eventType', eventType === this.eventType ? null : eventType); + this.setProperties({ + eventName : null, + category : null, + subCategory : null, + startDate : null, + endDate : null, + location : null, + ticketType : null, + cfs : null, + isOnline : null, + hasImage : null, + hasLogo : null, + isPast : null + }); + } + +} diff --git a/docs/app/components/event-invoice/billing-info.js b/docs/app/components/event-invoice/billing-info.js new file mode 100644 index 00000000000..d99cc367a8f --- /dev/null +++ b/docs/app/components/event-invoice/billing-info.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class BillingInfo extends Component {} diff --git a/docs/app/components/event-invoice/event-info.js b/docs/app/components/event-invoice/event-info.js new file mode 100644 index 00000000000..ec1f227681e --- /dev/null +++ b/docs/app/components/event-invoice/event-info.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class EventInfo extends Component {} diff --git a/docs/app/components/event-invoice/invoice-summary.js b/docs/app/components/event-invoice/invoice-summary.js new file mode 100644 index 00000000000..fe3f7d5e4a0 --- /dev/null +++ b/docs/app/components/event-invoice/invoice-summary.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class InvoiceSummary extends Component {} diff --git a/docs/app/components/event-invoice/payee-info.js b/docs/app/components/event-invoice/payee-info.js new file mode 100644 index 00000000000..3bb4216e809 --- /dev/null +++ b/docs/app/components/event-invoice/payee-info.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class PayeeInfo extends Component {} diff --git a/docs/app/components/events/event-import-section.js b/docs/app/components/events/event-import-section.js new file mode 100644 index 00000000000..4adeca526c4 --- /dev/null +++ b/docs/app/components/events/event-import-section.js @@ -0,0 +1,39 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; + +export default Component.extend(FormMixin, { + classNames: ['ui', 'stackable', 'centered', 'grid'], + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + file: { + identifier : 'file', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please upload a file') + }, + { + type : 'regExp', + value : '/^(.*.((zip|xml|ical|ics|xcal)$))?[^.]*$/i', + prompt : this.l10n.t('Please upload a file in suggested format') + } + ] + } + } + }; + }, + actions: { + onFileSelected(files) { + this.set('files', files); + }, + submit() { + this.onValid(() => { + this.uploadFile(this.files); + }); + } + } +}); diff --git a/docs/app/components/events/imports-history-section.js b/docs/app/components/events/imports-history-section.js new file mode 100644 index 00000000000..323493dfd55 --- /dev/null +++ b/docs/app/components/events/imports-history-section.js @@ -0,0 +1,7 @@ +import classic from 'ember-classic-decorator'; +import { classNames } from '@ember-decorators/component'; +import Component from '@ember/component'; + +@classic +@classNames('ui', 'stackable', 'centered', 'grid') +export default class ImportsHistorySection extends Component {} diff --git a/docs/app/components/events/view/export/api-response.js b/docs/app/components/events/view/export/api-response.js new file mode 100644 index 00000000000..b40c9401f32 --- /dev/null +++ b/docs/app/components/events/view/export/api-response.js @@ -0,0 +1,105 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import { htmlSafe } from '@ember/string'; +import { buildUrl } from 'open-event-frontend/utils/url'; +import ENV from 'open-event-frontend/config/environment'; + +export default Component.extend({ + + didInsertElement() { + this._super(...arguments); + this.makeRequest(); + }, + + isLoading: false, + + baseUrl: computed('eventId', function() { + return `${`${ENV.APP.apiHost}/${ENV.APP.apiNamespace}/events/`}${this.eventId}`; + }), + + displayUrl: computed('eventId', function() { + return `${`${ENV.APP.apiHost}/${ENV.APP.apiNamespace}/events/`}${this.eventId}`; + }), + + toggleSwitches: { + sessions : false, + microlocations : false, + tracks : false, + speakers : false, + sponsors : false, + tickets : false + }, + + makeRequest() { + this.set('isLoading', true); + if (this.event?.state === 'draft') { + this.set('json', 'You need to publish event in order to access event information via REST API'); + this.set('isLoading', false); + return; + } + this.loader + .load(this.displayUrl, { isExternal: true }) + .then(json => { + json = JSON.stringify(json, null, 2); + this.set('json', htmlSafe(syntaxHighlight(json))); + }) + .catch(e => { + if (this.isDestroyed) { + return; + } + console.error('Error while fetching export JSON from server', e); + this.notify.error(this.l10n.t('Could not fetch from the server'), { + id: 'server_fetch_error' + }); + this.set('json', 'Could not fetch from the server'); + }) + .finally(() => { + if (this.isDestroyed) { + return; + } + this.set('isLoading', false); + }); + }, + + buildDisplayUrl() { + const newUrl = this.baseUrl; + const include = []; + + for (const key in this.toggleSwitches) { + if (Object.prototype.hasOwnProperty.call(this.toggleSwitches, key)) { + this.toggleSwitches[key] && include.push(key); + } + } + + this.set('displayUrl', buildUrl(newUrl, { + include: include.length > 0 ? include : undefined + }, true)); + }, + + actions: { + checkboxChange(data) { + this.set(`toggleSwitches.${data}`, !this.get(`toggleSwitches.${data}`)); + this.buildDisplayUrl(); + this.makeRequest(); + } + } +}); + +function syntaxHighlight(json) { + json = json.replace(/&/g, '&').replace(//g, '>'); + return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][/+\-/]?\d+)?)/g, function(match) { + let cls = 'number'; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + cls = 'key'; + } else { + cls = 'string'; + } + } else if (/true|false/.test(match)) { + cls = 'boolean'; + } else if (/null/.test(match)) { + cls = 'null'; + } + return ` ${match}`; + }); +} diff --git a/docs/app/components/events/view/export/download-common.js b/docs/app/components/events/view/export/download-common.js new file mode 100644 index 00000000000..022038ceec6 --- /dev/null +++ b/docs/app/components/events/view/export/download-common.js @@ -0,0 +1,85 @@ +import classic from 'ember-classic-decorator'; +import { action, computed } from '@ember/object'; +import Component from '@ember/component'; +import { run } from '@ember/runloop'; + +@classic +export default class DownloadCommon extends Component { + isDownloadDisabled = true; + eventDownloadUrl = ''; + isLoading = false; + + @computed + get fileFormat() { + switch (this.downloadType) { + case 'iCalendar': + return 'ical'; + case 'Pentabarf XML': + return 'pentabarf'; + case 'iCalendar XML (xCal)': + return 'xcal'; + default: + return ' '; + } + } + + @computed + get displayUrl() { + return this.get(`model.${ this.fileFormat }Url`) !== null ? this.get(`model.${ this.fileFormat }Url`) : this.l10n.t('Please publish to generate the link'); + } + + requestLoop(exportJobInfo) { + run.later(() => { + this.loader + .load(exportJobInfo.task_url, { withoutPrefix: true }) + .then(exportJobStatus => { + if (exportJobStatus.state === 'SUCCESS') { + this.set('isDownloadDisabled', false); + this.set('eventDownloadUrl', exportJobStatus.result.download_url); + this.notify.success(this.l10n.t('Download Ready'), { + id: 'down_ready' + }); + } else if (exportJobStatus.state === 'WAITING') { + this.requestLoop(exportJobInfo); + this.set('eventExportStatus', exportJobStatus.state); + this.notify.alert(this.l10n.t('Task is going on.'), { + id: 'task_progress' + }); + } else { + console.error('Event exporting task failed', exportJobStatus); + this.set('eventExportStatus', exportJobStatus.state); + this.notify.error(this.l10n.t('Task failed.'), { + id: 'task_fail' + }); + } + }) + .catch(e => { + console.error('Error while exporting event', e); + this.set('eventExportStatus', 'FAILURE'); + this.notify.error(this.l10n.t('Task failed.'), { + id: 'task_failure' + }); + }) + .finally(() => { + this.set('isLoading', false); + }); + }, 3000); + } + + @action + startExportTask() { + this.set('isLoading', true); + this.loader + .load(`/events/${this.model.id}/export/${this.fileFormat}`) + .then(exportJobInfo => { + this.requestLoop(exportJobInfo); + }) + .catch(e => { + console.error('Error while starting exporting job', e); + this.set('isLoading', false); + this.notify.error(this.l10n.t('An unexpected error has occurred.'), { + id: 'unexpected_down_error' + }); + }); + } +} diff --git a/docs/app/components/events/view/export/download-zip.js b/docs/app/components/events/view/export/download-zip.js new file mode 100644 index 00000000000..ceb630d07ab --- /dev/null +++ b/docs/app/components/events/view/export/download-zip.js @@ -0,0 +1,29 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; +import { action } from '@ember/object'; + +@classic +export default class DownloadZip extends Component { + @action + async exportEventDownload(eventDownloadUrl) { + this.set('isLoading', true); + try { + const res = await this.loader.downloadFile(eventDownloadUrl, null, { isExternal: true }); + const anchor = document.createElement('a'); + anchor.style.display = 'none'; + anchor.href = URL.createObjectURL(new Blob([res], { type: 'octet/stream' })); + anchor.download = 'EventExport.zip'; + anchor.click(); + this.notify.success(this.l10n.t('Exported Event Downloaded successfully.'), { + id: 'export_succ' + }); + } catch (e) { + console.error('Error while downloading event zip', e); + this.notify.error(e, { + id: 'err_down' + }); + } + this.set('isLoading', false); + } + +} diff --git a/docs/app/components/events/view/overview/event-apps.js b/docs/app/components/events/view/overview/event-apps.js new file mode 100644 index 00000000000..c1163d53b55 --- /dev/null +++ b/docs/app/components/events/view/overview/event-apps.js @@ -0,0 +1,14 @@ +import classic from 'ember-classic-decorator'; +import { classNames } from '@ember-decorators/component'; +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import ENV from 'open-event-frontend/config/environment'; + +@classic +@classNames('ui', 'fluid', 'card') +export default class EventApps extends Component { + @computed('eventId') + get webAppGeneratorUrl() { + return `${ENV.webAppGenerator}/?email=${this.authManager.currentUser.email}&api=${ENV.APP.apiHost}/${ENV.APP.apiNamespace}/events/${this.eventId}`; + } +} diff --git a/docs/app/components/events/view/overview/event-setup-checklist.js b/docs/app/components/events/view/overview/event-setup-checklist.js new file mode 100644 index 00000000000..6a3a88a3d5a --- /dev/null +++ b/docs/app/components/events/view/overview/event-setup-checklist.js @@ -0,0 +1,7 @@ +import classic from 'ember-classic-decorator'; +import { classNames } from '@ember-decorators/component'; +import Component from '@ember/component'; + +@classic +@classNames('ui', 'fluid', 'card') +export default class EventSetupChecklist extends Component {} diff --git a/docs/app/components/events/view/overview/event-sponsors.js b/docs/app/components/events/view/overview/event-sponsors.js new file mode 100644 index 00000000000..2a2a980ff17 --- /dev/null +++ b/docs/app/components/events/view/overview/event-sponsors.js @@ -0,0 +1,7 @@ +import classic from 'ember-classic-decorator'; +import { classNames } from '@ember-decorators/component'; +import Component from '@ember/component'; + +@classic +@classNames('ui', 'fluid', 'card') +export default class EventSponsors extends Component {} diff --git a/docs/app/components/events/view/overview/event-tickets.js b/docs/app/components/events/view/overview/event-tickets.js new file mode 100644 index 00000000000..4f7e355f247 --- /dev/null +++ b/docs/app/components/events/view/overview/event-tickets.js @@ -0,0 +1,23 @@ +import classic from 'ember-classic-decorator'; +import { classNames } from '@ember-decorators/component'; +import { computed } from '@ember/object'; +import Component from '@ember/component'; + +@classic +@classNames('ui', 'fluid', 'card') +export default class EventTickets extends Component { + @computed('data.orderStat.tickets') + get tickets() { + return this.data.orderStat.tickets.completed + this.data.orderStat.tickets.placed; + } + + @computed('data.orderStat.orders') + get orders() { + return this.data.orderStat.orders.completed + this.data.orderStat.orders.placed; + } + + @computed('data.orderStat.sales') + get sales() { + return this.data.orderStat.sales.completed + this.data.orderStat.sales.placed; + } +} diff --git a/docs/app/components/events/view/overview/general-info.js b/docs/app/components/events/view/overview/general-info.js new file mode 100644 index 00000000000..353e6577ce8 --- /dev/null +++ b/docs/app/components/events/view/overview/general-info.js @@ -0,0 +1,13 @@ +import classic from 'ember-classic-decorator'; +import { action } from '@ember/object'; +import { classNames } from '@ember-decorators/component'; +import Component from '@ember/component'; + +@classic +@classNames('ui', 'fluid', 'card') +export default class GeneralInfo extends Component { + @action + openModal() { + this.set('isEventRevisionModalOpen', true); + } +} diff --git a/docs/app/components/events/view/overview/manage-roles.js b/docs/app/components/events/view/overview/manage-roles.js new file mode 100644 index 00000000000..cc7ee4f6236 --- /dev/null +++ b/docs/app/components/events/view/overview/manage-roles.js @@ -0,0 +1,115 @@ +import { tracked } from '@glimmer/tracking'; +import classic from 'ember-classic-decorator'; +import { classNames } from '@ember-decorators/component'; +import { action, computed } from '@ember/object'; +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +@classic +@classNames('ui', 'fluid', 'card') +export default class ManageRoles extends Component { + @tracked roleType = 'accepted'; + + @service errorHandler; + + @computed('data.roleInvites.@each', 'roleType') + get roleInvites() { + return this.data.roleInvites.filterBy('status', this.roleType); + } + + @action + openAddUserRoleModal() { + const currentInvite = this.data.roleInvites.createRecord({}); + this.set('currentInvite', currentInvite); + this.set('isAddUserRoleModalOpen', true); + } + + @action + addUserRoles() { + this.set('isLoading', true); + this.currentInvite.set('roleName', this.currentInvite.get('role.name')); + this.currentInvite.save() + .then(() => { + this.data.roleInvites.addObject(this.currentInvite); + this.set('isAddUserRoleModalOpen', false); + this.notify.success(this.isNewInvite ? this.l10n.t('Role Invite sent successfully.') : this.l10n.t('Role Invite updated successfully.'), { + id: 'man_role' + }); + }) + .catch(e => { + console.error('Error while updating role invite', e); + this.errorHandler.handle(e); + }) + .finally(() => { + this.set('isLoading', false); + }); + } + + @action + async resendInvite(invite) { + this.set('isLoading', true); + try { + const res = await this.loader.post('/role-invites/' + invite.id + '/resend-invite'); + if (res.success) { + this.notify.success(this.l10n.t('Invite resent successfully.'), + { + id: 'resend_invite_succ' + }); + } else { + this.notify.error(this.l10n.t('Oops something went wrong. Please try again.')); + } + } catch (error) { + console.error('Error while resending invite', error); + this.notify.error(this.l10n.t('Oops something went wrong. Please try again.')); + } finally { + this.set('isLoading', false); + } + } + + @action + deleteUserRoleInvite(invite) { + this.set('isLoading', true); + invite.destroyRecord() + .then(() => { + this.notify.success(this.l10n.t('Role Invite deleted successfully.'), { + id: 'del_role_succ' + }); + this.data.roleInvites.removeObject(invite); + }) + .catch(e => { + console.error('Error while deleting role invite', e); + this.notify.error(this.l10n.t('Oops something went wrong. Please try again.'), { + id: 'err_man_role' + }); + }) + .finally(() => { + this.set('isLoading', false); + }); + } + + @action + deleteUserRole(eventRole) { + this.set('isLoading', true); + eventRole.destroyRecord() + .then(() => { + this.notify.success(this.l10n.t('Role deleted successfully.'), { + id: 'del_role_succ' + }); + this.data.usersEventsRoles.removeObject(eventRole); + }) + .catch(e => { + console.error('Error while deleting role', e); + this.notify.error(this.l10n.t('Oops something went wrong. Please try again.'), { + id: 'err_man_role' + }); + }) + .finally(() => { + this.set('isLoading', false); + }); + } + + @action + filter(type) { + this.set('roleType', type); + } +} diff --git a/docs/app/components/events/view/overview/speaker-session.js b/docs/app/components/events/view/overview/speaker-session.js new file mode 100644 index 00000000000..bf53c6cbd12 --- /dev/null +++ b/docs/app/components/events/view/overview/speaker-session.js @@ -0,0 +1,7 @@ +import classic from 'ember-classic-decorator'; +import { classNames } from '@ember-decorators/component'; +import Component from '@ember/component'; + +@classic +@classNames('ui', 'fluid', 'card') +export default class SpeakerSession extends Component {} diff --git a/docs/app/components/events/view/publish-bar.hbs b/docs/app/components/events/view/publish-bar.hbs new file mode 100644 index 00000000000..e0eaee5e5f1 --- /dev/null +++ b/docs/app/components/events/view/publish-bar.hbs @@ -0,0 +1,27 @@ +{{#unless this.isEventUnsaved}} + + + {{#if (eq @event.state 'published')}} + {{t 'View'}} + {{else}} + {{t 'Preview'}} + {{/if}} + +{{/unless}} + +{{#if (or this.isEventPublishable @showOnInvalid)}} + +{{/if}} + + diff --git a/docs/app/components/events/view/publish-bar.ts b/docs/app/components/events/view/publish-bar.ts new file mode 100644 index 00000000000..e5afd3268a6 --- /dev/null +++ b/docs/app/components/events/view/publish-bar.ts @@ -0,0 +1,106 @@ +import Component from '@glimmer/component'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { inject as service } from '@ember/service'; +import { isEmpty } from '@ember/utils'; +import DS from 'ember-data'; + +interface Event extends DS.Model { // eslint-disable-line ember-suave/no-direct-property-access + identifier: string | null, + state: string, + name: string | null, + tickets: any[], + isStripeConnectionValid: boolean +} + +interface EventsViewPublishBarArgs { + event: Event, + showOnInvalid: boolean, + paidTickets: boolean, + paymentMode: boolean, + onSaved: (() => void) | null, + onValidate: ((cb: (proceed: boolean) => void) => void) | null +} + +export default class EventsViewPublishBar extends Component { + @service notify: any; + @service l10n: any; + @service errorHandler: any; + + @tracked isModalOpen = false; + @tracked isLoading = false; + + get isEventPublishable(): boolean { + const { event } = this.args; + return !(event.state === 'draft' && (isEmpty(event.tickets) || isEmpty(event.name))); + } + + get isEventUnsaved(): boolean { + return !this.args.event.identifier; + } + + @action + openModal(): void { + if (!this.args.onValidate) { + this.isModalOpen = true; + } else { + this.args.onValidate(proceed => { + this.isModalOpen = proceed; + }); + } + } + + @action + async togglePublishState(): Promise { + this.isModalOpen = false; + const { event } = this.args; + const { paidTickets, paymentMode } = this.args; + const { state } = event; + if (state === 'draft') { + if (isEmpty(event.tickets)) { + this.notify.error(this.l10n.t('Your event must have tickets before it can be published.'), + { + id: 'event_tickets' + }); + return; + } else if (paidTickets) { + if (!paymentMode) { + this.notify.error(this.l10n.t('Event with paid tickets must have a payment method before publishing/saving.'), + { + id: 'event_tickets' + }); + return; + } + } else if (!event.isStripeConnectionValid) { + this.notify.error(this.l10n.t('You need to connect to your Stripe account, if you choose Stripe as a payment gateway.'), + { + id: 'event_stripe' + }); + return; + } + } + this.isLoading = true; + event.state = state === 'draft' ? 'published' : 'draft'; + try { + await event.save(); + this.args.onSaved && this.args.onSaved(); + if (state === 'draft') { + this.notify.success(this.l10n.t('Your event has been published successfully.'), + { + id: 'event_publish' + }); + } else { + this.notify.success(this.l10n.t('Your event has been unpublished.'), + { + id: 'event_unpublish' + }); + } + } catch (e) { + console.error('Error while publishing/unpublishing event', e); + event.state = state; + this.errorHandler.handle(e); + } finally { + this.isLoading = false; + } + } +} diff --git a/docs/app/components/explore/side-bar.js b/docs/app/components/explore/side-bar.js new file mode 100644 index 00000000000..c6f8ca7439d --- /dev/null +++ b/docs/app/components/explore/side-bar.js @@ -0,0 +1,250 @@ +import { tracked } from '@glimmer/tracking'; +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; +import moment from 'moment-timezone'; +import { computed, action } from '@ember/object'; +import { not } from '@ember/object/computed'; +import { getDateRanges } from 'open-event-frontend/utils/dictionary/filters'; + +@classic +export default class SideBar extends Component { + + classNames = ['ui', 'fluid', 'explore', 'vertical', 'menu']; + + customStartDate = moment().toISOString(); + + customEndDate = null; + @tracked showFilters = false; + + @tracked suggestions = []; + isMapVisible = true; + + @computed('category', 'sub_category', 'event_type', 'startDate', 'endDate', 'location', 'ticket_type', 'cfs', 'is_online', 'is_location', 'is_mixed', 'has_logo', 'has_image', 'is_past') + get hideDefaultFilters() { + return !(this.category || this.sub_category || this.event_type || this.startDate || this.endDate || this.location || this.ticket_type || this.cfs || this.is_online || this.is_location || this.is_mixed || this.has_logo || this.has_image || this.is_past); + } + + @computed('model') + get latitude() { + return this.model?.lat ? this.model.lat : 20; + } + + @computed('model') + get longitude() { + return this.model?.lon ? this.model.lon : 80; + } + + @computed('category', 'sub_category') + get showAllCategories() { + return !this.category || !this.sub_category; + } + + showAllTypes = not('event_type'); + + get dateRanges() { + return getDateRanges.bind(this)(); + } + + @computed('device.isMobile', 'showFilters') + get showFiltersOnMobile() { + return (!this.device.isMobile || this.showFilters); + } + + @action + selectLogos(val) { + this.set('has_logo', this.has_logo === val ? null : val); + } + + @action + selectImages(val) { + this.set('has_image', this.has_image === val ? null : val); + } + + @action + async suggestionsTrigger(location) { + const response = this.loader.load(`https://nominatim.openstreetmap.org/search?q=${location}&format=jsonv2&addressdetails=1`, { isExternal: true }); + const [cords] = await Promise.all([response]); + this.set('suggestions', cords); + } + + @action + async updateLocation(e) { + const location = e.target.getLatLng(); + const response = this.loader.load(`https://nominatim.openstreetmap.org/reverse?lat=${location.lat}&lon=${location.lng}&format=jsonv2`, { isExternal: true }); + const [cords] = await Promise.all([response]); + if (cords.address) { + const locationUpdated = cords.address?.state ? cords.address.state : cords.address.country; + this.set('location', locationUpdated); + } else { + this.set('location', 'singapore'); + } + } + + @action + setAutocomplete(place) { + this.set('location', place); + } + + @action + onLocationChangeHandler(lat, lng) { + this.setProperties({ + zoom: 17, + lat, + lng + }); + } + + @action + enablePastEvents(val) { + this.set('startDate', null); + this.set('endDate', null); + this.set('dateType', null); + this.set('is_past', this.is_past === val ? null : val); + } + + @action + setSettingFilter(filter) { + if (this.is_online === null && this.is_location === null && this.is_mixed === null) { + this.setProperties({ + is_location : 'true', + is_mixed : 'true', + is_online : 'true' + }); + } + if (filter === 'is_online') { + this.set('is_online', this.is_online === null ? 'true' : null); + } else if (filter === 'is_location') { + this.set('is_location', this.is_location === null ? 'true' : null); + } else { + this.set('is_mixed', this.is_mixed === null ? 'true' : null); + } + if (this.is_online && this.is_location === null && this.is_mixed === null) { + this.set('location', null); + } + } + + @action + selectCategory(category, subCategory) { + this.set('category', (category === this.category && !subCategory) ? null : category); + this.set('sub_category', (!subCategory || subCategory === this.sub_category) ? null : subCategory); + } + + @action + selectEventType(eventType) { + this.set('event_type', eventType === this.event_type ? null : eventType); + } + + @action + selectTicketType(ticketType) { + this.set('ticket_type', ticketType === this.ticket_type ? null : ticketType); + } + + @action + dateValidate(date) { + if (moment(date).isAfter(this.customEndDate)) { + this.set('customEndDate', date); + } + this.send('selectDateFilter', 'custom_dates'); + } + + @action + selectEventCfs(cfs) { + this.set('cfs', cfs === this.cfs ? null : cfs); + } + + @action + selectDateFilter(dateType) { + let newStartDate = null; + let newEndDate = null; + + if (dateType === this.dateType && dateType !== 'custom_dates') { + this.set('dateType', null); + } else { + this.set('dateType', dateType); + switch (dateType) { + case 'custom_dates': + newStartDate = this.customStartDate; + newEndDate = this.customEndDate; + break; + + case 'all_dates': + newStartDate = 'all_date'; + break; + + case 'today': + newStartDate = moment().toISOString(); + newEndDate = moment().toISOString(); + break; + + case 'tomorrow': + newStartDate = moment().add(1, 'day').toISOString(); + newEndDate = newStartDate; + break; + + case 'this_week': + newStartDate = moment().startOf('week').toISOString(); + newEndDate = moment().endOf('week').toISOString(); + break; + + case 'this_weekend': + newStartDate = moment().isoWeekday('Friday').toISOString(); + newEndDate = moment().isoWeekday('Sunday').toISOString(); + break; + + case 'next_week': + newStartDate = moment().isoWeekday('Monday').add(1, 'week').toISOString(); + newEndDate = moment().isoWeekday('Sunday').add(1, 'week').toISOString(); + break; + + case 'this_month': + newStartDate = moment().startOf('month').toISOString(); + newEndDate = moment().endOf('month').toISOString(); + break; + + default: + } + } + this.set('startDate', newStartDate); + this.set('endDate', newEndDate); + this.set('is_past', null); + } + + @action + onDateChange() { + this.send('selectDateFilter', 'custom_dates'); + } + + @action + clearFilterCategory() { + this.setProperties({ + category : null, + sub_category : null + }); + + } + + @action + clearFilterTypes() { + this.set('event_type', null); + } + + @action + clearFilters() { + this.setProperties({ + startDate : null, + endDate : null, + dateType : null + }); + this.clearQueryParams(); + } + + @action + toggleFilters() { + this.set('showFilters', !this.showFilters); + } + + @action + toggleMap() { + this.toggleProperty('isMapVisible'); + } +} diff --git a/docs/app/components/footer-main.js b/docs/app/components/footer-main.js new file mode 100644 index 00000000000..04d15681afa --- /dev/null +++ b/docs/app/components/footer-main.js @@ -0,0 +1,32 @@ +import classic from 'ember-classic-decorator'; +import { classNames, tagName } from '@ember-decorators/component'; +import { action, computed } from '@ember/object'; +import Component from '@ember/component'; +import { filterBy } from '@ember/object/computed'; +import { inject as service } from '@ember/service'; +import { sortBy } from 'lodash-es'; + +@classic +@tagName('footer') +@classNames('ui', 'inverted', 'vertical', 'footer', 'segment', 'mt-16') +export default class FooterMain extends Component { + + @service cache; + + @filterBy('pages', 'place', 'footer') + footerPages; + + @computed + get currentLocale() { + return this.l10n.getLocale(); + } + + @action + switchLanguage(locale) { + this.l10n.switchLanguage(locale); + } + + async didInsertElement() { + this.set('pages', sortBy((await this.cache.query('pages', 'page', { public: true })).toArray(), 'index')); + } +} diff --git a/docs/app/components/forms/add-tag-form.hbs b/docs/app/components/forms/add-tag-form.hbs new file mode 100644 index 00000000000..2e8cbb7151e --- /dev/null +++ b/docs/app/components/forms/add-tag-form.hbs @@ -0,0 +1,46 @@ +
+

+ {{t 'Tags'}} +

+
+
+
+ {{#each this.tagList as |tag index|}} +
+
+ +
+
+ + {{#unless tag.isReadOnly}} + + {{/unless}} + {{#if (eq index ( sub this.tagList.length 1 ) ) }} + + {{/if}} + +
+
+ +
+
+ {{/each}} +
+ +
+

{{t 'You need to hit "Submit" to save your changes.'}}

+
diff --git a/docs/app/components/forms/add-tag-form.js b/docs/app/components/forms/add-tag-form.js new file mode 100644 index 00000000000..33dd4058e50 --- /dev/null +++ b/docs/app/components/forms/add-tag-form.js @@ -0,0 +1,69 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; +import { action, computed } from '@ember/object'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; + +@classic +export default class AddTagForm extends Component.extend(FormMixin) { + @service errorHandler; + @tracked tagsDeleted = []; + + @computed('data.tags.@each.isDeleted', 'tagsDeleted.@each') + get tagList() { + return this.data.tags.filter(tag => !this.tagsDeleted.includes(tag)); + } + + willDestroyElement() { + const tagsNeedRemove = []; + this.data.tags.forEach(tag => { + if (!tag.id) { + tagsNeedRemove.pushObject(tag); + } else { + if (tag?.changedAttributes()) { + tag.rollbackAttributes(); + } + } + }); + if (tagsNeedRemove.length > 0) { + this.data.tags.removeObjects(tagsNeedRemove); + } + this._super(...arguments); + } + + @action + addItem() { + this.data.tags.pushObject(this.store.createRecord('tag')); + } + + @action + removeItem(tag) { + if (tag.id) { + this.tagsDeleted.pushObject(tag); + } else { + this.data.tags.removeObject(tag); + } + } + + @action + async submit() { + try { + this.data.tags.forEach(tag => { + if (this.tagsDeleted.includes(tag)) { + tag.deleteRecord(); + } + tag.save(); + }); + this.notify.success( + this.l10n.t('Your tag has been saved.'), + { id: 'tag_save' } + ); + } catch (error) { + this.notify.error( + this.l10n.t('Tag failed.'), + { id: 'tag_save' } + ); + } + } +} diff --git a/docs/app/components/forms/admin/content/pages-form.js b/docs/app/components/forms/admin/content/pages-form.js new file mode 100644 index 00000000000..1242a2bf827 --- /dev/null +++ b/docs/app/components/forms/admin/content/pages-form.js @@ -0,0 +1,97 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; + +export default Component.extend(FormMixin, { + + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + name: { + identifier : 'name', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a name') + } + ] + }, + title: { + identifier : 'title', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a title') + } + ] + }, + url: { + identifier : 'url', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the path') + }, + { + type : 'regExp', + value : '/[^/](.*)/', + prompt : this.l10n.t('Path should not contain leading slash.') + }, + { + type : 'doesntContain[ ]', + prompt : this.l10n.t('Path should not contain whitespaces.') + } + ] + }, + place: { + identifier : 'place', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please select a place') + } + ] + }, + position: { + identifier : 'position', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a position') + } + ] + }, + language: { + identifier : 'language', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a language') + } + ] + } + } + }; + }, + + actions: { + submit(data) { + this.onValid(() => { + this.sendAction('save', data); + }); + }, + deletePage(data) { + if (!this.isCreate) { + data.destroyRecord(); + this.set('isFormOpen', false); + } + }, + close() { + if (this.isCreate) { + this.set('isFormOpen', false); + } + } + } +}); diff --git a/docs/app/components/forms/admin/content/social-links-form.js b/docs/app/components/forms/admin/content/social-links-form.js new file mode 100644 index 00000000000..a4580d3cb24 --- /dev/null +++ b/docs/app/components/forms/admin/content/social-links-form.js @@ -0,0 +1,35 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { protocolLessValidUrlPattern } from 'open-event-frontend/utils/validators'; + +export default Component.extend(FormMixin, { + + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + support: { + identifier : 'support', + optional : 'true', + rules : [ + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid url') + } + ] + } + } + }; + }, + actions: { + submit() { + this.onValid(() => { + this.sendAction('save'); + }); + } + } + +}); diff --git a/docs/app/components/forms/admin/content/translation-form.js b/docs/app/components/forms/admin/content/translation-form.js new file mode 100644 index 00000000000..41061660b74 --- /dev/null +++ b/docs/app/components/forms/admin/content/translation-form.js @@ -0,0 +1,44 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; + +export default Component.extend(FormMixin, { + + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + file: { + identifier : 'file', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please upload a file') + }, + { + type : 'regExp', + value : '/^(.*.((po)$))?[^.]*$/i', + prompt : this.l10n.t('Please upload a file in suggested format') + } + ] + }, + language: { + identifier : 'language', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please select a language') + } + ] + } + } + }; + }, + actions: { + submit() { + this.onValid(() => { + }); + } + } +}); diff --git a/docs/app/components/forms/admin/settings/analytics-form.js b/docs/app/components/forms/admin/settings/analytics-form.js new file mode 100644 index 00000000000..15d1fe9af02 --- /dev/null +++ b/docs/app/components/forms/admin/settings/analytics-form.js @@ -0,0 +1,9 @@ +import Component from '@ember/component'; + +export default Component.extend({ + actions: { + submit() { + this.sendAction('save'); + } + } +}); diff --git a/docs/app/components/forms/admin/settings/billing.js b/docs/app/components/forms/admin/settings/billing.js new file mode 100644 index 00000000000..31004e8be13 --- /dev/null +++ b/docs/app/components/forms/admin/settings/billing.js @@ -0,0 +1,109 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { validPhoneNumber } from 'open-event-frontend/utils/validators'; +import { action } from '@ember/object'; + +@classic +export default class Billing extends Component.extend(FormMixin) { + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + adminBillingPhone: { + identifier : 'adminBillingPhone', + optional : true, + rules : [ + { + type : 'regExp', + value : validPhoneNumber, + prompt : this.l10n.t('Please enter a valid mobile number.') + } + ] + }, + adminBillingEmail: { + identifier : 'adminBillingEmail', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the email') + }, + { + type : 'email', + prompt : this.l10n.t('Please enter a valid email address') + } + ] + }, + adminBillingPaypalEmail: { + identifier : 'adminBillingPaypalEmail', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the billing paypal email invoice money will be transferred to') + }, + { + type : 'email', + prompt : this.l10n.t('Please enter a valid email address') + } + ] + }, + adminBillingCountry: { + identifier : 'adminBillingCountry', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please select the country') + } + ] + }, + adminCompany: { + identifier : 'adminCompany', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the organisation') + } + ] + }, + adminBillingAddress: { + identifier : 'adminBillingAddress', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the Address') + } + ] + }, + adminBillingCity: { + identifier : 'adminBillingCity', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the city') + } + ] + }, + + adminBillingZip: { + identifier : 'adminBillingZip', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the ZIP code') + } + ] + } + } + }; + } + + @action + submit(e) { + e.preventDefault(); + this.onValid(() => { + this.save(); + }); + } +} diff --git a/docs/app/components/forms/admin/settings/images-form.js b/docs/app/components/forms/admin/settings/images-form.js new file mode 100644 index 00000000000..8cc8dc150c4 --- /dev/null +++ b/docs/app/components/forms/admin/settings/images-form.js @@ -0,0 +1,176 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; + +export default Component.extend(FormMixin, { + + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + largeWidth: { + identifier : 'large_width', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter width') + } + ] + }, + largeHeight: { + identifier : 'large_height', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter height') + } + ] + }, + largeQuality: { + identifier : 'large_quality', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter quality') + } + ] + }, + thumbWidth: { + identifier : 'thumb_width', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter width') + } + ] + }, + thumbHeight: { + identifier : 'thumb_height', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter height') + } + ] + }, + thumbQuality: { + identifier : 'thumb_quality', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter quality') + } + ] + }, + eventIconWidth: { + identifier : 'event_icon_width', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter width') + } + ] + }, + eventIconHeight: { + identifier : 'event_icon_height', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter height') + } + ] + }, + eventIconQuality: { + identifier : 'event_icon_quality', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter quality') + } + ] + }, + logoWidth: { + identifier : 'logo_width', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter width') + } + ] + }, + logoHeight: { + identifier : 'logo_height', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter height') + } + ] + }, + profileThumbSize: { + identifier : 'profile_thumb_size', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter size') + } + ] + }, + profileThumbQuality: { + identifier : 'profile_thumb_quality', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter quality') + } + ] + }, + profileSmallSize: { + identifier : 'profile_small_size', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter size') + } + ] + }, + profileSmallQuality: { + identifier : 'profile_small_quality', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter quality') + } + ] + }, + profileIconSize: { + identifier : 'profile_icon_size', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter size') + } + ] + }, + profileIconQuality: { + identifier : 'profile_icon_quality', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter quality') + } + ] + } + } + }; + }, + + actions: { + submit() { + this.onValid(() => { + this.sendAction('save'); + }); + } + } +}); diff --git a/docs/app/components/forms/admin/settings/microservices-form.js b/docs/app/components/forms/admin/settings/microservices-form.js new file mode 100644 index 00000000000..379a4b660d4 --- /dev/null +++ b/docs/app/components/forms/admin/settings/microservices-form.js @@ -0,0 +1,45 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { protocolLessValidUrlPattern } from 'open-event-frontend/utils/validators'; + +export default Component.extend(FormMixin, { + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + androidApp: { + optional : true, + identifier : 'android_app', + rules : [ + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid URL for Android app') + } + ] + }, + + webApp: { + optional : true, + identifier : 'web_app', + rules : [ + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid URL for web app') + } + ] + } + } + }; + }, + actions: { + submit() { + this.onValid(() => { + this.sendAction('save'); + }); + } + } +}); diff --git a/docs/app/components/forms/admin/settings/payment-gateway-form.js b/docs/app/components/forms/admin/settings/payment-gateway-form.js new file mode 100644 index 00000000000..50fd8763891 --- /dev/null +++ b/docs/app/components/forms/admin/settings/payment-gateway-form.js @@ -0,0 +1,260 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import FormMixin from 'open-event-frontend/mixins/form'; +import ENV from 'open-event-frontend/config/environment'; + +export default Component.extend(FormMixin, { + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + stripeClientId: { + identifier : 'stripe_client_id', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the client ID') + } + ] + }, + + stripeSecretKey: { + identifier : 'stripe_secret_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the secret key') + } + ] + }, + + stripePublishableKey: { + identifier : 'stripe_publishable_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the publishable key') + } + ] + }, + stripeTestSecretKey: { + identifier : 'stripe_test_secret_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the secret test key') + } + ] + }, + + stripeTestPublishableKey: { + identifier : 'stripe_test_publishable_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the publishable test key') + } + ] + }, + alipaySecretKey: { + identifier : 'alipay_secret_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the secret key') + } + ] + }, + + alipayPublishableKey: { + identifier : 'alipay_publishable_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the publishable key') + } + ] + }, + + paypalSandboxClient: { + identifier : 'sandbox_client_id', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the sandbox client id') + } + ] + }, + + paypalSandboxSecret: { + identifier : 'sandbox_secret_token', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the sandbox secret token') + } + ] + }, + + paypalClient: { + identifier : 'client_id', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the live client token') + } + ] + }, + + paypalSecret: { + identifier : 'secret_token', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the live secret token') + } + ] + }, + + omiseTestPublic: { + identifier : 'test_public_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the public test key') + } + ] + }, + + omiseTestSecret: { + identifier : 'test_secret_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the secret test key') + } + ] + }, + + omiseLivePublic: { + identifier : 'live_public_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the public live key') + } + ] + }, + + omiseLiveSecret: { + identifier : 'live_secret_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the secret live key') + } + ] + }, + + paytmLiveMerchant: { + identifier : 'paytm_live_merchant', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the live merchant ID') + } + ] + }, + + paytmLiveSecret: { + identifier : 'paytm_live_secret', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the secret live key') + } + ] + }, + + paytmSandboxMerchant: { + identifier : 'paytm_sandbox_merchant', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the test merchant ID') + } + ] + }, + + paytmSandboxSecret: { + identifier : 'paytm_sandbox_secret', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the secret test key') + } + ] + } + } + }; + }, + + isCheckedStripe: computed(function() { + return this.settings.stripeClientId || this.settings.stripeTestClientId; + }), + + stripeMode: computed(function() { + return ENV.environment === 'development' || ENV.environment === 'test' ? 'debug' : 'production'; + }), + + isCheckedPaypal: computed(function() { + return this.settings.paypalSandboxClient || this.settings.paypalClient; + }), + + isCheckedOmise: computed(function() { + return this.settings.omiseTestPublic || this.settings.omiseLivePublic; + }), + + isCheckedAliPay: computed('settings.alipaySecretKey', 'settings.alipayPublishableKey', function() { + return this.settings.alipaySecretKey && this.settings.alipayPublishableKey; + }), + + actions: { + submit() { + this.onValid(() => { + if (this.isCheckedStripe === false) { + this.settings.setProperties({ + 'stripeClientId' : null, + 'stripeSecretKey' : null, + 'stripePublishableKey' : null + }); + } + if (this.isCheckedPaypal === false) { + this.settings.setProperties({ + 'paypalSandboxClient' : null, + 'paypalSandboxSecret' : null, + 'paypalSecret' : null, + 'paypalClient' : null + }); + } + if (!this.isCheckedAliPay) { + this.settings.setProperties({ + 'aliPaySecretKey' : null, + 'aliPayPublishableKey' : null + }); + } + if (this.isCheckedOmise === false) { + this.settings.setProperties({ + 'omiseTestPublic' : null, + 'omiseTestSecret' : null, + 'omiseLivePublic' : null, + 'omiseLiveSecret' : null + }); + } + this.sendAction('save'); + }); + } + } +}); diff --git a/docs/app/components/forms/admin/settings/system-form.js b/docs/app/components/forms/admin/settings/system-form.js new file mode 100644 index 00000000000..f6e0849612f --- /dev/null +++ b/docs/app/components/forms/admin/settings/system-form.js @@ -0,0 +1,229 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { protocolLessValidUrlPattern } from 'open-event-frontend/utils/validators'; + +export default Component.extend(FormMixin, { + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + appName: { + identifier : 'app_name', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the App name') + } + ] + }, + + tagLine: { + identifier : 'tag_line', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a tag line') + } + ] + }, + + apiUrl: { + identifier : 'api_url', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the API Url') + }, + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid URL for the API') + } + ] + }, + + googleStorageBucketName: { + identifier : 'google_storage_bucket_name', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the Bucket name') + } + ] + }, + + googleStorageAccessKey: { + identifier : 'google_storage_access_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the access key') + } + ] + }, + + googleStorageAccessSecret: { + identifier : 'google_storage_access_secret', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the access secret') + } + ] + }, + + amazonS3Region: { + identifier : 'amazon_s3_region', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please select a region') + } + ] + }, + + amazonS3BucketName: { + identifier : 'amazon_s3_bucket_name', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the Bucket name') + } + ] + }, + + amazonS3Key: { + identifier : 'amazon_s3_key', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the key') + } + ] + }, + + amazonS3Secret: { + identifier : 'amazon_s3_secret', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the secret') + } + ] + }, + + googlereCAPTCHAsitekey: { + identifier : 'google_recaptcha_sitekey', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the reCAPTCHA site key') + } + ] + }, + + orderExpiryTime: { + identifier : 'order_expiry_time', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a Expiry Time for Order') + }] + }, + + googlereCAPTCHAsecretkey: { + identifier : 'google_recaptcha_secretkey', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the reCAPTCHA secret key') + } + ] + }, + + emailFrom: { + identifier : 'email_from', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the from email') + }, + { + type : 'email', + prompt : this.l10n.t('Please enter a valid email address') + } + ] + }, + + emailFromName: { + identifier : 'email_from_name', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter name for from email') + } + ] + }, + + frontendUrl: { + identifier : 'frontend_url', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the Frontend Url') + }, + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid URL for Frontend') + } + ] + }, + + smtpHost: { + identifier : 'smtp_host', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the SMTP host') + } + ] + }, + + smtpPort: { + identifier : 'smtp_port', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the SMTP port number') + }, + { + type : 'integer', + prompt : this.l10n.t('Please enter a valid port number') + } + ] + }, + + sendgridToken: { + identifier : 'sendgrid_token', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the token for Sendgrid') + } + ] + } + } + }; + }, + + actions: { + submit() { + this.onValid(() => { + this.sendAction('save'); + }); + } + } +}); diff --git a/docs/app/components/forms/admin/settings/system/captcha-form.js b/docs/app/components/forms/admin/settings/system/captcha-form.js new file mode 100644 index 00000000000..2080f270d7a --- /dev/null +++ b/docs/app/components/forms/admin/settings/system/captcha-form.js @@ -0,0 +1,7 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class CaptchaForm extends Component { + +} diff --git a/docs/app/components/forms/admin/settings/system/mail-settings.js b/docs/app/components/forms/admin/settings/system/mail-settings.js new file mode 100644 index 00000000000..8b55f7d594d --- /dev/null +++ b/docs/app/components/forms/admin/settings/system/mail-settings.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class MailSettings extends Component {} diff --git a/docs/app/components/forms/admin/settings/system/mail-settings/test-email-form.js b/docs/app/components/forms/admin/settings/system/mail-settings/test-email-form.js new file mode 100644 index 00000000000..cb188e2ce89 --- /dev/null +++ b/docs/app/components/forms/admin/settings/system/mail-settings/test-email-form.js @@ -0,0 +1,54 @@ +import classic from 'ember-classic-decorator'; +import { action } from '@ember/object'; +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; + +@classic +export default class TestEmailForm extends Component.extend(FormMixin) { + getValidationRules() { + return { + inline : true, + delay : false, + on : 'blur', + fields : { + testEmail: { + identifier : 'test_email', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the recipient E-mail') + }, + { + type : 'email', + prompt : this.l10n.t('Please enter a valid email address') + } + ] + } + } + }; + } + + @action + sendTestMail() { + this.onValid(() => { + const payload = { + recipient: this.recipientEmail + }; + const config = { + skipDataTransform: true + }; + this.loader.post('/test-mail', JSON.stringify(payload), config) + .then(response => { + this.notify.success(response.message, { + id: 'succ_response_test' + }); + }) + .catch(e => { + console.error('Error while sending test email', e); + this.notify.error(this.l10n.t('An unexpected error has occurred.'), { + id: 'test_mail_err' + }); + }); + }); + } +} diff --git a/docs/app/components/forms/admin/settings/system/order-expiry-form.js b/docs/app/components/forms/admin/settings/system/order-expiry-form.js new file mode 100644 index 00000000000..46ff5e66174 --- /dev/null +++ b/docs/app/components/forms/admin/settings/system/order-expiry-form.js @@ -0,0 +1,7 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class OrderExpiryForm extends Component { + +} diff --git a/docs/app/components/forms/admin/settings/system/social-media-token.js b/docs/app/components/forms/admin/settings/system/social-media-token.js new file mode 100644 index 00000000000..35d88511ecf --- /dev/null +++ b/docs/app/components/forms/admin/settings/system/social-media-token.js @@ -0,0 +1,5 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; + +@classic +export default class SocialMediaToken extends Component {} diff --git a/docs/app/components/forms/admin/settings/system/storage-option.js b/docs/app/components/forms/admin/settings/system/storage-option.js new file mode 100644 index 00000000000..c52c3b88692 --- /dev/null +++ b/docs/app/components/forms/admin/settings/system/storage-option.js @@ -0,0 +1,13 @@ +import classic from 'ember-classic-decorator'; +import { computed } from '@ember/object'; +import Component from '@ember/component'; +import { amazonS3Regions } from 'open-event-frontend/utils/dictionary/amazon-s3-regions'; +import { orderBy } from 'lodash-es'; + +@classic +export default class StorageOption extends Component { + @computed + get regions() { + return orderBy(amazonS3Regions); + } +} diff --git a/docs/app/components/forms/admin/settings/ticket-fees-form.js b/docs/app/components/forms/admin/settings/ticket-fees-form.js new file mode 100644 index 00000000000..2d7d5132647 --- /dev/null +++ b/docs/app/components/forms/admin/settings/ticket-fees-form.js @@ -0,0 +1,78 @@ +import classic from 'ember-classic-decorator'; +import { action, computed } from '@ember/object'; +import Component from '@ember/component'; +import { countries } from 'open-event-frontend/utils/dictionary/demography'; +import { paymentCountries, paymentCurrencies } from 'open-event-frontend/utils/dictionary/payment'; +import { orderBy, filter } from 'lodash-es'; + +@classic +export default class TicketFeesForm extends Component { + @computed + get paymentCountries() { + return orderBy(filter(countries, country => paymentCountries.includes(country.code)), 'name'); + } + + @computed + get paymentCurrencies() { + return orderBy(paymentCurrencies, 'name'); + } + + @computed('model.[]') + get ticketFees() { + return this.model.filter(fees => fees.country !== 'global'); + } + + getGlobalFee() { + return this.model.filter(fees => fees.country === 'global')[0]; + } + + @computed('model') + get globalFees() { + const globalFee = this.getGlobalFee(); + if (globalFee) {return globalFee} + const globalFeeItem = this.store.createRecord('ticket-fee', { + country: 'global' + }); + this.model.toArray().addObject(globalFeeItem); + return globalFeeItem; + } + + @action + addNewTicket() { + const settings = this.model; + const incorrect_settings = settings.filter(function(setting) { + return (!setting.get('country')); + }); + if (incorrect_settings.length > 0) { + this.notify.error(this.l10n.t('Existing items need to be completed before new items can be added.'), { + id: 'existing_item' + }); + this.set('isLoading', false); + } else { + this.model.addObject(this.store.createRecord('ticket-fee', { + maximumFee : 0.0, + serviceFee : 0.0 + })); + } + } + + @action + deleteTicket(rec) { + this.set('isLoading', true); + rec.destroyRecord() + .then(() => { + this.notify.success(this.l10n.t('Fee setting deleted successfully'), { + id: 'fee_delete_succ' + }); + }) + .catch(e => { + console.error('Error while deleting ticket', e); + this.notify.error(this.l10n.t('Oops something went wrong. Please try again'), { + id: 'fee_err' + }); + }) + .finally(() => { + this.set('isLoading', false); + }); + } +} diff --git a/docs/app/components/forms/admin/video-channel-form.hbs b/docs/app/components/forms/admin/video-channel-form.hbs new file mode 100644 index 00000000000..4cb20a307fc --- /dev/null +++ b/docs/app/components/forms/admin/video-channel-form.hbs @@ -0,0 +1,41 @@ +
+

+ {{t 'Video channel details'}} +

+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+
diff --git a/docs/app/components/forms/admin/video-channel-form.ts b/docs/app/components/forms/admin/video-channel-form.ts new file mode 100644 index 00000000000..a69db0b77c5 --- /dev/null +++ b/docs/app/components/forms/admin/video-channel-form.ts @@ -0,0 +1,37 @@ +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import VideoChannel from 'open-event-frontend/models/video-channel'; + +interface Args { + videoChannel: VideoChannel +} + +export default class VideoChannelForm extends Component { + @service l10n: any; + @service router: any; + @service notify: any; + @service errorHandler: any; + + @tracked loading = false; + + + @action async save(): Promise { + try { + this.loading = true; + await this.args.videoChannel.save(); + this.router.transitionTo('admin.video.channels.index'); + this.notify.success(this.l10n.t('Your video channel has been saved.'), + { + id: 'channel_save' + }); + } catch (e) { + console.error('Error while saving video channel', e); + this.errorHandler.handle(e); + } finally { + this.loading = false; + } + } + +} diff --git a/docs/app/components/forms/document-upload.hbs b/docs/app/components/forms/document-upload.hbs new file mode 100644 index 00000000000..7558634133c --- /dev/null +++ b/docs/app/components/forms/document-upload.hbs @@ -0,0 +1,36 @@ +
+

+ {{t 'Event Documents'}} +

+ + + +
+
+
+ {{#if @event.documentLinks}} + {{#each @event.documentLinks as |document| }} +
+
+ +
+ +
+ + + +
+
+ {{/each}} + {{/if}} +
+ +
\ No newline at end of file diff --git a/docs/app/components/forms/document-upload.js b/docs/app/components/forms/document-upload.js new file mode 100644 index 00000000000..a2fc653d960 --- /dev/null +++ b/docs/app/components/forms/document-upload.js @@ -0,0 +1,39 @@ +import classic from 'ember-classic-decorator'; +import Component from '@ember/component'; +import { action } from '@ember/object'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { inject as service } from '@ember/service'; + +@classic +export default class DocumentUpload extends Component.extend(FormMixin) { + + @service errorHandler; + + @action + removeDocument(document) { + this.event.documentLinks = this.event.documentLinks.filter(dl => dl !== document); + } + + @action + addEventDocument() { + this.event.documentLinks = [...this.event.documentLinks, { name: '', link: '' }]; + } + + @action + submit(event) { + event.preventDefault(); + this.onValid(async() => { + try { + await this.event.save(); + this.notify.success(this.l10n.t('Document updated successfully'), + { + id: 'doc_upd_succ' + }); + } catch (error) { + console.error('error while saving document', error); + this.errorHandler.handle(error); + } + }); + } + +} diff --git a/docs/app/components/forms/events/view/create-access-code.js b/docs/app/components/forms/events/view/create-access-code.js new file mode 100644 index 00000000000..fad9e5053b2 --- /dev/null +++ b/docs/app/components/forms/events/view/create-access-code.js @@ -0,0 +1,158 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { later } from '@ember/runloop'; + +export default Component.extend(FormMixin, { + getValidationRules() { + window.$.fn.form.settings.rules.checkMaxMin = () => { + return this.data.minQuantity <= this.data.maxQuantity; + }; + window.$.fn.form.settings.rules.checkMaxTotal = () => { + return this.data.maxQuantity <= this.data.ticketsNumber; + }; + return { + inline : true, + delay : false, + on : 'blur', + + fields: { + accessCode: { + identifier : 'access_code', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter access code') + }, + { + type : 'regExp', + value : '^[a-zA-Z0-9_-]*$' + } + ] + }, + numberOfAccessTickets: { + identifier : 'number_of_access_tickets', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter number of tickets') + }, + { + type : 'number', + prompt : this.l10n.t('Please enter proper number of tickets') + } + ] + }, + min: { + identifier : 'min', + optional : true, + rules : [ + { + type : 'number', + prompt : this.l10n.t('Please enter the proper number') + }, + { + type : 'checkMaxMin', + prompt : this.l10n.t('Minimum value should not be greater than maximum') + } + ] + }, + max: { + identifier : 'max', + optional : true, + rules : [ + { + type : 'number', + prompt : this.l10n.t('Please enter the proper number') + }, + { + type : 'checkMaxMin', + prompt : this.l10n.t('Maximum value should not be less than minimum') + }, + { + type : 'checkMaxTotal', + prompt : this.l10n.t('Maximum value should not be greater than number of tickets') + } + ] + }, + accessCodeStartDate: { + identifier : 'start_date', + optional : true, + rules : [ + { + type : 'date', + prompt : this.l10n.t('Please enter the proper date') + } + ] + }, + accessCodeEndDate: { + identifier : 'end_date', + optional : true, + rules : [ + { + type : 'date', + prompt : this.l10n.t('Please enter the proper date') + } + ] + } + } + }; + }, + accessCode : '', + accessLink : computed('data.code', function() { + const { params } = this.router._router.currentState.routerJsState; + const origin = this.fastboot.isFastBoot ? `${this.fastboot.request.protocol}//${this.fastboot.request.host}` : location.origin; + const link = origin + this.router.urlFor('public', params['events.view'].event_id, { queryParams: { code: this.data.code } }); + this.set('data.accessUrl', link); + return link; + }), + hiddenTickets: computed.filterBy('tickets', 'isHidden', true), + + allTicketTypesChecked: computed('tickets', function() { + if (this.hiddenTickets.length && this.data.tickets.length === this.hiddenTickets.length) { + return true; + } + return false; + }), + + isLinkSuccess: false, + + actions: { + toggleAllSelection(allTicketTypesChecked) { + this.toggleProperty('allTicketTypesChecked'); + const tickets = this.hiddenTickets; + if (allTicketTypesChecked) { + this.set('data.tickets', tickets.slice()); + } else { + this.data.tickets.clear(); + } + tickets.forEach(ticket => { + ticket.set('isChecked', allTicketTypesChecked); + }); + }, + updateTicketsSelection(ticket) { + if (!ticket.get('isChecked')) { + this.data.tickets.pushObject(ticket); + ticket.set('isChecked', true); + if (this.data.tickets.length === this.hiddenTickets.length) { + this.set('allTicketTypesChecked', true); + } + } else { + this.data.tickets.removeObject(ticket); + ticket.set('isChecked', false); + this.set('allTicketTypesChecked', false); + } + }, + submit(data) { + this.onValid(() => { + this.sendAction('save', data); + }); + }, + copiedText() { + this.set('isLinkSuccess', true); + later(this, () => { + this.set('isLinkSuccess', false); + }, 5000); + } + } +}); diff --git a/docs/app/components/forms/events/view/create-discount-code.js b/docs/app/components/forms/events/view/create-discount-code.js new file mode 100644 index 00000000000..a6a0d10fd4a --- /dev/null +++ b/docs/app/components/forms/events/view/create-discount-code.js @@ -0,0 +1,181 @@ +import Component from '@ember/component'; +import { computed } from '@ember/object'; +import FormMixin from 'open-event-frontend/mixins/form'; +import { later } from '@ember/runloop'; +import { currencySymbol } from 'open-event-frontend/helpers/currency-symbol'; +export default Component.extend(FormMixin, { + getValidationRules() { + window.$.fn.form.settings.rules.checkMaxMin = () => { + return this.data.minQuantity <= this.data.maxQuantity; + }; + window.$.fn.form.settings.rules.checkMaxTotal = () => { + return this.data.maxQuantity <= this.data.ticketsNumber; + }; + window.$.fn.form.settings.rules.checkTicketSelected = () => { + const tickets = this.eventTickets; + for (const ticket of tickets) { + if (ticket.isChecked) { + return true; + } + } + return false; + }; + + // TODO: Removing the Discount Code Time Validations due to the weird and buggy behaviour. Will be restored once a perfect solution is found. Please check issue: https://github.com/fossasia/open-event-frontend/issues/3667 + return { + inline : true, + delay : false, + on : 'blur', + + fields: { + discountCode: { + identifier : 'discount_code', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a discount code') + }, + { + type : 'regExp', + value : '^[a-zA-Z0-9_-]*$' + } + ] + }, + numberOfdiscountTickets: { + identifier : 'number_of_discount_tickets', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the number of tickets') + } + ] + }, + discountAmount: { + identifier : 'discount_amount', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the discount amount') + } + ] + }, + discountPercent: { + identifier : 'discount_percent', + rules : [ + { + type : 'empty', + prompt : this.l10n.t('Please enter the discount percentage') + } + ] + }, + minOrder: { + identifier : 'min_order', + optional : true, + rules : [ + { + type : 'integer', + prompt : this.l10n.t('Please enter a valid integer') + }, + { + type : 'checkMaxMin', + prompt : this.l10n.t('Minimum value should not be greater than maximum') + } + ] + }, + maxOrder: { + identifier : 'max_order', + optional : true, + rules : [ + { + type : 'integer', + prompt : this.l10n.t('Please enter a valid integer') + }, + { + type : 'checkMaxMin', + prompt : this.l10n.t('Maximum value should not be less than minimum') + }, + { + type : 'checkMaxTotal', + prompt : this.l10n.t('Maximum value should not be greater than number of tickets') + } + ] + }, + ticketName: { + identifier : 'ticket_name', + rules : [ + { + type : 'checkTicketSelected', + prompt : this.l10n.t('Please select at least 1 ticket.') + } + ] + } + } + }; + }, + discountLink: computed('data.code', function() { + const { params } = this.router._router.currentState.routerJsState; + const origin = this.fastboot.isFastBoot ? `${this.fastboot.request.protocol}//${this.fastboot.request.host}` : location.origin; + const link = origin + this.router.urlFor('public', params['events.view'].event_id, { queryParams: { code: this.data.code } }); + this.set('data.discountUrl', link); + return link; + }), + + amountLabel: computed('event.paymentCurrency', function() { + return `Amount (${currencySymbol([this.event.paymentCurrency])})`; + }), + + eventTickets: computed('tickets', function() { + return this.tickets.filter(ticket => ticket.type === 'paidRegistration' || ticket.type === 'paid'); + }), + + allTicketTypesChecked: computed('tickets', function() { + if (this.eventTickets.length && this.data.tickets.length === this.eventTickets.length) { + return true; + } + return false; + }), + + isLinkSuccess: false, + + actions: { + toggleAllSelection(allTicketTypesChecked) { + this.toggleProperty('allTicketTypesChecked'); + const tickets = this.eventTickets; + if (allTicketTypesChecked) { + this.set('data.tickets', tickets.slice()); + } else { + this.data.tickets.clear(); + } + tickets.forEach(ticket => { + ticket.set('isChecked', allTicketTypesChecked); + }); + }, + updateTicketsSelection(ticket) { + if (!ticket.get('isChecked')) { + this.data.tickets.pushObject(ticket); + ticket.set('isChecked', true); + if (this.data.tickets.length === this.eventTickets.length) { + this.set('allTicketTypesChecked', true); + } + } else { + this.data.tickets.removeObject(ticket); + ticket.set('isChecked', false); + this.set('allTicketTypesChecked', false); + } + }, + submit(data) { + this.onValid(() => { + this.sendAction('save', data); + }); + }, + copiedText() { + this.set('isLinkSuccess', true); + later(this, () => { + this.set('isLinkSuccess', false); + }, 5000); + }, + onChange() { + this.onValid(() => {}); + } + } +}); diff --git a/docs/app/components/forms/events/view/edit-session.js b/docs/app/components/forms/events/view/edit-session.js new file mode 100644 index 00000000000..12e44002621 --- /dev/null +++ b/docs/app/components/forms/events/view/edit-session.js @@ -0,0 +1,12 @@ +import Component from '@ember/component'; +import FormMixin from 'open-event-frontend/mixins/form'; + +export default Component.extend(FormMixin, { + actions: { + submit() { + this.onValid(() => { + this.sendAction('save'); + }); + } + } +}); diff --git a/docs/app/components/forms/events/view/exhibitors/exhibitor-form.hbs b/docs/app/components/forms/events/view/exhibitors/exhibitor-form.hbs new file mode 100644 index 00000000000..4db4d8c0922 --- /dev/null +++ b/docs/app/components/forms/events/view/exhibitors/exhibitor-form.hbs @@ -0,0 +1,157 @@ +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ +
+
+
+ + + +
+ {{t 'Select Sessions'}} +
+ +
+
+
+ + +
+
+ + +
+ +

+ {{t 'Social Media'}} + + + +

+ {{#each @exhibitor.socialLinks as |socialLink|}} + {{#if (not socialLink.is_custom)}} + +
+ + + +
+
+ {{/if}} + {{/each}} +

+ {{t 'Extra Links'}} + + + +

+ {{#each @exhibitor.socialLinks as |socialLink|}} + {{#if socialLink.is_custom}} + +
+ + + +
+
+ {{/if}} + {{/each}} + + +
+
+
diff --git a/docs/app/components/forms/events/view/exhibitors/exhibitor-form.ts b/docs/app/components/forms/events/view/exhibitors/exhibitor-form.ts new file mode 100644 index 00000000000..52e7d02c4e9 --- /dev/null +++ b/docs/app/components/forms/events/view/exhibitors/exhibitor-form.ts @@ -0,0 +1,137 @@ +import Component from '@glimmer/component'; +import Exhibitor, { SocialLink } from 'open-event-frontend/models/exhibitor'; +import { protocolLessValidUrlPattern, validEmail } from 'open-event-frontend/utils/validators'; +import { inject as service } from '@ember/service'; +import { Rules } from 'open-event-frontend/components/forms/form'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import Event from 'open-event-frontend/models/event'; + +interface Args { + exhibitor: Exhibitor, + event: Event +} + +export default class ExhibitorForm extends Component { + @service l10n: any; + @service router: any; + @service notify: any; + @service errorHandler: any; + + @tracked loading = false; + + get rules(): Rules { + return { + inline : true, + delay : false, + on : 'blur', + + fields: { + name: { + rules: [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a name.') + } + ] + }, + url: { + rules: [ + { + type : 'empty', + prompt : this.l10n.t('Please enter a URL.') + }, + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid URL.') + } + ] + }, + contactEmail: { + optional : true, + rules : [ + { + type : 'email', + value : validEmail, + prompt : this.l10n.t('Please enter a valid email address.') + } + ] + }, + contactLink: { + optional : true, + rules : [ + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid contact link.') + } + ] + }, + logoUrl: { + rules: [ + { + type : 'empty', + prompt : this.l10n.t('Please upload exhibitor\'s logo.') + } + ] + }, + videoUrl: { + optional : true, + rules : [ + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid URL.') + } + ] + }, + slidesUrl: { + optional : true, + rules : [ + { + type : 'regExp', + value : protocolLessValidUrlPattern, + prompt : this.l10n.t('Please enter a valid URL.') + } + ] + } + } + }; + } + + @action addSocialLink(type: string): void { + const { exhibitor } = this.args; + if (!exhibitor.socialLinks) { + exhibitor.socialLinks = []; + } + if (type === 'customLink') { + exhibitor.socialLinks = [...exhibitor.socialLinks, { name: '', link: '', is_custom: true }]; + } else { + exhibitor.socialLinks = [...exhibitor.socialLinks, { name: '', link: '', is_custom: false }]; + } + } + + @action removeSocialLink(link: SocialLink): void { + const { exhibitor } = this.args; + exhibitor.socialLinks = exhibitor.socialLinks.filter(sl => sl !== link); + } + + @action async save(): Promise { + try { + this.loading = true; + await this.args.exhibitor.save(); + this.router.transitionTo('events.view.exhibitors', this.args.event.id); + this.notify.success(this.l10n.t('Your exhibitor has been saved.'), + { + id: 'stream_save' + }); + } catch (e) { + console.error('Error while saving exhibtor', e); + this.errorHandler.handle(e); + } finally { + this.loading = false; + } + } + +} diff --git a/docs/app/components/forms/events/view/videoroom-form.hbs b/docs/app/components/forms/events/view/videoroom-form.hbs new file mode 100644 index 00000000000..dd5c9d37c38 --- /dev/null +++ b/docs/app/components/forms/events/view/videoroom-form.hbs @@ -0,0 +1,338 @@ +
+
+
+

+ {{t 'Video Channel'}} +

+
+
+ + +
+ {{t 'Add Video'}} +
+ +
+
+
+
+
+
+
+ + +
+ {{#if (eq this.data.stream.videoChannel.provider 'bbb')}} +
+ + +
+ {{/if}} + {{#if (not-eq this.data.stream.videoChannel.provider 'bbb')}} +
+ + {{#if (eq this.data.stream.videoChannel.provider 'youtube')}} + + {{else if (eq this.data.stream.videoChannel.provider 'youtube_privacy')}} + + {{else if (eq this.data.stream.videoChannel.provider 'vimeo')}} + + {{else}} + + {{/if}} +
+
+ + +
+ {{/if}} +
+ +