├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
├── actions
│ ├── install-js-dependencies
│ │ └── action.yaml
│ └── install-python-dependencies
│ │ └── action.yaml
└── workflows
│ └── deploy.yaml
├── .gitignore
├── .idea
├── .gitignore
├── .name
├── encodings.xml
├── inspectionProfiles
│ └── profiles_settings.xml
├── job.ivelum.com.iml
├── misc.xml
├── modules.xml
├── runConfigurations
│ ├── build_production.xml
│ ├── clean.xml
│ ├── deploy.xml
│ └── start_in_dev_mode.xml
└── vcs.xml
├── .stylelintrc.json
├── LICENSE
├── README.md
├── assets
├── Allianz-guide-ru.pdf
├── flag-ru.png
├── flag-us.png
├── logo.eps
├── meetings-en.jpeg
├── meetings-ru.gif
├── t-shirt-1.png
├── t-shirt-2.jpg
└── vault-boy.png
├── babel.config.json
├── challenges
├── frontend.md
├── php.md
└── python.md
├── cloudformation.yaml
├── deploy
├── __init__.py
├── aws.py
├── settings.py
└── utils.py
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
├── jsconfig.json
├── lambda-package
├── lambda_function.py
└── requirements.txt
├── package.json
├── requirements.txt
├── run.py
├── setup.cfg
├── src
├── Jobs.js
├── _global.scss
├── _mixins.scss
├── _reset.scss
├── _typography.scss
├── _vars.scss
├── chat.js
├── components
│ ├── About.jsx
│ ├── ApplicationForm.jsx
│ ├── ApplicationForm.module.scss
│ ├── Benefits.jsx
│ ├── Button.jsx
│ ├── Button.module.scss
│ ├── Contacts.jsx
│ ├── Contacts.module.scss
│ ├── ContentForm.jsx
│ ├── CountryDropdown.jsx
│ ├── Crisp.jsx
│ ├── DeveloperForm.jsx
│ ├── ExperienceRadioField.jsx
│ ├── ExperienceRadioField.module.scss
│ ├── ExternalLink.jsx
│ ├── Field.jsx
│ ├── Field.module.scss
│ ├── FieldLabel.jsx
│ ├── FieldLabel.module.scss
│ ├── Footer.jsx
│ ├── Footer.module.scss
│ ├── FormErrorMessage.jsx
│ ├── FormErrorMessage.module.scss
│ ├── GithubButton.jsx
│ ├── GithubButton.module.scss
│ ├── GoogleForm.jsx
│ ├── GoogleForm.module.css
│ ├── HrLine.jsx
│ ├── HrLine.module.scss
│ ├── IndexVacancies.jsx
│ ├── IndexVacancies.module.scss
│ ├── InterviewProcess.jsx
│ ├── JobPage.jsx
│ ├── JobPage.module.scss
│ ├── JobTextBlock.jsx
│ ├── JobTextBlock.module.scss
│ ├── PageTitle.jsx
│ ├── PageTitle.module.scss
│ ├── Row.jsx
│ ├── Row.module.scss
│ ├── SvgImage.jsx
│ ├── TechLogos.jsx
│ ├── TechLogos.module.scss
│ ├── UXForm.jsx
│ ├── WelcomeText.jsx
│ ├── WelcomeText.module.scss
│ ├── Youtube.jsx
│ ├── Youtube.module.scss
│ └── layout
│ │ ├── Header.jsx
│ │ ├── Header.module.scss
│ │ ├── Layout.jsx
│ │ ├── Layout.module.scss
│ │ └── Metadata.jsx
├── fonts
│ ├── PT-Mono_Bold.woff
│ ├── PT-Mono_Bold.woff2
│ ├── PT-Mono_Regular.woff
│ ├── PT-Mono_Regular.woff2
│ ├── Source-Sans_Bold.woff
│ └── Source-Sans_Bold.woff2
├── hooks
│ └── use-site-metadata.js
├── images
│ ├── aws.svg
│ ├── body-background.jpg
│ ├── cat.png
│ ├── django.svg
│ ├── docker.svg
│ ├── flag-am.svg
│ ├── flag-ge.svg
│ ├── flag-kg.svg
│ ├── flag-lt.svg
│ ├── flag-md.svg
│ ├── flag-ru.svg
│ ├── flag-ua.svg
│ ├── github-button.svg
│ ├── graphql.svg
│ ├── hi-ico.png
│ ├── ico-angle-arrow.svg
│ ├── ico-chat.svg
│ ├── ico-long-arrow.svg
│ ├── ico-mail.svg
│ ├── ico-telegram.svg
│ ├── ico-youtube.svg
│ ├── kubernetes.svg
│ ├── list-dot.svg
│ ├── logo.svg
│ ├── nextjs.svg
│ ├── php.svg
│ ├── python.svg
│ ├── react.svg
│ └── wordpress.svg
└── pages
│ ├── 404.jsx
│ ├── content
│ ├── form.jsx
│ └── index.jsx
│ ├── faq.jsx
│ ├── frontend
│ ├── form.jsx
│ └── index.jsx
│ ├── index.jsx
│ ├── job-application-accepted.jsx
│ ├── php
│ ├── form.jsx
│ └── index.jsx
│ ├── python
│ ├── form.jsx
│ └── index.jsx
│ ├── ux
│ ├── form.jsx
│ └── index.jsx
│ └── wordpress
│ ├── form.jsx
│ └── index.jsx
├── static
├── apple-touch-icon.png
├── favicon.ico
├── icon-192.png
├── icon-512.png
├── icon.svg
└── site.webmanifest
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 |
8 | [*.py]
9 | indent_style = space
10 | indent_size = 4
11 | max_line_length = 80
12 |
13 | [*.{html,j2,js,jsx,json,scss,yml,yaml}]
14 | indent_style = space
15 | indent_size = 2
16 | max_line_length = 80
17 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /public
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "extends": ["airbnb"],
6 | "parser": "@babel/eslint-parser",
7 | "rules": {
8 | "func-names": 0,
9 | "import/order": ["error", {
10 | "alphabetize": {"order": "asc"},
11 | "groups": [
12 | ["builtin", "external"],
13 | ["sibling", "parent", "internal"],
14 | "index"
15 | ],
16 | "newlines-between": "always",
17 | "pathGroups": [
18 | {
19 | "pattern": "./*.scss",
20 | "group": "index"
21 | },
22 | {
23 | "pattern": "../*.scss",
24 | "group": "index"
25 | },
26 | {
27 | "pattern": "@/images/*.png",
28 | "group": "index"
29 | },
30 | {
31 | "pattern": "@/images/*.svg",
32 | "group": "index"
33 | }
34 | ],
35 | "pathGroupsExcludedImportTypes": ["builtin", "external"]
36 | }],
37 | "jsx-a11y/anchor-is-valid": ["error", {
38 | "components": ["Link"],
39 | "specialLink": ["to"],
40 | "aspects": ["noHref", "invalidHref", "preferButton"]
41 | }],
42 | "jsx-a11y/click-events-have-key-events": [1],
43 | "jsx-a11y/label-has-associated-control": "off",
44 | "lines-between-class-members": ["error", "always", {
45 | "exceptAfterSingleLine": true
46 | }],
47 | "max-len": [2, 80, 4],
48 | "no-underscore-dangle": ["error", {
49 | "allow": ["_isMounted"]
50 | }],
51 | "no-unused-expressions": ["error", {
52 | "allowTernary": true
53 | }],
54 | "no-console": "error",
55 | "react/destructuring-assignment": "off",
56 | "react/jsx-one-expression-per-line": "off",
57 | "react/forbid-prop-types": [2, { "forbid": ["any"] }],
58 | "react/jsx-props-no-spreading": "off",
59 | "react/static-property-placement": ["off", "property assignment"]
60 | },
61 | "settings": {
62 | "import/resolver": {
63 | "alias": {
64 | "map": [
65 | ["@", "./src"]
66 | ],
67 | "extensions": [".js", ".jsx", ".png", "svg"]
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/.github/actions/install-js-dependencies/action.yaml:
--------------------------------------------------------------------------------
1 | name: 'Install js dependencies'
2 | runs:
3 | using: "composite"
4 | steps:
5 | - name: Get yarn cache directory path
6 | id: yarn-cache-dir-path
7 | run: echo "::set-output name=dir::$(yarn cache dir)"
8 | shell: bash
9 | - uses: actions/cache@v2
10 | id: yarn-cache
11 | with:
12 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
13 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
14 | restore-keys: |
15 | ${{ runner.os }}-yarn-
16 | - run: yarn --prefer-offline
17 | shell: bash
18 |
--------------------------------------------------------------------------------
/.github/actions/install-python-dependencies/action.yaml:
--------------------------------------------------------------------------------
1 | name: 'Install Python dependencies'
2 | runs:
3 | using: "composite"
4 | steps:
5 | - name: update pip
6 | run: |
7 | pip install -U wheel
8 | pip install -U setuptools
9 | python -m pip install -U pip
10 | shell: bash
11 | - name: get pip cache dir
12 | id: pip-cache
13 | run: echo "::set-output name=dir::$(pip cache dir)"
14 | shell: bash
15 | - name: cache pip
16 | uses: actions/cache@v2
17 | with:
18 | path: ${{ steps.pip-cache.outputs.dir }}
19 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
20 | - run: pip install -r requirements.txt
21 | shell: bash
22 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy
2 | on:
3 | push:
4 | branches: [master]
5 | workflow_dispatch:
6 | jobs:
7 | notify-build-start:
8 | if: ${{ github.event_name == 'push' }}
9 | runs-on: ubuntu-latest
10 | steps:
11 | # Send build notifications to Slack
12 | - uses: ivelum/github-action-slack-notify-build@v1.6.0
13 | id: slack
14 | with:
15 | channel_id: G054C3DPL
16 | status: STARTED
17 | color: '#ee9b00'
18 | env:
19 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
20 | outputs:
21 | status_message_id: ${{ steps.slack.outputs.message_id }}
22 |
23 | lint:
24 | runs-on: ubuntu-latest
25 | steps:
26 | - uses: actions/checkout@v2
27 | - uses: actions/setup-node@v2
28 | with:
29 | node-version: '16'
30 | - uses: actions/setup-python@v2
31 | with:
32 | python-version: 3.9
33 |
34 | - uses: ./.github/actions/install-js-dependencies
35 | - uses: ./.github/actions/install-python-dependencies
36 |
37 | - run: yarn eslint
38 | - run: yarn stylelint
39 | - run: isort .
40 | - run: flake8 .
41 |
42 | # Send notification on check failure
43 | - name: Notify slack fail
44 | uses: ivelum/github-action-slack-notify-build@v1.6.0
45 | if: failure()
46 | env:
47 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
48 | with:
49 | channel_id: G054C3DPL
50 | status: FAILED
51 | color: '#d7263d'
52 |
53 | deploy-app:
54 | needs: lint
55 | runs-on: ubuntu-latest
56 | steps:
57 | - uses: actions/checkout@v2
58 | - uses: actions/setup-node@v2
59 | with:
60 | node-version: '16'
61 | - uses: actions/setup-python@v2
62 | with:
63 | python-version: 3.9
64 |
65 | - uses: ./.github/actions/install-js-dependencies
66 | - uses: ./.github/actions/install-python-dependencies
67 |
68 | - run: yarn build
69 | - env:
70 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
71 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
72 | run: python run.py deploy-app
73 |
74 | # Send notification on build or deploy failure
75 | - name: Notify slack fail
76 | uses: ivelum/github-action-slack-notify-build@v1.6.0
77 | if: failure()
78 | env:
79 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
80 | with:
81 | channel_id: G054C3DPL
82 | status: FAILED
83 | color: '#d7263d'
84 |
85 | deploy-lambda:
86 | needs: lint
87 | runs-on: ubuntu-latest
88 | steps:
89 | - uses: actions/checkout@v2
90 | - uses: actions/setup-python@v2
91 | with:
92 | python-version: 3.9
93 |
94 | - uses: ./.github/actions/install-python-dependencies
95 | - run: pip install --target lambda-package/dependencies -r lambda-package/requirements.txt
96 |
97 | - env:
98 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
99 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
100 | GOOGLE_API_SERVICE_ACCOUNT_INFO: ${{ secrets.GOOGLE_API_SERVICE_ACCOUNT_INFO }}
101 | GOOGLE_SPREADSHEET_ID: ${{ secrets.GOOGLE_SPREADSHEET_ID }}
102 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
103 | PIPEDRIVE_TOKEN: ${{ secrets.PIPEDRIVE_TOKEN }}
104 | run: python run.py deploy-lambda
105 |
106 | # Send notification on build or deploy failure
107 | - name: Notify slack fail
108 | uses: ivelum/github-action-slack-notify-build@v1.6.0
109 | if: failure()
110 | env:
111 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
112 | with:
113 | channel_id: G054C3DPL
114 | status: FAILED
115 | color: '#d7263d'
116 |
117 | notify-build-success:
118 | if: ${{ github.event_name == 'push' }}
119 | needs: [deploy-app, deploy-lambda, notify-build-start]
120 | runs-on: ubuntu-latest
121 | steps:
122 | # Send notification on build success
123 | - name: Notify slack success
124 | uses: ivelum/github-action-slack-notify-build@v1.6.0
125 | env:
126 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
127 | with:
128 | message_id: ${{ needs.notify-build-start.outputs.status_message_id }}
129 | channel_id: G054C3DPL
130 | status: SUCCESS
131 | color: '#16db65'
132 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache/
2 | .DS_Store
3 | node_modules/
4 | public
5 | .venv
6 | lambda-package/dependencies/
7 | lambda-package/lambda-package.zip
8 |
9 | #
10 | # See https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore
11 | #
12 |
13 | # User-specific stuff
14 | .idea/**/aws.xml
15 | .idea/**/workspace.xml
16 | .idea/**/tasks.xml
17 | .idea/**/usage.statistics.xml
18 | .idea/**/dictionaries
19 | .idea/**/shelf
20 |
21 | # Generated files
22 | .idea/$CACHE_FILE$
23 | .idea/**/contentModel.xml
24 |
25 | # Sensitive or high-churn files
26 | .idea/**/dataSources/
27 | .idea/**/dataSources.ids
28 | .idea/**/dataSources.local.xml
29 | .idea/**/sqlDataSources.xml
30 | .idea/**/dynamic.xml
31 | .idea/**/uiDesigner.xml
32 | .idea/**/dbnavigator.xml
33 |
34 | # File-based project format
35 | *.iws
36 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | job.ivelum.com
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/job.ivelum.com.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/build_production.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/clean.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/deploy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/start_in_dev_mode.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-sass-guidelines",
3 | "rules": {
4 | "max-nesting-depth": [4],
5 | "selector-max-compound-selectors": [4],
6 | "selector-no-qualifying-type": true,
7 | "function-parentheses-space-inside": "never-single-line",
8 | "max-empty-lines": [1]
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 ivelum
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [job.ivelum.com](https://job.ivelum.com)
2 |
3 | This repo hosts everything related to working at [ivelum](https://ivelum.com),
4 | including our [public wiki](http://github.com/ivelum/job/wiki/), the
5 | [code challenges](https://github.com/ivelum/job/tree/master/challenges) that we
6 | use in our job interviews, and the [job.ivelum.com](https://job.ivelum.com)
7 | source code.
8 |
9 | ## Wiki
10 |
11 |
12 |
13 | The wiki covers a wide range of topics, including our work processes,
14 | our technologies used, preferred communication style, employee benefits,
15 | etc. Available in two languages:
16 |
17 | - [English](http://github.com/ivelum/job/wiki/)
18 | - [Русский](https://github.com/ivelum/job/wiki/Home-RU)
19 |
20 | Contact us at [job@ivelum.com](mailto:job@ivelum.com)
21 |
22 | ## Code challenges
23 |
24 | - for [Python developers](https://github.com/ivelum/job/blob/master/challenges/python.md);
25 | - for [PHP developers](https://github.com/ivelum/job/blob/master/challenges/php.md);
26 | - for [Frontend developers](https://github.com/ivelum/job/blob/master/challenges/frontend.md).
27 |
28 | If you'd like to complete one of the code challenges and send us your solution,
29 | please apply for the corresponding position first at
30 | [job.ivelum.com](https://job.ivelum.com).
31 |
32 | ## [job.ivelum.com](https://job.ivelum.com) source code
33 |
34 | The instructions below describe how to work with the jobs site source code. Key
35 | technologies used:
36 |
37 | - Frontend: [Gatsby.js](https://www.gatsbyjs.com/docs/tutorial/), React, CSS
38 | modules, SCSS;
39 | - Production: AWS, S3, CloudFront, Lambda;
40 | - Deploy automation: CloudFormation, GitHub Actions, Python scripts;
41 | - Linters: ESLint, Stylelint, Flake8, isort.
42 |
43 | ### Start your development environment
44 |
45 | 1. Clone the repository:
46 |
47 | ```shell
48 | $ git clone git@github.com:ivelum/job.git
49 | ```
50 |
51 | 2. Make sure that you have [Node v15 or later](https://nodejs.org/en/) and
52 | [Yarn v1.x](https://classic.yarnpkg.com/en/) installed on your machine.
53 | Check the versions as:
54 |
55 | ```shell
56 | $ node -v
57 | v15.14.0
58 |
59 | $ yarn --version
60 | 1.22.10
61 | ```
62 | 3. Install the JS dependencies with Yarn:
63 |
64 | ```shell
65 | $ yarn
66 | ```
67 |
68 | 4. Start the development server. If you're using a JetBrains IDE, such as
69 | WebStorm or PyCharm, you can use the shared Run/Debug configuration that is
70 | included in the repo. Alternatively, start it from the command line:
71 |
72 | ```shell
73 | $ yarn start
74 | ```
75 |
76 | The command above starts the development server in the "watch" mode,
77 | automatically updating the build as you change the source code. The source
78 | code is located in the `src` folder and is based on
79 | [Gatsby](https://www.gatsbyjs.com/docs/tutorial/).
80 |
81 | ### Codestyle checks
82 |
83 | Before pushing your work to the repo, please make sure that your changes
84 | pass the code style checks. We use [ESLint](https://eslint.org) for
85 | JavaScript/React code and [Stylelint](https://stylelint.io) for SCSS.
86 | How to run the checks locally:
87 |
88 | ```shell
89 | $ yarn eslint
90 |
91 | $ yarn stylelint
92 | ```
93 |
94 | ### Pre-commit hooks for codestyle
95 |
96 | We strongly encourage you to add the code style checks in your local
97 | Git pre-commit hook. Here's how to do this:
98 |
99 | 1. create a file `.git/hooks/pre-commit` if it doesn't exist yet;
100 | 2. open this file for editing and add the following line:
101 |
102 | ```shell
103 | yarn eslint && yarn stylelint
104 | ```
105 | 3. make the file executable:
106 | ```shell
107 | $ chmod +x .git/hooks/pre-commit
108 | ```
109 |
110 | Voila, now codestyle checks will run automatically before every commit on your
111 | machine.
112 |
113 | ### Deploy to production
114 |
115 | Any push to the `master` branch in this repo triggers an automated
116 | deployment to production on AWS, which typically takes ~2 minutes plus a minute
117 | or two for the CloudFront cache to clear. You can watch the progress and build
118 | status [on the Actions tab](https://github.com/ivelum/job-form/actions).
119 |
120 | Please note that we run linters before the build, so the build will fail if the
121 | code doesn't pass [Codestyle checks](#codestyle-checks).
122 |
123 |
124 | ### Working with Python sources
125 |
126 | Python is used for deploy scripts and the lambda function, which is responsible
127 | for the job applications form processing. If you need to modify these parts:
128 |
129 | 1. Make sure that you have [Python v3.9+](https://www.python.org/downloads/)
130 | installed:
131 |
132 | ```shell
133 | $ python --version
134 | Python 3.9.7
135 | ```
136 |
137 | 2. Install the Python dependencies:
138 |
139 | ```shell
140 | $ pip install -r requirements.txt
141 | ```
142 |
143 | 3. Breathe normally and work with the source code. When you're ready to push
144 | your changes to the repo, please check the Python code style and fix any
145 | problems reported:
146 |
147 | ```shell
148 | $ flake8 . && isort .
149 | ```
150 |
151 |
152 | ## License
153 |
154 | The [job.ivelum.com](https://job.ivelum.com) source code is licensed under the
155 | MIT license. All materials in Wiki, code challenges, and public website texts
156 | are licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/).
157 |
--------------------------------------------------------------------------------
/assets/Allianz-guide-ru.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/Allianz-guide-ru.pdf
--------------------------------------------------------------------------------
/assets/flag-ru.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/flag-ru.png
--------------------------------------------------------------------------------
/assets/flag-us.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/flag-us.png
--------------------------------------------------------------------------------
/assets/logo.eps:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/logo.eps
--------------------------------------------------------------------------------
/assets/meetings-en.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/meetings-en.jpeg
--------------------------------------------------------------------------------
/assets/meetings-ru.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/meetings-ru.gif
--------------------------------------------------------------------------------
/assets/t-shirt-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/t-shirt-1.png
--------------------------------------------------------------------------------
/assets/t-shirt-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/t-shirt-2.jpg
--------------------------------------------------------------------------------
/assets/vault-boy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/assets/vault-boy.png
--------------------------------------------------------------------------------
/babel.config.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [
3 | [
4 | "@babel/plugin-proposal-decorators",
5 | {
6 | "legacy": true
7 | }
8 | ],
9 | "@babel/plugin-proposal-function-sent",
10 | "@babel/plugin-proposal-export-namespace-from",
11 | "@babel/plugin-proposal-numeric-separator",
12 | "@babel/plugin-proposal-throw-expressions",
13 | "@babel/plugin-syntax-dynamic-import",
14 | "@babel/plugin-syntax-import-meta",
15 | [
16 | "@babel/plugin-proposal-class-properties",
17 | {
18 | "loose": false
19 | }
20 | ],
21 | "@babel/plugin-proposal-json-strings"
22 | ],
23 | "presets": [
24 | [
25 | "@babel/env",
26 | {
27 | "useBuiltIns": "usage",
28 | "corejs": "3.9.1"
29 | }
30 | ],
31 | "@babel/preset-react"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/challenges/frontend.md:
--------------------------------------------------------------------------------
1 | ## Обратите внимание
2 |
3 | Перед решением тестового задания заполните, пожалуйста, небольшую форму
4 | отклика для интересующей вас вакансии по адресу:
5 |
6 | https://job.ivelum.com
7 |
8 | После заполнения анкеты мы свяжемся с вами, ответим на ваши вопросы по вакансии
9 | и расскажем подробнее о наших проектах. По результатам этого общения мы можем
10 | предложить вам попробовать сделать тестовое задание, которое описано ниже.
11 | Присылайте ваше решение на адрес [job@ivelum.com](mailto:job@ivelum.com).
12 |
13 |
14 | ## 1. GitHub code viewer
15 |
16 | Создать SPA-приложение для просмотра кода публичных репозиториев на GitHub. Чем
17 | решение будет проще, тем лучше. Достаточно реализовать самый базовый функционал,
18 | который вы сочтете разумным. Копировать интерфейс GitHub не требуется; даже
19 | наоборот - будет здорово, если ваше решение будет хоть в чем-то лучше и удобнее,
20 | чем сам GitHub.
21 |
22 | Условия:
23 | * данные должны загружаться из
24 | [GitHub GraphQL API](https://docs.github.com/en/graphql);
25 | * приложение должно быть построено на [React](https://reactjs.org), желательно
26 | последней версии;
27 | * помимо React, можно использовать любые дополнительные библиотеки и фреймворки;
28 | * система сборки проекта - любая, на ваше усмотрение. Не забудьте только
29 | добавить короткую инструкцию, как собрать проект и запустить его локально;
30 | * если задачу удалось сделать быстро, и у вас еще остался энтузиазм - как насчет
31 | написания тестов?
32 |
33 | Присылайте ваше решение в виде ссылки на публичный репозиторий на GitHub.
34 |
35 |
36 | ## Обратите внимание
37 |
38 | Напомним порядок действий:
39 |
40 | 1. Пожалуйста, заполните форму отклика на сайте https://job.ivelum.com;
41 | 2. Мы свяжемся с вами, ответим на ваши вопросы по вакансии и расскажем про наши
42 | проекты. По результатам этого общения мы можем предложить вам попробовать
43 | сделать тестовое задание, которое описано выше;
44 | 3. Присылайте ваше решение на [job@ivelum.com](mailto:job@ivelum.com).
45 |
--------------------------------------------------------------------------------
/challenges/php.md:
--------------------------------------------------------------------------------
1 | ## Обратите внимание
2 |
3 | Перед решением тестового задания заполните, пожалуйста, небольшую форму
4 | отклика для интересующей вас вакансии по адресу:
5 |
6 | https://job.ivelum.com
7 |
8 | После заполнения анкеты мы свяжемся с вами, ответим на ваши вопросы по вакансии
9 | и расскажем подробнее о наших проектах. По результатам этого общения мы можем
10 | предложить вам попробовать сделать тестовое задание, которое описано ниже.
11 | Присылайте ваше решение на адрес [job@ivelum.com](mailto:job@ivelum.com).
12 |
13 |
14 | ## 1. Hacker™ News proxy
15 |
16 | Реализовать простой http-прокси-сервер, запускаемый локально, который
17 | показывает содержимое страниц [Hacker News](https://news.ycombinator.com).
18 | Прокси должен модицифировать текст на страницах следующим образом: после
19 | каждого слова из шести букв должен стоять значок «™». Пример™:
20 |
21 | Исходный текст: https://news.ycombinator.com/item?id=13713480
22 |
23 | ```
24 | The visual description of the colliding files, at
25 | http://shattered.io/static/pdf_format.png, is not very helpful
26 | in understanding how they produced the PDFs, so I took apart
27 | the PDFs and worked it out.
28 |
29 | Basically, each PDF contains a single large (421,385-byte) JPG
30 | image, followed by a few PDF commands to display the JPG. The
31 | collision lives entirely in the JPG data - the PDF format is
32 | merely incidental here. Extracting out the two images shows two
33 | JPG files with different contents (but different SHA-1 hashes
34 | since the necessary prefix is missing). Each PDF consists of a
35 | common prefix (which contains the PDF header, JPG stream
36 | descriptor and some JPG headers), and a common suffix (containing
37 | image data and PDF display commands).
38 | ```
39 |
40 | Через ваш прокси™: http://127.0.0.1:8232/item?id=13713480
41 |
42 | ```
43 | The visual™ description of the colliding files, at
44 | http://shattered.io/static/pdf_format.png, is not very helpful
45 | in understanding how they produced the PDFs, so I took apart
46 | the PDFs and worked™ it out.
47 |
48 | Basically, each PDF contains a single™ large (421,385-byte) JPG
49 | image, followed by a few PDF commands to display the JPG. The
50 | collision lives entirely in the JPG data - the PDF format™ is
51 | merely™ incidental here. Extracting out the two images™ shows two
52 | JPG files with different contents (but different SHA-1 hashes™
53 | since the necessary prefix™ is missing). Each PDF consists of a
54 | common™ prefix™ (which contains the PDF header™, JPG stream™
55 | descriptor and some JPG headers), and a common™ suffix™ (containing
56 | image data and PDF display commands).
57 | ```
58 |
59 | Условия:
60 | * PHP 7+
61 | * страницы должны™ отображаться и работать полностью корректно, в точности так,
62 | как и оригинальные (за исключением модифицированного текста™);
63 | * при навигации по ссылкам, которые ведут на другие™ страницы HN, браузер
64 | должен™ оставаться на адресе™ вашего™ прокси™;
65 | * можно использовать любые общедоступные библиотеки, которые сочтёте нужным™;
66 | * чем меньше™ кода, тем лучше. Рекомендуем следовать PSR;
67 | * если в условиях вам не хватает каких-то данных™, опирайтесь на здравый смысл.
68 |
69 | Если задачу™ удалось сделать быстро™, и у вас еще остался энтузиазм - как
70 | насчет™ написания тестов™?
71 |
72 | Присылайте ваше решение в виде ссылки™ на gist или на публичный репозиторий на
73 | GitHub™.
74 |
75 |
76 | ## Обратите внимание
77 |
78 | Напомним порядок действий:
79 |
80 | 1. Пожалуйста, заполните форму отклика на сайте https://job.ivelum.com;
81 | 2. Мы свяжемся с вами, ответим на ваши вопросы по вакансии и расскажем про наши
82 | проекты. По результатам этого общения мы можем предложить вам попробовать
83 | сделать тестовое задание, которое описано выше;
84 | 3. Присылайте ваше решение на [job@ivelum.com](mailto:job@ivelum.com).
85 |
--------------------------------------------------------------------------------
/challenges/python.md:
--------------------------------------------------------------------------------
1 | ## Обратите внимание
2 |
3 | Перед решением тестового задания заполните, пожалуйста, небольшую форму
4 | отклика для интересующей вас вакансии по адресу:
5 |
6 | https://job.ivelum.com
7 |
8 | После заполнения анкеты мы свяжемся с вами, ответим на ваши вопросы по вакансии
9 | и расскажем подробнее о наших проектах. По результатам этого общения мы можем
10 | предложить вам попробовать сделать тестовое задание, которое описано ниже.
11 | Присылайте ваше решение на адрес [job@ivelum.com](mailto:job@ivelum.com).
12 |
13 |
14 | ## Hacker™ News proxy
15 |
16 | Реализовать простой http-прокси-сервер, запускаемый локально, который
17 | показывает содержимое страниц [Hacker News](https://news.ycombinator.com).
18 | Прокси должен модицифировать текст на страницах следующим образом: после
19 | каждого слова из шести букв должен стоять значок «™». Пример™:
20 |
21 | Исходный текст: https://news.ycombinator.com/item?id=13713480
22 |
23 | ```
24 | The visual description of the colliding files, at
25 | http://shattered.io/static/pdf_format.png, is not very helpful
26 | in understanding how they produced the PDFs, so I took apart
27 | the PDFs and worked it out.
28 |
29 | Basically, each PDF contains a single large (421,385-byte) JPG
30 | image, followed by a few PDF commands to display the JPG. The
31 | collision lives entirely in the JPG data - the PDF format is
32 | merely incidental here. Extracting out the two images shows two
33 | JPG files with different contents (but different SHA-1 hashes
34 | since the necessary prefix is missing). Each PDF consists of a
35 | common prefix (which contains the PDF header, JPG stream
36 | descriptor and some JPG headers), and a common suffix (containing
37 | image data and PDF display commands).
38 | ```
39 |
40 | Через ваш прокси™: http://127.0.0.1:8232/item?id=13713480
41 |
42 | ```
43 | The visual™ description of the colliding files, at
44 | http://shattered.io/static/pdf_format.png, is not very helpful
45 | in understanding how they produced the PDFs, so I took apart
46 | the PDFs and worked™ it out.
47 |
48 | Basically, each PDF contains a single™ large (421,385-byte) JPG
49 | image, followed by a few PDF commands to display the JPG. The
50 | collision lives entirely in the JPG data - the PDF format™ is
51 | merely™ incidental here. Extracting out the two images™ shows two
52 | JPG files with different contents (but different SHA-1 hashes™
53 | since the necessary prefix™ is missing). Each PDF consists of a
54 | common™ prefix™ (which contains the PDF header™, JPG stream™
55 | descriptor and some JPG headers), and a common™ suffix™ (containing
56 | image data and PDF display commands).
57 | ```
58 |
59 | Условия:
60 | * последняя версия™ Python™
61 | * страницы должны™ отображаться и работать полностью корректно, в точности так,
62 | как и оригинальные (за исключением модифицированного текста™);
63 | * при навигации по ссылкам, которые ведут на другие™ страницы HN, браузер
64 | должен™ оставаться на адресе™ вашего™ прокси™;
65 | * можно использовать любые общедоступные библиотеки, которые сочтёте нужным™;
66 | * чем меньше™ кода, тем лучше. PEP8 — обязательно;
67 | * если в условиях вам не хватает каких-то данных™, опирайтесь на здравый смысл.
68 |
69 | Если задачу™ удалось сделать быстро™, и у вас еще остался энтузиазм - как
70 | насчет™ написания тестов™?
71 |
72 | Присылайте ваше решение в виде ссылки™ на gist или на публичный репозиторий на
73 | GitHub™.
74 |
75 |
76 | ## Обратите внимание
77 |
78 | Напомним порядок действий:
79 |
80 | 1. Пожалуйста, заполните форму отклика на сайте https://job.ivelum.com;
81 | 2. Мы свяжемся с вами, ответим на ваши вопросы по вакансии и расскажем про наши
82 | проекты. По результатам этого общения мы можем предложить вам попробовать
83 | сделать тестовое задание, которое описано выше;
84 | 3. Присылайте ваше решение на [job@ivelum.com](mailto:job@ivelum.com).
85 |
--------------------------------------------------------------------------------
/cloudformation.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Parameters:
3 | BucketName:
4 | Type: String
5 | WebsiteCertificateARN:
6 | Type: String
7 | Resources:
8 | WebsiteBucket:
9 | Type: AWS::S3::Bucket
10 | Properties:
11 | BucketName:
12 | Ref: BucketName
13 | WebsiteConfiguration:
14 | IndexDocument: index.html
15 | ErrorDocument: 404.html
16 | WebsiteBucketPolicy:
17 | Type: AWS::S3::BucketPolicy
18 | Properties:
19 | Bucket:
20 | Ref: WebsiteBucket
21 | PolicyDocument:
22 | Version: "2012-10-17"
23 | Statement:
24 | - Action: "s3:GetObject"
25 | Effect: Allow
26 | Principal: "*"
27 | Resource:
28 | Fn::Sub: "arn:aws:s3:::${WebsiteBucket}/*"
29 | WebsiteDistribution:
30 | Type: AWS::CloudFront::Distribution
31 | DependsOn: WebsiteBucket
32 | Properties:
33 | DistributionConfig:
34 | Aliases:
35 | - job.ivelum.com
36 | CustomErrorResponses:
37 | - ErrorCode: 403
38 | ResponseCode: 404
39 | ResponsePagePath: /404.html
40 | Origins:
41 | - DomainName:
42 | Ref: WebsiteBucket
43 | Id:
44 | Ref: WebsiteBucket
45 | CustomOriginConfig:
46 | HTTPPort: 80
47 | HTTPSPort: 443
48 | OriginProtocolPolicy: 'http-only'
49 | Enabled: 'true'
50 | DefaultRootObject: index.html
51 | DefaultCacheBehavior:
52 | TargetOriginId:
53 | Ref: WebsiteBucket
54 | ViewerProtocolPolicy: 'redirect-to-https'
55 | AllowedMethods:
56 | - GET
57 | - HEAD
58 | - OPTIONS
59 | CachedMethods:
60 | - GET
61 | - HEAD
62 | - OPTIONS
63 | Compress: true
64 | ForwardedValues:
65 | QueryString: false
66 | Cookies:
67 | Forward: none
68 | Headers:
69 | - Access-Control-Request-Headers
70 | - Access-Control-Request-Method
71 | - Origin
72 | ViewerCertificate:
73 | AcmCertificateArn:
74 | Ref: WebsiteCertificateARN
75 | MinimumProtocolVersion: TLSv1.2_2018
76 | SslSupportMethod: sni-only
77 | DeployUser:
78 | Type: AWS::IAM::User
79 | Properties:
80 | ManagedPolicyArns:
81 | - arn:aws:iam::aws:policy/AWSCloudFormationReadOnlyAccess
82 | Policies:
83 | - PolicyDocument:
84 | Statement:
85 | - Action:
86 | - s3:ListBucket
87 | Effect: Allow
88 | Resource:
89 | Fn::Sub: "arn:aws:s3:::${WebsiteBucket}"
90 | - Action:
91 | - s3:*Object*
92 | Effect: Allow
93 | Resource:
94 | Fn::Sub: "arn:aws:s3:::${WebsiteBucket}/*"
95 | - Action:
96 | - cloudfront:CreateInvalidation
97 | Effect: Allow
98 | Resource: "*"
99 | - Action:
100 | - lambda:GetFunction
101 | - lambda:UpdateFunctionCode
102 | - lambda:UpdateFunctionConfiguration
103 | Effect: Allow
104 | Resource: !GetAtt JobApplicationLambda.Arn
105 | PolicyName: manage-static-website
106 | LambdaExecutionRole:
107 | Type: AWS::IAM::Role
108 | Properties:
109 | AssumeRolePolicyDocument:
110 | Version: "2012-10-17"
111 | Statement:
112 | - Effect: "Allow"
113 | Principal:
114 | Service: lambda.amazonaws.com
115 | Action: sts:AssumeRole
116 | ManagedPolicyArns:
117 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
118 | JobApplicationLambda:
119 | Type: "AWS::Lambda::Function"
120 | Properties:
121 | Handler: "lambda_function.lambda_handler"
122 | Role: !GetAtt LambdaExecutionRole.Arn
123 | Code:
124 | # use CF feature - it compares yaml config with its previous version
125 | # without looking at the actual state of the lambda function
126 | # this way we can update code, deps and env vars during deploy
127 | ZipFile: import this
128 | Runtime: "python3.9"
129 | Timeout: 10
130 | JobApplicationAPI:
131 | Type: AWS::ApiGatewayV2::Api
132 | Properties:
133 | Description: Job Application HTTP API
134 | Name: job-application-lambda-api
135 | ProtocolType: HTTP
136 | Target: !GetAtt JobApplicationLambda.Arn
137 | CorsConfiguration:
138 | AllowOrigins: ["https://job.ivelum.com"]
139 | AllowMethods: ["GET", "HEAD", "OPTIONS", "POST"]
140 | AllowHeaders: ["*"]
141 | JobApplicationAPIInvokeLambdaPermission:
142 | Type: AWS::Lambda::Permission
143 | Properties:
144 | FunctionName: !Ref JobApplicationLambda
145 | Action: lambda:InvokeFunction
146 | Principal: apigateway.amazonaws.com
147 | SourceArn: !Sub arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${JobApplicationAPI}/$default/$default
148 |
149 | Outputs:
150 | JobApplicationInvokeURL:
151 | Value: !Sub https://${JobApplicationAPI}.execute-api.${AWS::Region}.amazonaws.com
152 |
--------------------------------------------------------------------------------
/deploy/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ivelum/job/0f2363f2440780ba4ab1fdbad503cbab92470a1c/deploy/__init__.py
--------------------------------------------------------------------------------
/deploy/aws.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 |
4 | from .settings import AWS_REGION, PROJECT_NAME
5 | from .utils import run
6 |
7 |
8 | def aws_cmd(cmd, region):
9 | if os.environ.get('AWS_ACCESS_KEY_ID'):
10 | command = f'aws --region {region or AWS_REGION}'
11 | else:
12 | profile = os.environ.get('AWS_PROFILE') or PROJECT_NAME
13 | command = f'aws --profile {profile}'
14 | if region:
15 | command = f'{command} --region {region}'
16 | return f'{command} {cmd}'
17 |
18 |
19 | def aws(cmd, region=None, parse_output=True, **kwargs):
20 | """
21 | Shortcut for aws cli.
22 | """
23 | if parse_output:
24 | kwargs['capture_stdout'] = True
25 | result = run(aws_cmd(cmd, region), **kwargs)
26 | if parse_output:
27 | result = json.loads(''.join(result.stdout_lines))
28 | return result
29 |
30 |
31 | def resource_details(stack_name, logical_id):
32 | return aws(
33 | 'cloudformation describe-stack-resource --stack-name %s '
34 | '--logical-resource-id %s' % (stack_name, logical_id),
35 | )['StackResourceDetail']
36 |
--------------------------------------------------------------------------------
/deploy/settings.py:
--------------------------------------------------------------------------------
1 | AWS_REGION = 'eu-north-1'
2 |
3 | AWS_ACCOUNT_ID = '063992876227'
4 |
5 | PROJECT_NAME = 'job-form'
6 |
--------------------------------------------------------------------------------
/deploy/utils.py:
--------------------------------------------------------------------------------
1 | import json
2 | import time
3 | from functools import wraps
4 |
5 | import click
6 | import sarge
7 |
8 |
9 | def run(cmd, raise_on_error=True, capture_stdout=False, parse_json=False,
10 | print_cmd=False, **kwargs):
11 | """
12 | Wrapper around sarge.run which can raise errors and capture stdout.
13 | """
14 | if capture_stdout:
15 | kwargs['stdout'] = sarge.Capture()
16 | if raise_on_error:
17 | kwargs['stderr'] = sarge.Capture()
18 | if print_cmd:
19 | click.echo(f'->> {cmd}')
20 | result = sarge.run(cmd, **kwargs)
21 | code = result.returncode
22 | if code and raise_on_error:
23 | raise RuntimeError(
24 | 'Command failed, exit code %s - "%s":\n%s' % (
25 | code,
26 | cmd,
27 | result.stderr.read().decode(),
28 | ))
29 | result.json = None
30 | if result.stdout:
31 | result.stdout_lines = result.stdout.read().decode().split('\n')
32 | if result.stdout_lines[-1] == '':
33 | result.stdout_lines = result.stdout_lines[:-1]
34 | if parse_json:
35 | result.json = json.loads('\n'.join(result.stdout_lines))
36 | else:
37 | result.stdout_lines = []
38 | return result
39 |
40 |
41 | def timing(func):
42 | """
43 | Decorator which prints function execution time.
44 | """
45 | @wraps(func)
46 | def inner(*args, **kwargs):
47 | start = time.time()
48 | result = func(*args, **kwargs)
49 | func_args = ', '.join(
50 | [str(a) for a in args] +
51 | ['%s=%s' % (k, v) for k, v in kwargs.items()],
52 | )
53 | print('\n--- %s(%s): %0.3f sec ---\n\n' % ( # noqa T001
54 | func.__name__,
55 | func_args,
56 | time.time() - start,
57 | ))
58 | return result
59 | return inner
60 |
--------------------------------------------------------------------------------
/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Crisp from './src/components/Crisp';
4 |
5 | // eslint-disable-next-line import/prefer-default-export
6 | export const wrapPageElement = ({ element }) => (
7 | // eslint-disable-next-line react/jsx-filename-extension
8 |
9 | {element}
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteMetadata: {
3 | siteUrl: 'https://job.ivelum.com',
4 | title: 'Работа в ivelum',
5 | },
6 | trailingSlash: 'always',
7 | plugins: [
8 | 'gatsby-plugin-react-helmet',
9 | 'gatsby-plugin-sass',
10 | 'gatsby-plugin-breakpoints',
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | exports.onCreateWebpackConfig = ({ actions, getConfig }) => {
4 | const config = getConfig();
5 | const originalSvgRule = config.module.rules.find(
6 | (rule) => rule.test && rule.test.toString().includes('|svg'),
7 | );
8 | let newRuleTest = originalSvgRule.test.toString().replace('|svg|', '|');
9 | newRuleTest = newRuleTest.substring(1, newRuleTest.length - 1);
10 | originalSvgRule.test = new RegExp(newRuleTest);
11 | const newSvgRule = {
12 | test: /\.svg$/,
13 | use: [
14 | {
15 | loader: '@svgr/webpack',
16 | options: {
17 | svgoConfig: {
18 | plugins: [
19 | {
20 | name: 'preset-default',
21 | params: {
22 | overrides: {
23 | // disable the plugin
24 | removeViewBox: false,
25 | },
26 | },
27 | },
28 | ],
29 | },
30 | },
31 | },
32 | 'url-loader',
33 | ],
34 | };
35 | config.module.rules.push(newSvgRule);
36 | actions.replaceWebpackConfig(config);
37 | actions.setWebpackConfig({
38 | resolve: {
39 | alias: {
40 | '@': path.resolve(__dirname, 'src'),
41 | },
42 | },
43 | });
44 | };
45 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/*"]
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lambda-package/lambda_function.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os
3 | import sys
4 | from json import JSONDecodeError
5 |
6 | from pipedrive.client import Client
7 |
8 |
9 | PIPEDRIVE_CUSTOM_FIELD_FALLBACK = "6244292b70f654e8adb467e7c4b6e417c59099a8"
10 |
11 | PERSON_FIELDS_MAP = {
12 | 'skype': '4717d3279137c308cc6aa9d67a48622ab596b7ae',
13 | 'whatsapp': 'b117a397d11ea09aacbb47d98a5f371e8c2bc8c3',
14 | 'telegram': '6ab6aa18532570c3f6799700ec3c9d7cca1e4d28',
15 | }
16 |
17 | CUSTOM_FIELDS_MAP = {
18 | "city": "f7731ae79d66ce127edafbea25acc23d741964d1",
19 | "country": "76d376013db86003c179c2cf97035dd001e6f578",
20 | "portfolio": "bcc3e04c1f33c099e2ee03ad6b7f4e5789cc8f10",
21 | "education": "2322c3b704ededc65e25c65fc4fdef436b2d7a4a",
22 | "english": "6ffd54d9947c4a749d5794205c4457ab6a4c13fd",
23 | "lovedTasks": "8808eb21eb97c4de0e2124a0ebc2f51caa616daf",
24 | "unlovedTasks": "9d8ec5f27d6f04082e81b9e7742bb1b394ef752a",
25 | "experiencedjango": "dda69ba4f52302a1ca0e5db4c3a2a7fea3f4fd5b",
26 | "experienceposition": "b4dc419032b8de6f7f1d67c2c12ec2998200143b",
27 | "linuxCommands": "853591293300cce011d4549f92ea7cc8e54d8c35",
28 | "experiencedb": "db5f0991b4f13b1ad3802274d9a764449564c9d0",
29 | "experiencetests": "17a9192a295f3a0dd3248a02729e190fa979b601",
30 | "experiencelinux": "59818648fcbc5a5b2552d09d25c67ebca0221cf2",
31 | "experiencerequirements": "b5011e781a2e6a6eb4f04a1041fef86d13447173",
32 | "experiencefrontend": "32a1c8173da00a92cac9de5dde834ebb33a7f967",
33 | "experiencepython": "6e182fd384f109671c83cb631de95a2bd8fe711f",
34 | "specializedExperience": "273b41a8ee293c877ffe4dbb784451a16d429f95",
35 | "experiencephp": "3c6b10339c72dec10796ec6bcd7a28c5a99fdd56",
36 | "experiencewordpress": "6fce4f306ae12b0a25bc424e66649013d3d121ac",
37 | "experienceOverall": "9c59f474228e1d86dfb0219e77b0553e9d01b55f",
38 | "recommendedBy": "14379c7424c02e6c22109bbb364c0a29a7cd9692",
39 | "referrer": "a97a50f0b9a4cb5386f687774370b951356669a2",
40 | "experiencerereact": "e800164efb4ad750453a0ed100f1253c90dc0db9",
41 | "experiencejs": "d845ea966017a800c33daf960ad25217da87f9bd",
42 | "experiencemarkup": "5319f706a4121961b69e80f9f6bb305c5a92acea",
43 | "experiencefigma": "3937cc3bcf16f74704d88ddd5db325cd966bf69e",
44 | "experiencedesign": "4ba10f18f6b51eb1121cf8b3364a0e1f67e09bfc",
45 | "experienceusability": "bee1537c8a8e9a4b056f13423812be4661663275",
46 | }
47 |
48 |
49 | def pipedrive_get_or_create_person(client, form_person):
50 | resp = client.persons.search_persons({
51 | "term": form_person["email"],
52 | "exact_match": 1,
53 | "fields": "email",
54 | "limit": 1,
55 | })
56 |
57 | if len(resp['data']['items']):
58 | result = resp['data']['items'][0]['item']
59 | print(f"Person found: {result['id']}")
60 | return result
61 |
62 | resp = client.persons.create_person(form_person)
63 | print(f"Person created {resp['data']['id']}")
64 | return resp['data']
65 |
66 |
67 | def pipedrive_create_deals(client, job, person, data):
68 | cf_mapping = CUSTOM_FIELDS_MAP.copy()
69 |
70 | cf_data = {
71 | PIPEDRIVE_CUSTOM_FIELD_FALLBACK: '',
72 | }
73 |
74 | for k, v in data.items():
75 | if k in cf_mapping:
76 | cf_data[cf_mapping[k]] = v
77 | else:
78 | # in case form field was not found, keep it in special pd field
79 | cf_data[PIPEDRIVE_CUSTOM_FIELD_FALLBACK] += f'{k}: {v}\n'
80 |
81 | name = person.get('name', '')
82 | name = name or person.get('primary_email', '').split('@')[0]
83 | payload = {
84 | 'title': f'{job} - {name}',
85 | 'person_id': person['id'],
86 | **cf_data,
87 | }
88 | resp = client.deals.create_deal(payload)
89 |
90 | print(f"Deals {resp['data']['id']} was created")
91 |
92 |
93 | def send_data_to_pipedrive(job, form_data):
94 | client = Client(domain='https://api.pipedrive.com')
95 | client.set_api_token(os.environ.get('PIPEDRIVE_TOKEN'))
96 | form_person = {
97 | 'email': form_data.pop('email', ''),
98 | 'name': form_data.pop('fullName', ''),
99 | PERSON_FIELDS_MAP['skype']: form_data.pop('skype', ''),
100 | PERSON_FIELDS_MAP['whatsapp']: form_data.pop('whatsapp', ''),
101 | PERSON_FIELDS_MAP['telegram']: form_data.pop('telegram', ''),
102 | }
103 |
104 | api_person = pipedrive_get_or_create_person(client, form_person)
105 | pipedrive_create_deals(client, job, api_person, form_data)
106 |
107 |
108 | def lambda_handler(event, context):
109 | def error_response(message):
110 | sys.stdout.write(str(event))
111 | return {'status': 'error', 'message': message}
112 |
113 | try:
114 | event_body = json.loads(event['body'])
115 | assert isinstance(event_body, dict)
116 | except (KeyError, JSONDecodeError, AssertionError):
117 | return error_response('Wrong input: malformed body.')
118 |
119 | try:
120 | job = event_body.pop('job')
121 | except KeyError:
122 | return error_response('Wrong input: missing "job" param.')
123 |
124 | try:
125 | send_data_to_pipedrive(job, event_body)
126 | except Exception:
127 | return error_response('Failed to save data')
128 |
129 | return {'status': 'ok'}
130 |
--------------------------------------------------------------------------------
/lambda-package/requirements.txt:
--------------------------------------------------------------------------------
1 | slack-sdk==3.9.1
2 | pipedrive-python-lib==1.2.3
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "start": "gatsby develop",
5 | "build": "gatsby build",
6 | "serve": "gatsby serve",
7 | "clean": "gatsby clean",
8 | "eslint": "eslint --ext .js --ext .jsx src",
9 | "stylelint": "stylelint --custom-syntax postcss-scss 'src/**/*.scss'",
10 | "stylefix": "stylelint --fix --custom-syntax postcss-scss 'src/**/*.scss'"
11 | },
12 | "dependencies": {
13 | "@hookform/resolvers": "2.9.8",
14 | "@reach/router": "1.3.4",
15 | "classnames": "2.3.2",
16 | "gatsby": "4.24.0",
17 | "gatsby-plugin-breakpoints": "1.3.7",
18 | "gatsby-plugin-react-helmet": "5.24.0",
19 | "gatsby-plugin-sass": "5.24.0",
20 | "lodash": "4.17.21",
21 | "prop-types": "15.8.1",
22 | "react": "18.2.0",
23 | "react-dom": "18.2.0",
24 | "react-helmet": "6.1.0",
25 | "react-hook-form": "7.36.1",
26 | "react-textarea-autosize": "8.3.4",
27 | "sass": "1.55.0",
28 | "yup": "0.32.11"
29 | },
30 | "devDependencies": {
31 | "@babel/core": "7.19.3",
32 | "@babel/eslint-parser": "7.19.1",
33 | "@babel/plugin-proposal-class-properties": "7.18.6",
34 | "@babel/plugin-proposal-decorators": "7.19.3",
35 | "@babel/plugin-proposal-export-namespace-from": "7.18.9",
36 | "@babel/plugin-proposal-function-sent": "7.18.6",
37 | "@babel/plugin-proposal-json-strings": "7.18.6",
38 | "@babel/plugin-proposal-numeric-separator": "7.18.6",
39 | "@babel/plugin-proposal-throw-expressions": "7.18.6",
40 | "@babel/plugin-syntax-dynamic-import": "7.8.3",
41 | "@babel/plugin-syntax-import-meta": "7.10.4",
42 | "@babel/preset-env": "7.19.3",
43 | "@babel/preset-react": "7.18.6",
44 | "@svgr/webpack": "6.3.1",
45 | "eslint": "8.24.0",
46 | "eslint-config-airbnb": "19.0.4",
47 | "eslint-import-resolver-alias": "1.1.2",
48 | "eslint-loader": "4.0.2",
49 | "eslint-plugin-import": "2.26.0",
50 | "eslint-plugin-jsx-a11y": "6.6.1",
51 | "eslint-plugin-react": "7.31.8",
52 | "postcss": "8.4.16",
53 | "stylelint": "14.13.0",
54 | "stylelint-config-sass-guidelines": "9.0.1",
55 | "stylelint-order": "5.0.0",
56 | "stylelint-scss": "4.3.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | awscli==1.20.25
2 | click==8.0.1
3 | flake8==3.9.2
4 | isort==5.9.3
5 | sarge==0.1.6
6 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 | import time
4 | from pathlib import Path
5 |
6 | import click
7 |
8 | from deploy.aws import aws, resource_details
9 | from deploy.settings import PROJECT_NAME
10 | from deploy.utils import run, timing
11 |
12 |
13 | BASE_DIR = os.path.dirname(os.path.abspath(__file__))
14 |
15 |
16 | @click.group()
17 | def cli():
18 | pass
19 |
20 |
21 | @cli.command(help='Deploy app to production')
22 | @timing
23 | def deploy_app():
24 | # Upload website static content
25 | s3_bucket = resource_details(
26 | PROJECT_NAME,
27 | 'WebsiteBucket',
28 | )['PhysicalResourceId']
29 | dist_folder = os.path.join(BASE_DIR, 'public')
30 | aws(f's3 sync {dist_folder} s3://{s3_bucket} --delete', parse_output=False)
31 |
32 | # Invalidate CDN
33 | cloudfront_id = resource_details(
34 | PROJECT_NAME,
35 | 'WebsiteDistribution',
36 | )['PhysicalResourceId']
37 | aws(f'cloudfront create-invalidation --distribution-id {cloudfront_id} '
38 | f'--paths "/*"')
39 |
40 |
41 | @cli.command(help='Deploy lambda function to production')
42 | @timing
43 | def deploy_lambda():
44 | function_name = resource_details(
45 | PROJECT_NAME,
46 | 'JobApplicationLambda',
47 | )['PhysicalResourceId']
48 |
49 | package_path = Path('lambda-package')
50 | package_deps_path = package_path / 'dependencies'
51 | code_archive_name = 'lambda-package.zip'
52 |
53 | run(f'zip -r ../{code_archive_name} .', cwd=package_deps_path)
54 | run(f'zip -g {code_archive_name} lambda_function.py', cwd=package_path)
55 |
56 | code_archive_path = f'fileb://{package_path}/{code_archive_name}'
57 | aws(
58 | f'lambda update-function-code '
59 | f'--function-name {function_name} '
60 | f'--zip-file {code_archive_path} '
61 | f'--publish',
62 | )
63 |
64 | def get_function_config():
65 | return aws(
66 | f'lambda get-function --function-name {function_name}',
67 | )['Configuration']
68 |
69 | function_config = get_function_config()
70 | status = function_config['LastUpdateStatus']
71 | while status == 'InProgress':
72 | time.sleep(1)
73 | function_config = get_function_config()
74 | status = function_config['LastUpdateStatus']
75 |
76 | if status == 'Failed':
77 | status_reason = function_config['LastUpdateStatusReason']
78 | status_reason_code = function_config['LastUpdateStatusReasonCode']
79 | raise RuntimeError(
80 | f'Failed to update lambda function. '
81 | f'Reason: {status_reason_code} | {status_reason}.',
82 | )
83 |
84 | info = os.environ['GOOGLE_API_SERVICE_ACCOUNT_INFO']
85 | info = info.replace('"', '\\"') # encode quotes inside json string
86 | spreadsheet_id = os.environ['GOOGLE_SPREADSHEET_ID']
87 | slack_bot_token = os.environ['SLACK_BOT_TOKEN']
88 | pipedrive_token = os.environ['PIPEDRIVE_TOKEN']
89 | aws(
90 | f'lambda update-function-configuration '
91 | f'--function-name {function_name} '
92 | f'--environment "Variables={{'
93 | f'GOOGLE_API_SERVICE_ACCOUNT_INFO=\'{info}\','
94 | f'GOOGLE_SPREADSHEET_ID={spreadsheet_id},'
95 | f'PIPEDRIVE_TOKEN={pipedrive_token},'
96 | f'SLACK_BOT_TOKEN={slack_bot_token}'
97 | f'}}"',
98 | )
99 |
100 |
101 | if __name__ == '__main__':
102 | cli()
103 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 80
3 | exclude = .git,.venv,node_modules,lambda-package/dependencies,public
4 |
5 | [isort]
6 | line_length = 80
7 | lines_after_imports = 2
8 | multi_line_output = 2
9 | skip = .git,.venv,node_modules,lambda-package/dependencies,public
10 |
--------------------------------------------------------------------------------
/src/Jobs.js:
--------------------------------------------------------------------------------
1 | // Jobs. Steve Jobs.
2 | const Jobs = {
3 | python: {
4 | name: 'Python',
5 | title: 'Python/Django разработчик',
6 | subTitle: 'Full-stack',
7 | description: 'Full-stack разработчик (Python/Django + frontend)',
8 | url: '/python/',
9 | active: false,
10 | },
11 | wordpress: {
12 | name: 'WordPress',
13 | title: 'PHP/WordPress разработчик',
14 | subTitle: 'Full-stack',
15 | description: 'Full-stack разработчик (PHP/WordPress + frontend)',
16 | url: '/wordpress/',
17 | active: false,
18 | },
19 | php: {
20 | name: 'PHP',
21 | title: 'PHP разработчик',
22 | subTitle: 'Full-stack',
23 | description: 'Full-stack разработчик (PHP + frontend)',
24 | url: '/php/',
25 | active: false,
26 | },
27 | content: {
28 | name: 'Content',
29 | title: 'Контент-менеджер, продюсер',
30 | description: 'Content, Product Marketing, Digital, SEO, SMM',
31 | url: '/content/',
32 | active: false,
33 | },
34 | ux: {
35 | name: 'UX',
36 | title: 'UX/UI дизайнер',
37 | subTitle: 'Проект Teamplify',
38 | description: 'Дизайнер продукта в команду Teamplify',
39 | url: '/ux/',
40 | active: false,
41 | },
42 | frontend: {
43 | name: 'Frontend',
44 | title: 'Frontend разработчик',
45 | subTitle: 'Full-stack',
46 | description: 'Frontend / full-stack разработчик',
47 | url: '/frontend/',
48 | active: false,
49 | },
50 | };
51 |
52 | export default Jobs;
53 |
--------------------------------------------------------------------------------
/src/_global.scss:
--------------------------------------------------------------------------------
1 | @import './vars';
2 | @import './reset';
3 | @import './typography';
4 | @import './mixins';
5 |
6 | body {
7 | background-image: url('./images/body-background.jpg');
8 | background-position: center top;
9 | background-repeat: no-repeat;
10 | background-size: auto;
11 | @include media-up(sm) {
12 | line-height: 2.2;
13 | }
14 | }
15 |
16 | ::selection {
17 | background-color: rgba($font-color-orange, 0.3);
18 | }
19 |
20 | h1 {
21 | font-family: $font-family-title;
22 | font-size: 28px;
23 | font-weight: bold;
24 | line-height: 1.3;
25 | @include media-up(md) {
26 | font-size: 34px;
27 | line-height: 1.2;
28 | }
29 | @include media-up(lg) {
30 | font-size: 40px;
31 | }
32 | @include media-up(xl) {
33 | font-size: 46px;
34 | line-height: 1.1;
35 | }
36 | }
37 |
38 | h2 {
39 | font-family: $font-family-title;
40 | font-size: 20px;
41 | font-weight: bold;
42 | line-height: 1.2;
43 | @include media-up(lg) {
44 | font-size: 22px;
45 | }
46 | @include media-up(xl) {
47 | font-size: 24px;
48 | }
49 | }
50 |
51 | a {
52 | @include tr(color 0.6s, background-size 0.6s, border 0.6s);
53 | background-image: linear-gradient($font-color-orange, $font-color-orange);
54 | background-position: 0 100%;
55 | background-repeat: no-repeat;
56 | background-size: 0 1px;
57 | overflow-wrap: break-word;
58 |
59 | &:hover {
60 | background-size: 100% 1px;
61 | color: $font-color;
62 | }
63 | }
64 |
65 | // stylelint-disable-next-line selector-class-pattern
66 | .noUnderline {
67 | background-image: none;
68 | }
69 |
70 | p {
71 | margin-top: 20px;
72 |
73 | &:first-child {
74 | margin-top: 0;
75 | }
76 | }
77 |
78 | ul,
79 | ol {
80 | color: $font-color;
81 | counter-reset: item;
82 | list-style: none;
83 | margin: 20px 0 0;
84 | padding: 0;
85 | }
86 |
87 | ul li {
88 | counter-increment: item;
89 | margin: 15px 0 0;
90 | padding: 0 0 0 36px;
91 | position: relative;
92 |
93 | &:first-child {
94 | margin-top: 0;
95 | }
96 |
97 | &::before {
98 | background-image: url('./images/list-dot.svg');
99 | background-position: center;
100 | background-repeat: no-repeat;
101 | background-size: 16px;
102 | content: '';
103 | height: 16px;
104 | left: 5px;
105 | position: absolute;
106 | top: calc((1em * 1.8) / 2 - 8px);
107 | width: 16px;
108 | @include media-up(sm) {
109 | top: calc((1em * 2.2) / 2 - 8px);
110 | }
111 | }
112 | }
113 |
114 | ol li {
115 | counter-increment: item;
116 | margin: 15px 0 0;
117 | padding: 0 0 0 36px;
118 | position: relative;
119 |
120 | &:first-child {
121 | margin-top: 0;
122 | }
123 |
124 | &::before {
125 | color: $font-color-orange;
126 | content: counter(item) '.';
127 | left: 5px;
128 | position: absolute;
129 | top: 0;
130 | @include media-up(sm) {
131 | top: 3px;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/src/_mixins.scss:
--------------------------------------------------------------------------------
1 | @use 'sass:math';
2 |
3 | /* Media Queries */
4 |
5 | @mixin media-up($name, $mq-breakpoints: $grid-breakpoints) {
6 | @if map-has-key($mq-breakpoints, $name) {
7 | $name: map-get($mq-breakpoints, $name);
8 | }
9 | @media screen and (min-width: #{$name}) {
10 | @content;
11 | }
12 | }
13 |
14 | /* Transitions */
15 |
16 | @mixin tr($transitions...) {
17 | $unfolded-transitions: ();
18 | @each $transition in $transitions {
19 | $unfolded-transitions: append($unfolded-transitions, unfold-transition($transition), comma);
20 | }
21 | transition: $unfolded-transitions;
22 | }
23 |
24 | @function unfold-transition ($transition) {
25 | $property: all;
26 | $duration: 0.3s;
27 | $easing: cubic-bezier(0, 0.5, 0.5, 1);
28 | $delay: null;
29 | $default-properties: ($property, $duration, $easing, $delay);
30 |
31 | $unfolded-transition: ();
32 | @for $i from 1 through length($default-properties) {
33 | $p: null;
34 | @if $i <= length($transition) {
35 | $p: nth($transition, $i);
36 | } @else {
37 | $p: nth($default-properties, $i);
38 | }
39 | $unfolded-transition: append($unfolded-transition, $p);
40 | }
41 | @return $unfolded-transition;
42 | };
43 |
44 | /* Column Width */
45 |
46 | @function col-percent($amount) {
47 | @return (math.div(100%, $grid-columns) * $amount);
48 | };
49 |
--------------------------------------------------------------------------------
/src/_reset.scss:
--------------------------------------------------------------------------------
1 | *,
2 | *::before,
3 | *::after {
4 | box-sizing: inherit;
5 | outline: 0;
6 | }
7 |
8 | * {
9 | box-sizing: border-box;
10 | }
11 |
12 | html {
13 | box-sizing: border-box;
14 | font-family: sans-serif;
15 | line-height: 1.15;
16 | -ms-overflow-style: scrollbar;
17 | }
18 |
19 | article,
20 | aside,
21 | figcaption,
22 | figure,
23 | footer,
24 | header,
25 | hgroup,
26 | main,
27 | nav,
28 | section {
29 | display: block;
30 | }
31 |
32 | body {
33 | background-color: $body-background-color;
34 | color: $font-color;
35 | font-family: $font-family-base;
36 | font-size: $font-size-base;
37 | font-weight: $font-weight-base;
38 | line-height: $line-height-base;
39 | margin: 0;
40 | text-align: left;
41 | }
42 |
43 | [tabindex='-1']:focus {
44 | outline: 0 !important;
45 | }
46 |
47 | hr {
48 | box-sizing: content-box;
49 | height: 0;
50 | overflow: visible;
51 | }
52 |
53 | h1,
54 | h2,
55 | h3,
56 | h4,
57 | h5,
58 | h6 {
59 | margin: 0;
60 | }
61 |
62 | p {
63 | margin: 0;
64 | }
65 |
66 | ol,
67 | ul,
68 | li,
69 | dl,
70 | dt,
71 | dd {
72 | margin: 0;
73 | }
74 |
75 | b,
76 | strong {
77 | font-weight: bold;
78 | }
79 |
80 | a {
81 | background-color: transparent;
82 | color: $link-color;
83 | text-decoration: $link-decoration;
84 | }
85 |
86 | a:not([href]):not([tabindex]) {
87 | color: inherit;
88 | text-decoration: none;
89 |
90 | &:hover {
91 | color: inherit;
92 | text-decoration: none;
93 | }
94 |
95 | &:focus {
96 | color: inherit;
97 | outline: 0;
98 | text-decoration: none;
99 | }
100 | }
101 |
102 | img {
103 | border-style: none;
104 | vertical-align: middle;
105 | }
106 |
107 | svg {
108 | overflow: hidden;
109 | vertical-align: middle;
110 | }
111 |
112 | table {
113 | border-collapse: collapse;
114 | }
115 |
116 | th {
117 | text-align: inherit;
118 | }
119 |
120 | label {
121 | display: inline-block;
122 | margin: 0;
123 | }
124 |
125 | button {
126 | border-radius: 0;
127 | }
128 |
129 | input,
130 | button,
131 | select,
132 | optgroup,
133 | textarea {
134 | font-family: inherit;
135 | font-size: inherit;
136 | line-height: inherit;
137 | margin: 0;
138 | }
139 |
140 | button,
141 | input {
142 | overflow: visible;
143 | }
144 |
145 | button,
146 | select {
147 | text-transform: none;
148 | }
149 |
150 | select {
151 | word-wrap: normal;
152 | }
153 |
154 | button::-moz-focus-inner,
155 | [type='button']::-moz-focus-inner,
156 | [type='reset']::-moz-focus-inner,
157 | [type='submit']::-moz-focus-inner {
158 | border-style: none;
159 | padding: 0;
160 | }
161 |
162 | textarea {
163 | overflow: auto;
164 | resize: vertical;
165 | }
166 |
167 | fieldset {
168 | border: 0;
169 | margin: 0;
170 | min-width: 0;
171 | padding: 0;
172 | }
173 |
174 | legend {
175 | color: inherit;
176 | display: block;
177 | line-height: inherit;
178 | max-width: 100%;
179 | padding: 0;
180 | white-space: normal;
181 | width: 100%;
182 | }
183 |
184 | [type='number']::-webkit-inner-spin-button,
185 | [type='number']::-webkit-outer-spin-button {
186 | height: auto;
187 | }
188 |
189 | [type='search'] {
190 | appearance: none;
191 | outline-offset: -2px;
192 | }
193 |
194 | [type='search']::-webkit-search-decoration {
195 | appearance: none;
196 | }
197 |
--------------------------------------------------------------------------------
/src/_typography.scss:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'PT Mono';
3 | font-style: normal;
4 | font-weight: normal;
5 | src: url('./fonts/PT-Mono_Regular.woff2') format('woff2'), url('./fonts/PT-Mono_Regular.woff') format('woff');
6 | }
7 | @font-face {
8 | font-family: 'PT Mono';
9 | font-style: normal;
10 | font-weight: bold;
11 | src: url('./fonts/PT-Mono_Bold.woff2') format('woff2'), url('./fonts/PT-Mono_Bold.woff') format('woff');
12 | }
13 | @font-face {
14 | font-family: 'Source Sans Pro';
15 | font-style: normal;
16 | font-weight: bold;
17 | src: url('./fonts/Source-Sans_Bold.woff2') format('woff2'), url('./fonts/Source-Sans_Bold.woff') format('woff');
18 | }
19 |
--------------------------------------------------------------------------------
/src/_vars.scss:
--------------------------------------------------------------------------------
1 |
2 | $grid-columns: 12;
3 | $gutter-width: 30px;
4 | $container-max-width: 1140px;
5 | $grid-breakpoints: (xs: 0, sm: 576px, md: 768px, lg: 992px, xl: 1200px);
6 |
7 | $body-background-color: #fff;
8 |
9 | $font-family-base: 'PT Mono', monospace;
10 | $font-family-title: 'Source Sans Pro', sans-serif;
11 | $font-size-base: 15px;
12 | $font-weight-base: normal;
13 | $line-height-base: 1.8;
14 | $font-color: #1a1a1a;
15 | $font-color-gray: #666;
16 | $font-color-orange: #f36717;
17 |
18 | $link-color: #f36717;
19 | $link-hover-color: #c04600;
20 | $link-decoration: none;
21 |
22 | $border-color: #666;
23 |
24 | $form-border-color: #ccc;
25 | $form-focus-border-color: #666;
26 | $form-label-color: #1a1a1a;
27 |
28 | $error-color: #ff7a7a;
29 |
--------------------------------------------------------------------------------
/src/chat.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/prefer-default-export
2 | export function openChat() {
3 | if (window.$crisp) {
4 | window.$crisp.push(['do', 'chat:show']);
5 | window.$crisp.push(['do', 'chat:open']);
6 | }
7 | return false;
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/About.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'gatsby';
2 | import React from 'react';
3 |
4 | import ExternalLink, { ExternalLinks } from './ExternalLink';
5 |
6 | export default function About() {
7 | return (
8 |
9 | Привет! Мы —{' '}
10 | ivelum,
11 | занимаемся продуктовой разработкой с 2003 года и работаем
12 | над крупными софтверными проектами. У нас свободный график
13 | для всех сотрудников и отлично поставленные
14 | процессы с Минимумом Бюрократии (тм). Подробнее о нас
15 | и о том, как мы работаем можно
16 | почитать в нашей{' '}
17 | Wiki,{' '}
18 | а также рекомендуем заглянуть
19 | в раздел вопросы и ответы.
20 |
10 |
11 | Удалёнка
12 | ,{' '}
13 |
14 | свободный график
15 | , полная занятость. Работа с частичной занятостью
16 | не рассматривается;
17 |
18 |
19 | Корпоративный Macbook или денежная компенсация на покупку
20 | собственного оборудования. Подробнее —
21 | в разделе{' '}
22 |
23 | пакет компенсаций
24 | в нашей вики;
25 |
26 |
27 |
28 | Курсы английского
29 | с профессиональным преподавателем, носителем языка;
30 |
31 |
32 | Обратите внимание: для этой вакансии есть ограничения
33 | по странам с которыми мы можем работать. В частности, мы не сможем
34 | заключить с вами контракт, если вы находитесь в России, Беларуси,
35 | Туркменистане, Китае и ряде других стран. Это касается не
36 | гражданства, а только нахождения на территории этих стран. Во время
37 | заполнения анкеты выберите вашу страну из
38 | списка. Если для нее есть какие-то ограничения, вы сразу увидите
39 | сообщение об этом.
40 |
9 | После заполнения{' '}
10 | {
11 | (jobIsActive)
12 | ? формы отклика
13 | : 'формы отклика'
14 | } вы увидите приглашение для записи на короткий ознакомительный звонок
15 | в Zoom, он занимает буквально 15 минут. Вы сможете выбрать удобное для
16 | вас время и немедленно забронировать его. На этом звонке можно обсудить
17 | любые интересующие вас вопросы по проектам или по вакансии, а также
18 | решить, каким образом продолжить общение - или пойти на техническое
19 | интервью, или же попробовать сначала выполнить тестовое задание.
20 |
21 |
22 | Техническое интервью также проводится в режиме видеозвонка в Zoom и
23 | обычно занимает не более полутора часов. По вашему желанию его можно
24 | провести не только в рабочий день, но и в выходной. Как правило, мы
25 | обсуждаем следующие три основные темы:
26 |
27 |
28 |
29 | любые вопросы от вас — о компании,
30 | о вакансии, о проекте и др.;
31 |
32 |
33 | разговор о каком-то проекте из вашего опыта;
34 |
35 |
36 | экспресс-опросник по технологиям из нашего стека.
37 |
38 |
39 |
40 | Мы не задаем никаких абстрактных вопросов,
41 | не гоняем по алгоритмам и не предлагаем
42 | вам писать код во время собеседований.
43 | Все общение — только по делу
44 | и в комфортной обстановке.
45 |
46 | >
47 | );
48 | }
49 |
50 | InterviewProcess.propTypes = {
51 | jobIsActive: PropTypes.bool.isRequired,
52 | };
53 |
--------------------------------------------------------------------------------
/src/components/JobPage.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React from 'react';
3 |
4 | import About from './About';
5 | import Button from './Button';
6 | import HrLine from './HrLine';
7 |
8 | import * as styles from './JobPage.module.scss';
9 |
10 | export default function JobPage({ children, job }) {
11 | let applicationLink;
12 | if (job.active) {
13 | applicationLink = (
14 |
12 | Привет!
13 |
14 | Ниже перечислены актуальные вакансии в
15 | ivelum.
16 | Все вакансии подразумевают полную занятость, удаленную работу
17 | и свободный график. Дополнительная информация:{' '}
18 | вопросы и ответы,{' '}
19 | Wiki{' '}
20 | о работе в ivelum.
21 |
23 | Нам нужен человек, который помог бы нам создавать и распространять
24 | качественный контент по теме разработки софта и управления ей.
25 | Это могут быть статьи, видео, выступления с докладами на
26 | конференциях и любой другой креативный движ. Весь контент будет
27 | так или иначе связан с нашими{' '}
28 |
29 | услугами по заказной разработке
30 | и{' '}
31 |
32 | продуктом для управления командами разработчиков
33 | .
34 |
35 |
36 | Мы ожидаем от вас:
37 |
38 |
39 |
опыт работы в этой сфере от 5 лет;
40 |
41 | хорошее знание русского и английского, безупречная грамотность;
42 |
43 |
44 | вы можете самостоятельно (без ChatGPT) написать интересную статью
45 | по IT-тематике или помочь отредактировать статью, которую написал
46 | другой человек;
47 |
48 |
49 | вы знаете, как сделать так, чтобы ваше творчество увидело большое
50 | количество людей;
51 |
52 |
53 | если у вас также есть опыт работы с видео - вообще замечательно 🙂
54 |
26 | У нас в{' '}
27 |
28 | ivelum
29 | несколько команд, каждая из которых работает над
30 | своим проектом. При этом, технологический стек и применяемые
31 | подходы во всех командах похожи. Сейчас у нас открыта
32 | позиция для Frontend / full-stack разработчика в проект платформы
33 | для медиа-изданий в Интернет, которая интегрирует работу с
34 | контентом, рекламой, аудиторией и аналитикой. Помимо этого, у нас
35 | есть и другие проекты - в области информационной безопасности,
36 | медицины, онлайн-обучения, и управления персоналом — в будущем
37 | возможен переход в одну из этих команд.
38 |
39 |
40 | Посмотрите короткое видео (3 мин) с кратким обзорным
41 | рассказом о вакансии:
42 |
43 |
44 |
45 |
46 |
57 |
58 | В своих проектах мы используем React и GraphQL, на беке может быть
59 | Next.js, Python или PHP. Хостинг — в AWS, на Kubernetes или
60 | ECS. Все наши проекты глубоко автоматизированы, включая
61 | развертывание как продакшен-инфраструктуры, так и локального
62 | окружения разработки. Мы активно применяем автоматизированное
63 | тестирование, линтеры и code review. См. также:
64 |
65 |
66 |
67 | более полный {' '}
68 |
69 | список технологий
70 | в нашей вики;
71 |
72 |
73 |
74 | блог Teamplify
75 | со статьями о технологиях и менеджменте;
76 |
77 |
78 | наш{' '}
79 |
80 | канал на YouTube
81 | .
82 |
83 |
84 |
85 |
86 |
87 | Мы ожидаем, что у вас от четырех лет опыта в веб-разработке,
88 | вы отлично знаете React и готовы работать не только над фронтедом,
89 | но и иногда заглянуть в бекенд тоже не против. Основная работа
90 | предстоит на фронте, однако, если для вашей задачи потребуются
91 | какие-то небольшие доработки на беке, мы хотели бы, чтобы вы могли
92 | с ними справиться самостоятельно. Если в будущем вы захотите
93 | перейти на позицию, в которой будет еще больший процент бекенда,
94 | мы будем это только приветствовать.
95 |
96 |
97 | Также, мы всячески приветствуем креатив, самостоятельность и
98 | открытые обсуждения. Поэтому, кроме технических скиллов, мы ожидаем,
99 | что вы настроены на командную работу, готовы исследовать разные
100 | варианты решения задачи, обсуждать их с коллегами, проектировать,
101 | принимать решения и воплощать их в жизнь.
102 |
103 |
104 | У нас русскоязычная команда, но продукты, над которыми мы работаем,
105 | имеют англоязычный интерфейс и рассчитаны на интернациональную
106 | аудиторию. Также мы активно общаемся с нашими партнерами из США.
107 | В связи с этим, русский знать обязательно; английский —
108 | достаточно уровня Intermediate и готовности его улучшать (у нас
109 | налажено корпоративное обучение английскому).
110 |
12 | Спасибо за заполнение анкеты.
13 | Приглашаем вас на короткое ознакомительное интервью (15 мин), на
14 | котором вы сможете задать все интересующие вас вопросы о вакансии,
15 | а мы немного узнаем о вас.
16 |
17 |
18 | Пожалуйста, выберите одну удобную вам дату и время по ссылке:
19 |
20 | {ExternalLinks.company.bookInterview}
21 |
22 | Ссылка на Zoom-конференцию будет создана
23 | автоматически и придет вам на почту.
24 |
23 | Мы ищем опытного PHP / full-stack разработчика в команду,
24 | занимающуюся поддержкой и развитием сайта ценителей вина, с
25 | обзорами, тестами и активным пользовательским комьюнити. В редакции
26 | работают профессиональные винные критики с большим опытом, авторы
27 | книг и обладатели наград в этой области.
28 |
29 |
30 | С технической точки зрения это довольно старый проект,
31 | построенный на Drupal, который сейчас обретает новое дыхание. В нем
32 | хватает проблем, однако команда, недавно выкупившая проект,
33 | готова их решать и модернизировать технологии.
34 | Например, предстоит работа с платными подписками и
35 | их возможной миграцией на Stripe, в будущем - вероятна переделка на
36 | Headless CMS с переводом фронта на React / Next.js, и т.д. Планы
37 | большие, и мы ищем разработчика, который помог бы их реализовать.
38 |
39 |
40 | У нас есть и другие проекты на PHP, Python
41 | и Node.js, в будущем может предстоять работа и с ними.
42 |
43 |
44 |
45 |
46 | Мы ожидаем, что у вас не меньше пяти лет опыта в веб-разработке,
47 | вы готовы работать как над бекендом, так и над фронтендом,
48 | инфраструктурная часть вам также не чужда. Идеально, если помимо
49 | PHP, вы открыты к работе с Python и Node.js в будущем.
50 |
51 |
52 | Мы всячески приветствуем креатив, самостоятельность
53 | и открытые обсуждения. Поэтому кроме технических скиллов,
54 | ожидаем, что вы настроены на командную работу,
55 | готовы исследовать разные варианты решения задачи, обсуждать
56 | их с коллегами, проектировать, принимать решения
57 | и воплощать их в жизнь.
58 |
59 |
60 | У нас русскоязычная команда, но продукты, над которыми
61 | мы работаем, имеют англоязычный интерфейс и рассчитаны
62 | на интернациональную аудиторию. Также мы активно
63 | общаемся с нашими партнерами из США. В связи
64 | с этим русский знать обязательно;
65 | а для английского — достаточно уровня Intermediate
66 | и готовности его улучшать (у нас налажено корпоративное
67 | обучение английскому).
68 |
26 | У нас в{' '}
27 |
28 | ivelum
29 | несколько команд, каждая из которых работает над
30 | своим проектом. Технологический стек и применяемые
31 | подходы в них похожи. Сейчас у нас открыты позиции для
32 | Python / full-stack разработчиков в разные команды и проекты,
33 | например:
34 |
35 |
36 |
37 |
38 | Teamplify
39 |
40 | — помощник для команд разработки, помогающий
41 | улучшить прозрачность работы, упростить отчетность и
42 | экономить время на статус-митингах;
43 |
44 |
45 | AI в медицине — помощь в коммуникации с пациентом перед и
46 | после приема в клинике;
47 |
48 |
49 | Платформа для профессиональных медиа-изданий —
50 | управление контентом, работа с аудиторией, рекламой, аналитикой;
51 |
52 |
53 | Платформа для обеспечения безопасности программных
54 | продуктов — анализ уязвимостей и рисков, интеграция мер
55 | по безопасности в процесс разработки ПО;
56 |
57 |
58 | ... и другие. Пока вы читаете эту вакансию, мы ведем переговоры
59 | о старте еще двух проектов, которые могут начаться в самом
60 | ближайшем будущем.
61 |
62 |
63 |
64 | Посмотрите короткое видео (4 мин)
65 | с рассказом о вакансии:
66 |
67 |
68 |
69 |
70 |
73 |
74 | Основные технологии: Python и Django последних версий, React.js,
75 | GraphQL, Docker, AWS. Все наши проекты глубоко автоматизированы,
76 | включая развертывание как продакшен-инфраструктуры,
77 | так и локального окружения разработки. Мы активно
78 | применяем автоматизированное тестирование, линтеры и code
79 | review. См. также:
80 |
81 |
82 |
83 | более полный {' '}
84 |
85 | список технологий
86 | в нашей вики;
87 |
88 |
89 |
90 | блог Teamplify
91 | со статьями о технологиях и менеджменте;
92 |
93 |
94 | наш{' '}
95 |
96 | канал на YouTube
97 | .
98 |
99 |
100 |
101 |
102 |
103 | Мы ожидаем, что у вас несколько лет опыта в веб-разработке, вы
104 | готовы работать как над бекендом, так и над фронтендом,
105 | инфраструктурная часть вам также не чужда. Все технологии из
106 | нашего стека знать необязательно, но, как минимум, опыт
107 | работы с Python, JavaScript и Linux точно пригодится.
108 |
109 |
110 | Мы всячески приветствуем креатив, самостоятельность и открытые
111 | обсуждения. Поэтому кроме технических скиллов, ожидаем, что
112 | вы настроены на командную работу, готовы исследовать разные
113 | варианты решения задачи, обсуждать их с коллегами, проектировать,
114 | принимать решения и воплощать их в жизнь.
115 |
116 |
117 | У нас русскоязычная команда, но продукты, над которыми мы
118 | работаем, имеют англоязычный интерфейс и рассчитаны на
119 | интернациональную аудиторию. Также мы активно общаемся с
120 | нашими партнерами из США. В связи с этим русский знать обязательно,
121 | а для английского — достаточно уровня Intermediate
122 | и готовности его улучшать (у нас налажено корпоративное
123 | обучение английскому).
124 |
23 |
24 | Teamplify
25 | - инструмент для управления командами разработчиков,
26 | помогающий улучшить прозрачность работы, упростить отчетность и
27 | экономить время на статус-митингах. Teamplify родился как внутренний
28 | проект компании, изначально мы использовали его только для своих
29 | команд. Он хорошо работал для нас, и в какой-то момент мы решили
30 | превратить его в публичный продукт и выпустить на рынок.
31 |
32 |
33 |
34 |
35 | Мы ищем опытного продуктового дизайнера для Teamplify. Мы ожидаем,
36 | что вы отлично разбираетесь в UX, умеете анализировать потребности
37 | пользователей и предлагать удобные и красивые решения, и занимаетесь
38 | этим уже как минимум несколько лет. Понимание того, как происходит
39 | разработка ПО, будет большим плюсом, так как наш продукт
40 | предназначен для команд разработчиков.
41 |
42 |
43 | Также мы ценим хорошие коммуникативные навыки - конструктивные
44 | обсуждения, умение понятно и грамотно изложить свою мысль, и устно,
45 | и письменно, причем как на русском, так и на английском. У нас
46 | русскоязычная команда, но продукт имеет англоязычный интерфейс и
47 | ориентирован на интернациональную аудиторию. Общение с большинством
48 | клиентов происходит на английском. Поэтому русский и английский
49 | знать обязательно. Английский можем помочь подтянуть – у нас
50 | налажено корпоративное обучение ему; однако, ваш собственный
51 | уровень на момент начала работы должен быть не ниже среднего.
52 |
23 | Мы ищем опытного PHP / full-stack разработчика в команду,
24 | занимающуюся поддержкой и развитием сайтов крупного медиа-холдинга.
25 | Суммарная посещаемость сайтов - более 70 млн. уникальных посетителей
26 | в месяц; тематики - автомобили, обустройство дома, популярная наука,
27 | фотография, и т.д.
28 |
29 |
30 | Все эти сайты построены на WordPress, и нас часто спрашивают - не
31 | планируем ли мы переписать их на что-то другое. Нет, не планируем 🙂
32 | WordPress как платформа, разумеется, имеет свои известные
33 | ограничения, но, несмотря на это, де-факто является стандартом
34 | среди профессиональных медиа-издателей. Слияния и поглощения в этой
35 | индустрии происходят регулярно, и по статистике 2/3 из новых сайтов,
36 | которыми пополняется портфолио холдинга, оказываются построены на
37 | WordPress, и противостоять этому тренду просто нецелесообразно.
38 |
39 |
40 | Заметим, однако, что у нас не "дикий" WordPress, а очень
41 | даже культурный: с CI-пайплайнами, код-стайлом, линтерами и
42 | автоматизированными тестами. Мы проводим код-ревью и стараемся
43 | следовать прочим инженерным best practices.
44 |
45 |
46 | У нас есть и другие проекты на PHP, Python
47 | и Node.js, в будущем может предстоять работа и с ними.
48 |
49 |
50 |
51 |
52 | Мы ожидаем, что у вас не меньше пяти лет опыта в веб-разработке,
53 | включая практический опыт с WordPress, и вы готовы работать как над
54 | бекендом, так и над фронтендом, инфраструктурная часть вам также
55 | не чужда. Идеально, если помимо PHP, вы открыты к работе с Python
56 | и Node.js в будущем.
57 |
58 |
59 | Мы всячески приветствуем креатив, самостоятельность
60 | и открытые обсуждения. Поэтому кроме технических скиллов,
61 | ожидаем, что вы настроены на командную работу,
62 | готовы исследовать разные варианты решения задачи, обсуждать
63 | их с коллегами, проектировать, принимать решения
64 | и воплощать их в жизнь.
65 |
66 |
67 | У нас русскоязычная команда, но продукты, над которыми
68 | мы работаем, имеют англоязычный интерфейс и рассчитаны
69 | на интернациональную аудиторию. Также мы активно
70 | общаемся с нашими партнерами из США. В связи
71 | с этим русский знать обязательно;
72 | а для английского — достаточно уровня Intermediate
73 | и готовности его улучшать (у нас налажено корпоративное
74 | обучение английскому).
75 |