├── .gitignore
├── .prettierrc
├── src
├── static
│ ├── robots.txt
│ ├── social.png
│ ├── favicon.ico
│ ├── favicon.png
│ ├── logo138.png
│ ├── privacy-policy-en.pdf
│ ├── privacy-policy-ru.pdf
│ ├── _redirects
│ ├── favicon.svg
│ └── 404.html
├── components
│ ├── organisms
│ │ ├── Company
│ │ │ ├── fs-no.avif
│ │ │ ├── fs-no.jpg
│ │ │ ├── fs-no.webp
│ │ │ ├── flatstack.jpg
│ │ │ ├── fs-warsaw.jpg
│ │ │ ├── flatstack.avif
│ │ │ ├── flatstack.webp
│ │ │ ├── flatstack2x.avif
│ │ │ ├── fs-antalya.avif
│ │ │ ├── fs-antalya.jpg
│ │ │ ├── fs-antalya.webp
│ │ │ ├── fs-warsaw.avif
│ │ │ ├── fs-warsaw.webp
│ │ │ ├── index.jsx
│ │ │ └── Content.jsx
│ │ ├── Hero
│ │ │ ├── title-bg.jpg
│ │ │ ├── title-bg.webp
│ │ │ ├── index.jsx
│ │ │ └── Content.jsx
│ │ ├── Join
│ │ │ ├── hr-photo.avif
│ │ │ ├── hr-photo.jpg
│ │ │ ├── hr-photo.webp
│ │ │ ├── SuccessModal.jsx
│ │ │ ├── HrDecoratedPhoto.jsx
│ │ │ └── index.jsx
│ │ ├── Relocation
│ │ │ ├── image.avif
│ │ │ ├── image.jpg
│ │ │ ├── image.webp
│ │ │ ├── index.jsx
│ │ │ └── Content.jsx
│ │ ├── Warsaw
│ │ │ ├── office_700.jpg
│ │ │ ├── office_1400.avif
│ │ │ ├── office_1400.jpg
│ │ │ ├── office_1400.webp
│ │ │ ├── office_700.avif
│ │ │ ├── office_700.webp
│ │ │ ├── index.jsx
│ │ │ └── Content.jsx
│ │ ├── Feedbacks
│ │ │ ├── alina_128.avif
│ │ │ ├── alina_128.jpg
│ │ │ ├── alina_128.webp
│ │ │ ├── alina_64.avif
│ │ │ ├── alina_64.jpg
│ │ │ ├── alina_64.webp
│ │ │ ├── arkadey_64.jpg
│ │ │ ├── askar_128.avif
│ │ │ ├── askar_128.jpg
│ │ │ ├── askar_128.webp
│ │ │ ├── askar_64.avif
│ │ │ ├── askar_64.jpg
│ │ │ ├── askar_64.webp
│ │ │ ├── dmitry_128.jpg
│ │ │ ├── dmitry_64.avif
│ │ │ ├── dmitry_64.jpg
│ │ │ ├── dmitry_64.webp
│ │ │ ├── arkadey_128.avif
│ │ │ ├── arkadey_128.jpg
│ │ │ ├── arkadey_128.webp
│ │ │ ├── arkadey_64.avif
│ │ │ ├── arkadey_64.webp
│ │ │ ├── dmitry_128.avif
│ │ │ ├── dmitry_128.webp
│ │ │ ├── index.jsx
│ │ │ ├── FeedbackCard.jsx
│ │ │ └── Content.jsx
│ │ ├── Header
│ │ │ ├── index.jsx
│ │ │ ├── Content.jsx
│ │ │ └── LogoSvg.jsx
│ │ ├── Vacancy
│ │ │ ├── index.jsx
│ │ │ └── Content.jsx
│ │ └── Footer
│ │ │ ├── index.jsx
│ │ │ └── Content.jsx
│ ├── atoms
│ │ ├── Container.jsx
│ │ ├── List.jsx
│ │ ├── ButtonLink.jsx
│ │ ├── RequiredFormText.jsx
│ │ ├── Card.jsx
│ │ ├── Input.jsx
│ │ ├── Link.jsx
│ │ └── Button.jsx
│ ├── icons
│ │ ├── CloseIcon.jsx
│ │ ├── AlertIcon.jsx
│ │ ├── FacebookIcon.jsx
│ │ ├── LoadingIcon.jsx
│ │ ├── MediumIcon.jsx
│ │ ├── TwitterIcon.jsx
│ │ ├── LinkedInIcon.jsx
│ │ ├── EmailIcon.jsx
│ │ ├── GitHubIcon.jsx
│ │ ├── TelegramIcon.jsx
│ │ └── InstagramIcon.jsx
│ ├── molecules
│ │ ├── Alert.jsx
│ │ ├── Section.jsx
│ │ ├── RecommendButton
│ │ │ ├── HiddenFormForNetlify.jsx
│ │ │ ├── index.jsx
│ │ │ ├── RecommendModal.jsx
│ │ │ └── RecommendForm.jsx
│ │ ├── ShareButton
│ │ │ ├── index.jsx
│ │ │ └── ShareModal.jsx
│ │ └── Modal.jsx
│ ├── L10nContext.jsx
│ └── Page.jsx
├── config.js
├── theme.js
├── webpack-templates
│ └── index.js
├── index-en.jsx
├── Document.jsx
├── render.js
├── hooks
│ └── useOnClickHydrate.js
└── locales
│ └── en.js
├── .browserslistrc
├── .editorconfig
├── README.md
├── .eslintrc
├── stylelint.config.js
├── LICENSE
├── babel.config.js
├── .github
└── workflows
│ └── ci.yml
├── lighthouserc.json
├── package.json
├── webpack.config.js
└── PatchHtmlWebpackPluginPlugin.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .snowpack
2 | dist
3 | node_modules
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
5 |
--------------------------------------------------------------------------------
/src/static/robots.txt:
--------------------------------------------------------------------------------
1 | # www.robotstxt.org/
2 |
3 | User-agent: *
4 | Allow: /
5 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | > 1%
2 | last 4 Chrome version
3 | last 4 Firefox version
4 | not dead
5 |
--------------------------------------------------------------------------------
/src/static/social.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/static/social.png
--------------------------------------------------------------------------------
/src/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/static/favicon.ico
--------------------------------------------------------------------------------
/src/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/static/favicon.png
--------------------------------------------------------------------------------
/src/static/logo138.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/static/logo138.png
--------------------------------------------------------------------------------
/src/static/privacy-policy-en.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/static/privacy-policy-en.pdf
--------------------------------------------------------------------------------
/src/static/privacy-policy-ru.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/static/privacy-policy-ru.pdf
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-no.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-no.avif
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-no.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-no.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-no.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-no.webp
--------------------------------------------------------------------------------
/src/components/organisms/Hero/title-bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Hero/title-bg.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Hero/title-bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Hero/title-bg.webp
--------------------------------------------------------------------------------
/src/components/organisms/Join/hr-photo.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Join/hr-photo.avif
--------------------------------------------------------------------------------
/src/components/organisms/Join/hr-photo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Join/hr-photo.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Join/hr-photo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Join/hr-photo.webp
--------------------------------------------------------------------------------
/src/components/organisms/Company/flatstack.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/flatstack.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-warsaw.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-warsaw.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Relocation/image.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Relocation/image.avif
--------------------------------------------------------------------------------
/src/components/organisms/Relocation/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Relocation/image.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Relocation/image.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Relocation/image.webp
--------------------------------------------------------------------------------
/src/components/organisms/Warsaw/office_700.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Warsaw/office_700.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Company/flatstack.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/flatstack.avif
--------------------------------------------------------------------------------
/src/components/organisms/Company/flatstack.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/flatstack.webp
--------------------------------------------------------------------------------
/src/components/organisms/Company/flatstack2x.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/flatstack2x.avif
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-antalya.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-antalya.avif
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-antalya.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-antalya.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-antalya.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-antalya.webp
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-warsaw.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-warsaw.avif
--------------------------------------------------------------------------------
/src/components/organisms/Company/fs-warsaw.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Company/fs-warsaw.webp
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/alina_128.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/alina_128.avif
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/alina_128.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/alina_128.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/alina_128.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/alina_128.webp
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/alina_64.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/alina_64.avif
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/alina_64.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/alina_64.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/alina_64.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/alina_64.webp
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/arkadey_64.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/arkadey_64.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/askar_128.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/askar_128.avif
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/askar_128.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/askar_128.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/askar_128.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/askar_128.webp
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/askar_64.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/askar_64.avif
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/askar_64.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/askar_64.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/askar_64.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/askar_64.webp
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/dmitry_128.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/dmitry_128.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/dmitry_64.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/dmitry_64.avif
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/dmitry_64.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/dmitry_64.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/dmitry_64.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/dmitry_64.webp
--------------------------------------------------------------------------------
/src/components/organisms/Warsaw/office_1400.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Warsaw/office_1400.avif
--------------------------------------------------------------------------------
/src/components/organisms/Warsaw/office_1400.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Warsaw/office_1400.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Warsaw/office_1400.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Warsaw/office_1400.webp
--------------------------------------------------------------------------------
/src/components/organisms/Warsaw/office_700.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Warsaw/office_700.avif
--------------------------------------------------------------------------------
/src/components/organisms/Warsaw/office_700.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Warsaw/office_700.webp
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/arkadey_128.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/arkadey_128.avif
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/arkadey_128.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/arkadey_128.jpg
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/arkadey_128.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/arkadey_128.webp
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/arkadey_64.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/arkadey_64.avif
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/arkadey_64.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/arkadey_64.webp
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/dmitry_128.avif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/dmitry_128.avif
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/dmitry_128.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fs/flatstack-warsaw/main/src/components/organisms/Feedbacks/dmitry_128.webp
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/src/static/_redirects:
--------------------------------------------------------------------------------
1 | https://flatstack-warsaw.netlify.app/* https://warsaw.flatstack.com/:splat 301!
2 | /index.html / 301!
3 | /js/script.js https://plausible.io/js/plausible.js 200
4 | /api/event https://plausible.io/api/event 202
5 |
--------------------------------------------------------------------------------
/src/components/atoms/Container.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const Container = styled.div`
4 | width: 100%;
5 | max-width: 75em;
6 | margin: 0 auto;
7 | padding-right: 1em;
8 | padding-left: 1em;
9 | `;
10 |
11 | export default Container;
12 |
--------------------------------------------------------------------------------
/src/components/atoms/List.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const List = styled.ul`
4 | margin: 1em 0;
5 | padding: 0 0 0 2em;
6 |
7 | list-style: disc;
8 | `;
9 |
10 | List.Item = styled.li`
11 | margin: 0 0 0.5em 0;
12 | `;
13 |
14 | export default List;
15 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | sectionIds: {
3 | join: 'join-section',
4 | vacancy: 'vacancy-section',
5 | warsaw: 'warsaw-section',
6 | relocation: 'relocation-section',
7 | feedbacks: 'feedbacks-section',
8 | company: 'company-section',
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/src/components/atoms/ButtonLink.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Button from './Button';
3 |
4 | const ButtonLink = styled(Button).attrs(() => ({
5 | as: 'a',
6 | }))``;
7 |
8 | export { variants, paddingVariants } from './Button';
9 |
10 | export default ButtonLink;
11 |
--------------------------------------------------------------------------------
/src/theme.js:
--------------------------------------------------------------------------------
1 | export default {
2 | colors: {
3 | accent: '#e00519',
4 | accentPale: '#faecec',
5 | primary: '#f3f3f3',
6 | secondary: '#c4c4c4',
7 | text: '#000000',
8 | invertedText: '#ffffff',
9 | paleText: '#4e4e4e',
10 | outline: '#086cea',
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/components/organisms/Header/index.jsx:
--------------------------------------------------------------------------------
1 | const Content =
2 | typeof window === 'undefined' ? require('./Content').default : () => null;
3 |
4 | const Header = () => {
5 | return (
6 |
9 | );
10 | };
11 |
12 | export default Header;
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Website for Flatstack Warsaw
2 |
3 | 
4 | [](https://app.netlify.com/sites/flatstack-warsaw/deploys)
5 |
6 | This repo contains source code for Flatstack Warsaw website
7 |
--------------------------------------------------------------------------------
/src/webpack-templates/index.js:
--------------------------------------------------------------------------------
1 | import L10nContext from '../components/L10nContext';
2 | import Page from '../components/Page';
3 | import enLocale from '../locales/en';
4 | import render from '../render';
5 |
6 | export default render(() => (
7 |
8 |
9 |
10 | ));
11 |
--------------------------------------------------------------------------------
/src/components/organisms/Warsaw/index.jsx:
--------------------------------------------------------------------------------
1 | import Section from '../../molecules/Section';
2 | import config from '../../../config';
3 |
4 | const Content =
5 | typeof window === 'undefined' ? require('./Content').default : () => null;
6 |
7 | const Warsaw = () => {
8 | return (
9 |
15 | );
16 | };
17 |
18 | export default Warsaw;
19 |
--------------------------------------------------------------------------------
/src/components/organisms/Company/index.jsx:
--------------------------------------------------------------------------------
1 | import config from '../../../config';
2 | import Section from '../../molecules/Section';
3 |
4 | const Content =
5 | typeof window === 'undefined' ? require('./Content').default : () => null;
6 |
7 | const Company = () => {
8 | return (
9 |
15 | );
16 | };
17 |
18 | export default Company;
19 |
--------------------------------------------------------------------------------
/src/components/organisms/Vacancy/index.jsx:
--------------------------------------------------------------------------------
1 | import config from '../../../config';
2 | import Section from '../../molecules/Section';
3 |
4 | const Content =
5 | typeof window === 'undefined' ? require('./Content').default : () => null;
6 |
7 | const Vacancy = () => {
8 | return (
9 |
15 | );
16 | };
17 |
18 | export default Vacancy;
19 |
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/index.jsx:
--------------------------------------------------------------------------------
1 | import config from '../../../config';
2 | import Section from '../../molecules/Section';
3 |
4 | const Content =
5 | typeof window === 'undefined' ? require('./Content').default : () => null;
6 |
7 | const Feedbacks = () => {
8 | return (
9 |
15 | );
16 | };
17 |
18 | export default Feedbacks;
19 |
--------------------------------------------------------------------------------
/src/components/organisms/Relocation/index.jsx:
--------------------------------------------------------------------------------
1 | import config from '../../../config';
2 | import Section from '../../molecules/Section';
3 |
4 | const Content =
5 | typeof window === 'undefined' ? require('./Content').default : () => null;
6 |
7 | const Relocation = () => {
8 | return (
9 |
15 | );
16 | };
17 |
18 | export default Relocation;
19 |
--------------------------------------------------------------------------------
/src/components/atoms/RequiredFormText.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useL10n } from '../L10nContext';
3 |
4 | const RequiredText = styled.p`
5 | color: ${({ theme }) => theme.colors.paleText};
6 | `;
7 |
8 | const AsteriskWrapper = styled.span`
9 | color: red;
10 | `;
11 |
12 | const RequiredFormText = () => {
13 | const { t } = useL10n();
14 | return (
15 | {t('form.requiredText', { AsteriskWrapper })}
16 | );
17 | };
18 |
19 | export default RequiredFormText;
20 |
--------------------------------------------------------------------------------
/src/components/icons/CloseIcon.jsx:
--------------------------------------------------------------------------------
1 | const CloseIcon = (props) => {
2 | return (
3 |
15 | );
16 | };
17 |
18 | export default CloseIcon;
19 |
--------------------------------------------------------------------------------
/src/components/organisms/Footer/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Container from '../../atoms/Container';
3 |
4 | const Content =
5 | typeof window === 'undefined' ? require('./Content').default : () => null;
6 |
7 | const Wrapper = styled.footer`
8 | padding: 3em 0;
9 |
10 | color: ${({ theme }) => theme.colors.paleText};
11 | `;
12 |
13 | const Footer = () => (
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
21 | export default Footer;
22 |
--------------------------------------------------------------------------------
/src/index-en.jsx:
--------------------------------------------------------------------------------
1 | import 'regenerator-runtime/runtime';
2 |
3 | import ReactDOM from 'react-dom';
4 | import { ThemeProvider } from 'styled-components';
5 | import L10nContext from './components/L10nContext';
6 | import Page from './components/Page';
7 | import enLocale from './locales/en';
8 | import theme from './theme';
9 |
10 | const target = document.getElementById('root');
11 |
12 | const App = () => (
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 | ReactDOM.hydrate(, target);
21 |
--------------------------------------------------------------------------------
/src/components/icons/AlertIcon.jsx:
--------------------------------------------------------------------------------
1 | const AlertIcon = (props) => {
2 | return (
3 |
17 | );
18 | };
19 |
20 | export default AlertIcon;
21 |
--------------------------------------------------------------------------------
/src/components/molecules/Alert.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import AlertIcon from '../icons/AlertIcon';
3 |
4 | const Wrapper = styled.div`
5 | display: flex;
6 | align-items: center;
7 | padding: 0.5em;
8 |
9 | background-color: ${({ theme }) => theme.colors.accentPale};
10 | border-radius: 0.7em;
11 |
12 | & > * {
13 | margin: 0.5em;
14 | }
15 | `;
16 |
17 | const StyledAlertIcon = styled(AlertIcon)`
18 | display: block;
19 | flex: none;
20 | `;
21 |
22 | const Alert = ({ children }) => {
23 | return (
24 |
25 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | export default Alert;
32 |
--------------------------------------------------------------------------------
/src/components/molecules/Section.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import Container from '../atoms/Container';
4 |
5 | const Wrapper = styled.section`
6 | margin-bottom: 4em;
7 | padding: ${({ grey }) => (grey ? '2em' : '0')} 0;
8 |
9 | background-color: ${({ theme, grey }) => grey && theme.colors.primary};
10 |
11 | &:last-of-type {
12 | margin-bottom: 0;
13 | }
14 | `;
15 |
16 | const Section = React.forwardRef(({ children, ...props }, ref) => (
17 |
18 | {children}
19 |
20 | ));
21 |
22 | Section.Title = styled.h2`
23 | margin: 0 0 1em 0;
24 |
25 | font-size: 2em;
26 | `;
27 |
28 | export default Section;
29 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@babel/eslint-parser",
3 | "extends": ["airbnb", "airbnb/hooks", "plugin:prettier/recommended"],
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "jest": true
8 | },
9 | "rules": {
10 | "react/no-unknown-property": "off", // for compatibility with Preact
11 | "react/react-in-jsx-scope": "off",
12 | "react/jsx-props-no-spreading": "off",
13 | "react/prop-types": "off",
14 | "jsx-a11y/html-has-lang": "off",
15 | "react/jsx-filename-extension": "off",
16 | "no-plusplus": "off",
17 | "import/no-unresolved": [
18 | "error",
19 | { "commonjs": true, "caseSensitive": true, "ignore": ["^react$", "^react-dom$", "^react-dom\/.*$", "\\?"] }
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/components/icons/FacebookIcon.jsx:
--------------------------------------------------------------------------------
1 | const FacebookIcon = ({ currentColor, ...restProps }) => {
2 | return (
3 |
21 | );
22 | };
23 |
24 | export default FacebookIcon;
25 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | processors: ['stylelint-processor-styled-components'],
3 | extends: [
4 | 'stylelint-config-recommended',
5 | 'stylelint-config-rational-order',
6 | 'stylelint-config-styled-components',
7 | ],
8 | rules: {
9 | 'order/properties-order': [],
10 | 'plugin/rational-order': [
11 | true,
12 | {
13 | 'empty-line-between-groups': true,
14 | },
15 | ],
16 | // We decided to deny media queries.
17 | // Prefer flex layout with flex-wrap.
18 | // If designer give you mockup with potential media queries,
19 | // come and discuss it. Try to convince to change design
20 | // and get rid of media queries.
21 | 'media-feature-name-disallowed-list': ['max-width', 'min-width'],
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/src/components/icons/LoadingIcon.jsx:
--------------------------------------------------------------------------------
1 | const LoadingIcon = (props) => {
2 | return (
3 |
29 | );
30 | };
31 |
32 | export default LoadingIcon;
33 |
--------------------------------------------------------------------------------
/src/components/atoms/Card.jsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | export const variants = {
4 | BORDERED: 'BORDERED',
5 | FILL_PRIMARY: 'FILL_PRIMARY',
6 | FILL_ACCENT_PALE: 'FILL_ACCENT_PALE',
7 | };
8 |
9 | const cssByVariant = {
10 | [variants.BORDERED]: css`
11 | border: 1px solid ${({ theme }) => theme.colors.secondary};
12 | `,
13 | [variants.FILL_PRIMARY]: css`
14 | background-color: ${({ theme }) => theme.colors.primary};
15 | `,
16 | [variants.FILL_ACCENT_PALE]: css`
17 | background-color: ${({ theme }) => theme.colors.accentPale};
18 | `,
19 | };
20 |
21 | const Card = styled.div`
22 | padding: 2em;
23 |
24 | border-radius: 1em;
25 |
26 | ${({ variant }) => cssByVariant[variant]}
27 | `;
28 |
29 | Card.defaultProps = {
30 | variant: variants.BORDERED,
31 | };
32 |
33 | export default Card;
34 |
--------------------------------------------------------------------------------
/src/Document.jsx:
--------------------------------------------------------------------------------
1 | const DETECT_WEBP_SUPPORT_SCRIPT =
2 | 'var i=new Image;i.onload=i.onerror=function(){document.body.classList.add(i.height==1?"webp":"no-webp")};i.src="data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==";\n';
3 |
4 | const Document = ({ children, styles, helmet }) => (
5 |
6 |
7 | {helmet.title.toComponent()}
8 | {helmet.meta.toComponent()}
9 | {helmet.link.toComponent()}
10 | {styles}
11 | {helmet.script.toComponent()}
12 |
13 |
14 |
19 |
20 |
21 |
22 | );
23 |
24 | export default Document;
25 |
--------------------------------------------------------------------------------
/src/render.js:
--------------------------------------------------------------------------------
1 | import { renderToString, renderToStaticMarkup } from 'react-dom/server';
2 | import { ServerStyleSheet, ThemeProvider } from 'styled-components';
3 | import { HelmetProvider } from 'react-helmet-async';
4 | import Document from './Document';
5 | import theme from './theme';
6 |
7 | export default function render(Page) {
8 | const sheet = new ServerStyleSheet();
9 |
10 | const helmetContext = {};
11 |
12 | const content = renderToString(
13 |
14 |
15 | {sheet.collectStyles()}
16 |
17 | ,
18 | );
19 |
20 | const html = renderToStaticMarkup(
21 |
22 | {content}
23 | ,
24 | );
25 | sheet.seal();
26 |
27 | return `${html}`;
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/molecules/RecommendButton/HiddenFormForNetlify.jsx:
--------------------------------------------------------------------------------
1 | const HiddenFormForNetlify = () => {
2 | return (
3 |
23 | );
24 | };
25 |
26 | export default HiddenFormForNetlify;
27 |
--------------------------------------------------------------------------------
/src/components/icons/MediumIcon.jsx:
--------------------------------------------------------------------------------
1 | const MediumIcon = ({ currentColor, ...restProps }) => {
2 | return (
3 |
16 | );
17 | };
18 |
19 | export default MediumIcon;
20 |
--------------------------------------------------------------------------------
/src/components/icons/TwitterIcon.jsx:
--------------------------------------------------------------------------------
1 | const TwitterIcon = ({ currentColor, ...restProps }) => {
2 | return (
3 |
15 | );
16 | };
17 |
18 | export default TwitterIcon;
19 |
--------------------------------------------------------------------------------
/src/components/icons/LinkedInIcon.jsx:
--------------------------------------------------------------------------------
1 | const LinkedInIcon = ({ currentColor, ...restProps }) => {
2 | return (
3 |
16 | );
17 | };
18 |
19 | export default LinkedInIcon;
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Flatstack
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 |
--------------------------------------------------------------------------------
/src/static/favicon.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/components/icons/EmailIcon.jsx:
--------------------------------------------------------------------------------
1 | const EmailIcon = (props) => {
2 | return (
3 |
20 | );
21 | };
22 |
23 | export default EmailIcon;
24 |
--------------------------------------------------------------------------------
/src/hooks/useOnClickHydrate.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { useCallback } from 'preact/compat';
3 |
4 | const useOnClickHydrate = ({ repeatClickAfterHydrate = true } = {}) => {
5 | const [firstClick, setFirstClick] = useState(null);
6 |
7 | const handleClick = useCallback(
8 | (event) => {
9 | if (firstClick) {
10 | return;
11 | }
12 |
13 | const coords = {
14 | x: event.clientX,
15 | y: event.clientY,
16 | };
17 |
18 | if (coords.x === 0 && coords.y === 0) {
19 | const rect = event.target.getBoundingClientRect();
20 | coords.x = rect.left + rect.width / 2;
21 | coords.y = rect.top + rect.height / 2;
22 | }
23 |
24 | setFirstClick(coords);
25 | },
26 | [firstClick],
27 | );
28 |
29 | useEffect(() => {
30 | if (!firstClick || !repeatClickAfterHydrate) {
31 | return;
32 | }
33 |
34 | document
35 | .elementFromPoint(firstClick.x, firstClick.y)
36 | .dispatchEvent(new Event('click'));
37 | }, [repeatClickAfterHydrate, firstClick]);
38 |
39 | return { shouldBeHydrated: !!firstClick, handleClick };
40 | };
41 |
42 | export default useOnClickHydrate;
43 |
--------------------------------------------------------------------------------
/src/components/atoms/Input.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const InputWrapper = styled.div`
4 | display: flex;
5 | flex-direction: column;
6 | `;
7 | const Label = styled.label`
8 | margin-bottom: 0.5em;
9 | `;
10 |
11 | const HtmlInput = styled.input`
12 | width: 100%;
13 | padding: 0.6em 0.7em;
14 |
15 | font-size: 1rem;
16 |
17 | border: 1px solid ${({ theme }) => theme.colors.secondary};
18 | border-radius: 0.7em;
19 | `;
20 |
21 | const RequiredMark = styled.span`
22 | color: red;
23 | `;
24 |
25 | const Input = ({
26 | textarea,
27 | name,
28 | label,
29 | className,
30 | inputClassName,
31 | id,
32 | required,
33 | ...rest
34 | }) => {
35 | return (
36 |
37 | {label ? (
38 |
41 | ) : null}
42 |
50 |
51 | );
52 | };
53 |
54 | export default Input;
55 |
--------------------------------------------------------------------------------
/src/components/organisms/Join/SuccessModal.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Button, { variants as buttonVariants } from '../../atoms/Button';
3 | import { useL10n } from '../../L10nContext';
4 | import Modal from '../../molecules/Modal';
5 | import HrDecoratedPhoto from './HrDecoratedPhoto';
6 |
7 | const Wrapper = styled.div`
8 | display: flex;
9 | flex-direction: column;
10 | align-items: stretch;
11 | `;
12 |
13 | const StyledHrPhoto = styled(HrDecoratedPhoto)`
14 | align-self: center;
15 | `;
16 |
17 | const Description = styled.p`
18 | margin: 2em 0;
19 | `;
20 |
21 | const SuccessModal = ({ isOpen, onClose }) => {
22 | const { t } = useL10n();
23 |
24 | return (
25 |
31 |
32 |
33 | {t('join.form.successDescription')}
34 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default SuccessModal;
43 |
--------------------------------------------------------------------------------
/src/components/icons/GitHubIcon.jsx:
--------------------------------------------------------------------------------
1 | const GitHubIcon = ({ currentColor, ...restProps }) => {
2 | return (
3 |
17 | );
18 | };
19 |
20 | export default GitHubIcon;
21 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | const isStatic = api.caller((caller) => caller.target === 'node');
3 |
4 | return {
5 | presets: [
6 | [
7 | '@babel/preset-react',
8 | {
9 | runtime: 'automatic',
10 | importSource: 'preact/compat',
11 | },
12 | ],
13 | [
14 | '@babel/preset-env',
15 | {
16 | ...(isStatic
17 | ? {
18 | targets: {
19 | node: 'current',
20 | },
21 | }
22 | : {
23 | useBuiltIns: 'usage',
24 | corejs: { version: 3 },
25 | }),
26 | },
27 | ],
28 | ],
29 | plugins: [
30 | ...(isStatic
31 | ? [
32 | [
33 | 'transform-define',
34 | {
35 | 'typeof window': 'undefined',
36 | },
37 | ],
38 | ]
39 | : [
40 | [
41 | 'transform-define',
42 | {
43 | 'typeof window': 'object',
44 | },
45 | ],
46 | ]),
47 | 'polished',
48 | ['babel-plugin-styled-components', { ssr: true }],
49 | ],
50 | };
51 | };
52 |
--------------------------------------------------------------------------------
/src/components/icons/TelegramIcon.jsx:
--------------------------------------------------------------------------------
1 | const TelegramIcon = ({ currentColor, ...restProps }) => {
2 | return (
3 |
21 | );
22 | };
23 |
24 | export default TelegramIcon;
25 |
--------------------------------------------------------------------------------
/src/static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Page Not Found
7 |
8 |
54 |
55 |
56 |
57 | Page Not Found
58 | Sorry, but the page you were trying to view does not exist.
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 |
8 | jobs:
9 | lint:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - uses: actions/setup-node@v1
15 | with:
16 | node-version: '15.x'
17 |
18 | - run: yarn
19 |
20 | - run: yarn lint
21 |
22 | test:
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/checkout@v2
26 |
27 | - uses: actions/setup-node@v1
28 | with:
29 | node-version: '15.x'
30 |
31 | - run: yarn
32 |
33 | - run: yarn test
34 |
35 | lighthouse:
36 | runs-on: ubuntu-latest
37 | steps:
38 | - uses: actions/checkout@v2
39 | - name: Wait for Netlify Deploy
40 | uses: probablyup/wait-for-netlify-action@3.2.0
41 | id: netlify
42 | with:
43 | site_id: '8a46652f-46d7-462d-92b1-80918e914419' # See Settings > Site Details > General in the Netlify UI
44 | env:
45 | NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}
46 | - name: Audit URLs using Lighthouse
47 | uses: treosh/lighthouse-ci-action@v8
48 | with:
49 | urls: |
50 | ${{ steps.netlify.outputs.url }}
51 | configPath: './lighthouserc.json'
52 | uploadArtifacts: true
53 |
--------------------------------------------------------------------------------
/src/components/organisms/Join/HrDecoratedPhoto.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useL10n } from '../../L10nContext';
3 | import HrPhotoWebpPath from './hr-photo.webp';
4 | import HrPhotoAvifPath from './hr-photo.avif';
5 | import HrPhotoJpgPath from './hr-photo.jpg';
6 |
7 | const Picture = styled.picture`
8 | position: relative;
9 |
10 | display: block;
11 |
12 | &::before {
13 | position: absolute;
14 | top: -0.5em;
15 | left: -1em;
16 |
17 | display: block;
18 | width: 3em;
19 | height: 3em;
20 |
21 | background-color: ${({ theme }) => theme.colors.accent};
22 | border-radius: 50%;
23 |
24 | content: '';
25 | }
26 |
27 | &::after {
28 | position: absolute;
29 | right: 0;
30 | bottom: -1em;
31 |
32 | display: block;
33 | width: 1.5em;
34 | height: 1.5em;
35 |
36 | background-color: ${({ theme }) => theme.colors.accent};
37 | border-radius: 50%;
38 |
39 | content: '';
40 | }
41 | `;
42 |
43 | const Image = styled.img`
44 | display: block;
45 |
46 | border-radius: 50%;
47 | `;
48 |
49 | const HrDecoratedPhoto = (props) => {
50 | const { t } = useL10n();
51 |
52 | return (
53 |
54 |
55 |
56 |
63 |
64 | );
65 | };
66 |
67 | export default HrDecoratedPhoto;
68 |
--------------------------------------------------------------------------------
/src/components/L10nContext.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useContext } from 'react';
2 | import enLocale from '../locales/en';
3 |
4 | const Context = React.createContext(null);
5 |
6 | function noTranslationFn(key) {
7 | return () => {
8 | throw new Error(`translation for "${key}" not found`);
9 | };
10 | }
11 |
12 | function getByPath(path, obj) {
13 | const pathSegments = path.split('.');
14 |
15 | let result = obj;
16 |
17 | for (let i = 0; i < pathSegments.length; i++) {
18 | const curr = result[pathSegments[i]];
19 |
20 | if (!curr) {
21 | return null;
22 | }
23 |
24 | result = curr;
25 | }
26 |
27 | return result;
28 | }
29 |
30 | export const Provider = ({ locale, values, children }) => {
31 | const t = useCallback(
32 | (key, options) => {
33 | const translation =
34 | getByPath(key, values.translations) ||
35 | getByPath(key, enLocale.translations) ||
36 | noTranslationFn(key);
37 |
38 | if (typeof translation === 'function') {
39 | return translation(options);
40 | }
41 |
42 | return translation;
43 | },
44 | [values],
45 | );
46 |
47 | return (
48 |
49 | {children}
50 |
51 | );
52 | };
53 |
54 | export const useL10n = () => {
55 | const contextValue = useContext(Context);
56 |
57 | if (contextValue === null) {
58 | throw new Error('useL10n cannot be used without L10nContext.Provider');
59 | }
60 |
61 | return contextValue;
62 | };
63 |
64 | export default {
65 | Provider,
66 | useL10n,
67 | };
68 |
--------------------------------------------------------------------------------
/src/components/atoms/Link.jsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 |
3 | export const underlineTypes = {
4 | SHOW_ON_HOVER: 'SHOW_ON_HOVER',
5 | HIDE_ON_HOVER: 'HIDE_ON_HOVER',
6 | NEVER: 'NEVER',
7 | ALWAYS: 'ALWAYS',
8 | };
9 |
10 | const hoverEffect = css`
11 | &::before {
12 | position: absolute;
13 | top: -0.2em;
14 | right: -0.4em;
15 | bottom: -0.2em;
16 | left: -0.4em;
17 |
18 | display: inline-block;
19 |
20 | background-color: ${({ theme }) => theme.colors.accent};
21 |
22 | transform: scaleX(0);
23 | transform-origin: top left;
24 | opacity: 0.3;
25 |
26 | transition: transform 0.3s ease-out;
27 |
28 | content: '';
29 | }
30 |
31 | &:hover::before,
32 | &:focus::before {
33 | transform: scaleX(1);
34 | }
35 | `;
36 |
37 | const Link = styled.a`
38 | position: relative;
39 |
40 | display: inline-block;
41 |
42 | color: ${({ theme }) => theme.colors.text};
43 | text-decoration: ${({ underlineType }) =>
44 | underlineType === underlineTypes.HIDE_ON_HOVER ||
45 | underlineType === underlineTypes.ALWAYS
46 | ? 'underline'
47 | : 'none'};
48 |
49 | cursor: pointer;
50 |
51 | &:hover,
52 | &:focus {
53 | text-decoration: ${({ underlineType }) =>
54 | underlineType === underlineTypes.SHOW_ON_HOVER ||
55 | underlineType === underlineTypes.ALWAYS
56 | ? 'underline'
57 | : 'none'};
58 | }
59 |
60 | ${({ noHoverEffect }) => !noHoverEffect && hoverEffect};
61 | `;
62 |
63 | Link.defaultProps = {
64 | underlineType: underlineTypes.ALWAYS,
65 | };
66 |
67 | export default Link;
68 |
--------------------------------------------------------------------------------
/src/components/icons/InstagramIcon.jsx:
--------------------------------------------------------------------------------
1 | const InstagramIcon = ({ currentColor, ...restProps }) => {
2 | return (
3 |
38 | );
39 | };
40 |
41 | export default InstagramIcon;
42 |
--------------------------------------------------------------------------------
/src/components/molecules/RecommendButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState, lazy, Suspense } from 'react';
2 | import { useErrorBoundary } from 'preact/hooks';
3 | import styled from 'styled-components';
4 | import { useL10n } from '../../L10nContext';
5 | import Button from '../../atoms/Button';
6 | import LoadingIcon from '../../icons/LoadingIcon';
7 |
8 | const RecommendModal = lazy(() => import('./RecommendModal'));
9 |
10 | const StyledLoadingIcon = styled(LoadingIcon)`
11 | margin: -0.5em -0.5em -0.5em 0.5em;
12 |
13 | color: ${({ theme }) => theme.colors.accent};
14 | font-size: 1.5em;
15 | `;
16 |
17 | const RecommendButton = (props) => {
18 | const { t } = useL10n();
19 | const [isModalOpen, setIsModalOpen] = useState(false);
20 | useErrorBoundary(() => {
21 | window.location.reload();
22 | });
23 |
24 | const handleClick = async () => {
25 | setIsModalOpen(true);
26 | };
27 |
28 | return (
29 | <>
30 | {isModalOpen ? (
31 |
34 | {t('recommendModal.triggerButtonText')}
35 |
36 | }
37 | >
38 |
41 | setIsModalOpen(false)} />
42 |
43 | ) : (
44 |
47 | )}
48 | >
49 | );
50 | };
51 |
52 | export default RecommendButton;
53 |
--------------------------------------------------------------------------------
/src/components/molecules/RecommendButton/RecommendModal.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useRef } from 'react';
2 | import { useL10n } from '../../L10nContext';
3 | import Modal from '../Modal';
4 | import RecommendForm, { statuses } from './RecommendForm';
5 |
6 | const RecommendModal = ({ isOpen, onClose }) => {
7 | const { t } = useL10n();
8 | const formRef = useRef();
9 | const [formStatus, setFormStatus] = useState(statuses.IDLE);
10 |
11 | const handleSubmit = async (event) => {
12 | if (
13 | typeof window.FormData === 'undefined' ||
14 | typeof window.fetch === 'undefined' ||
15 | typeof window.URLSearchParams === 'undefined'
16 | ) {
17 | return;
18 | }
19 |
20 | event.preventDefault();
21 |
22 | setFormStatus(statuses.SUBMITTING);
23 |
24 | const formData = new FormData(formRef.current);
25 |
26 | try {
27 | const { ok } = await fetch('/', {
28 | method: 'POST',
29 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
30 | body: new URLSearchParams(formData).toString(),
31 | });
32 | setFormStatus(ok ? statuses.SUCCESS : statuses.FAIL);
33 | } catch (err) {
34 | setFormStatus(statuses.FAIL);
35 | }
36 | };
37 |
38 | return (
39 |
40 | {formStatus !== statuses.SUCCESS ? (
41 |
46 | ) : (
47 | {t('recommendModal.form.successMessage')}
48 | )}
49 |
50 | );
51 | };
52 |
53 | export default RecommendModal;
54 |
--------------------------------------------------------------------------------
/lighthouserc.json:
--------------------------------------------------------------------------------
1 | {
2 | "ci": {
3 | "collect": {
4 | "numberOfRuns": 5
5 | },
6 | "assert": {
7 | "preset": "lighthouse:recommended",
8 | "assertions": {
9 | "viewport": "error",
10 | "first-contentful-paint": [
11 | "error",
12 | { "maxNumericValue": 2000, "aggregationMethod": "optimistic" }
13 | ],
14 | "interactive": [
15 | "error",
16 | { "maxNumericValue": 2000, "aggregationMethod": "optimistic" }
17 | ],
18 | "max-potential-fid": "off",
19 | "total-blocking-time": ["error", { "maxNumericValue": 199 }],
20 | "cumulative-layout-shift": [
21 | "error",
22 | { "maxNumericValue": 0, "aggregationMethod": "optimistic" }
23 | ],
24 | "categories:performance": [
25 | "error",
26 | { "minScore": 1, "aggregationMethod": "optimistic" }
27 | ],
28 | "categories:accessibility": [
29 | "error",
30 | { "minScore": 1, "aggregationMethod": "optimistic" }
31 | ],
32 | "resource-summary:script:size": ["error", { "maxNumericValue": 50000 }],
33 | "resource-summary:total:size": ["error", { "maxNumericValue": 300000 }],
34 | "resource-summary:font:count": ["error", { "maxNumericValue": 0 }],
35 | "resource-summary:third-party:count": [
36 | "error",
37 | { "maxNumericValue": 0 }
38 | ],
39 | "installable-manifest": "off",
40 | "is-crawlable": "off",
41 | "maskable-icon": "off",
42 | "works-offline": "off",
43 | "service-worker": "off",
44 | "splash-screen": "off",
45 | "themed-omnibox": "off",
46 | "offline-start-url": "off",
47 | "csp-xss": "off",
48 | "canonical": "off"
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/organisms/Relocation/Content.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useL10n } from '../../L10nContext';
3 | import Alert from '../../molecules/Alert';
4 | import List from '../../atoms/List';
5 | import imagePathJpg from './image.jpg';
6 | import imagePathWebp from './image.webp';
7 | import imagePathAvif from './image.avif';
8 | import Section from '../../molecules/Section';
9 |
10 | const Wrapper = styled.div`
11 | display: flex;
12 | flex-wrap: wrap;
13 | margin: -1em;
14 | `;
15 |
16 | const Picture = styled.picture`
17 | display: block;
18 | flex: 0 0 16em;
19 | max-width: calc(100% - 2em);
20 | margin: 1em;
21 | `;
22 |
23 | const Image = styled.img`
24 | display: block;
25 | width: 100%;
26 | height: auto;
27 |
28 | border-radius: 1em;
29 | `;
30 |
31 | const TextWrapper = styled.div`
32 | flex: 1 1 20em;
33 | margin: 1em;
34 | `;
35 |
36 | const Content = () => {
37 | const { t } = useL10n();
38 |
39 | return (
40 | <>
41 | {t('relocation.title')}
42 |
43 |
44 |
45 |
46 |
47 |
54 |
55 |
56 | {t('relocation.alert')}
57 | {t('relocation.description')}
58 | {t('relocation.listTitle')}
59 |
60 | {t('relocation.listItems').map((item) => (
61 | {item}
62 | ))}
63 |
64 |
65 |
66 | >
67 | );
68 | };
69 |
70 | export default Content;
71 |
--------------------------------------------------------------------------------
/src/components/organisms/Hero/index.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import config from '../../../config';
3 | import { useL10n } from '../../L10nContext';
4 | import useOnClickHydrate from '../../../hooks/useOnClickHydrate';
5 | import Section from '../../molecules/Section';
6 | import ShareButton from '../../molecules/ShareButton';
7 | import ButtonLink, {
8 | variants as buttonLinkVariants,
9 | } from '../../atoms/ButtonLink';
10 | import { variants as buttonVariants } from '../../atoms/Button';
11 | import RecommendButton from '../../molecules/RecommendButton';
12 |
13 | const StyledSection = styled(Section)`
14 | margin: 5em 0;
15 | `;
16 |
17 | const ContentWrapper = styled.div`
18 | display: flex;
19 | flex-direction: column;
20 | align-items: flex-start;
21 | margin-bottom: 3em;
22 | `;
23 |
24 | const ActionsWrapper = styled.div`
25 | display: flex;
26 | flex-wrap: wrap;
27 | margin: -0.5em;
28 |
29 | & > * {
30 | margin: 0.5em;
31 | }
32 | `;
33 |
34 | const Content =
35 | typeof window === 'undefined' ? require('./Content').default : () => null;
36 |
37 | const Hero = () => {
38 | const { t } = useL10n();
39 | const { handleClick, shouldBeHydrated } = useOnClickHydrate();
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
50 |
54 | {t('hero.actions.apply')}
55 |
56 |
57 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default Hero;
64 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "flatstack-warsaw",
3 | "version": "1.0.0",
4 | "license": "MIT",
5 | "private": true,
6 | "scripts": {
7 | "start": "webpack serve",
8 | "build": "webpack",
9 | "test": "echo \"This template does not include a test runner by default.\"",
10 | "format": "prettier --write \"src/**/*.{js,jsx,json,md}\"",
11 | "lint": "eslint './src/**/*.{js,jsx}' && stylelint './src/**/*.{js,jsx}'"
12 | },
13 | "dependencies": {
14 | "@headlessui/react": "^1.4.1",
15 | "core-js": "^3.18.3",
16 | "polished": "^4.1.3",
17 | "preact": "^10.5.14",
18 | "react-helmet-async": "^1.1.2",
19 | "regenerator-runtime": "^0.13.9",
20 | "styled-components": "^5.3.1"
21 | },
22 | "devDependencies": {
23 | "@babel/core": "^7.15.8",
24 | "@babel/eslint-parser": "^7.15.8",
25 | "@babel/preset-env": "^7.15.8",
26 | "@babel/preset-react": "^7.14.5",
27 | "babel-loader": "^8.2.2",
28 | "babel-plugin-polished": "^1.1.0",
29 | "babel-plugin-styled-components": "^1.13.2",
30 | "babel-plugin-transform-define": "^2.0.1",
31 | "clean-webpack-plugin": "^3.0.0",
32 | "copy-webpack-plugin": "^7.0.0",
33 | "eslint": "^7.32.0",
34 | "eslint-config-airbnb": "^18.2.1",
35 | "eslint-config-prettier": "^8.3.0",
36 | "eslint-plugin-import": "^2.24.2",
37 | "eslint-plugin-jsx-a11y": "^6.4.1",
38 | "eslint-plugin-prettier": "^4.0.0",
39 | "eslint-plugin-react": "^7.26.1",
40 | "eslint-plugin-react-hooks": "^4.2.0",
41 | "html-webpack-plugin": "^5.3.2",
42 | "preact-render-to-string": "^5.1.19",
43 | "prettier": "^2.4.1",
44 | "stylelint": "^13",
45 | "stylelint-config-rational-order": "^0.1.2",
46 | "stylelint-config-recommended": "^5",
47 | "stylelint-config-styled-components": "^0.1.1",
48 | "stylelint-order": "^4",
49 | "stylelint-processor-styled-components": "^1.10.0",
50 | "webpack": "^5.58.1",
51 | "webpack-cli": "^4.9.0",
52 | "webpack-dev-server": "^4.3.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/molecules/ShareButton/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState, lazy, Suspense } from 'react';
2 | import { useErrorBoundary } from 'preact/hooks';
3 | import styled from 'styled-components';
4 | import { useL10n } from '../../L10nContext';
5 | import Button from '../../atoms/Button';
6 | import LoadingIcon from '../../icons/LoadingIcon';
7 |
8 | const ShareModal = lazy(() => import('./ShareModal'));
9 |
10 | const StyledLoadingIcon = styled(LoadingIcon)`
11 | margin: -0.5em -0.5em -0.5em 0.5em;
12 |
13 | color: ${({ theme }) => theme.colors.accent};
14 | font-size: 1.5em;
15 | `;
16 |
17 | const ShareButton = (props) => {
18 | const { t } = useL10n();
19 | const [isModalOpen, setIsModalOpen] = useState(false);
20 | useErrorBoundary(() => {
21 | window.location.reload();
22 | });
23 |
24 | const handleShare = async () => {
25 | const shareData = {
26 | title: t('og:title'),
27 | text: t('og:description'),
28 | url: window.location.href,
29 | };
30 |
31 | if (typeof navigator.share !== 'undefined') {
32 | try {
33 | await navigator.share(shareData);
34 | return;
35 | } catch (err) {
36 | if (err.name === 'AbortError') {
37 | return;
38 | }
39 | }
40 | }
41 |
42 | setIsModalOpen(true);
43 | };
44 |
45 | return (
46 | <>
47 | {isModalOpen ? (
48 |
51 | {t('shareModal.triggerButtonText')}
52 |
53 | }
54 | >
55 |
58 | setIsModalOpen(false)} />
59 |
60 | ) : (
61 |
64 | )}
65 | >
66 | );
67 | };
68 |
69 | export default ShareButton;
70 |
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/FeedbackCard.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Card, { variants as cardVariants } from '../../atoms/Card';
3 |
4 | const Photo = styled.picture`
5 | display: block;
6 |
7 | flex: none;
8 |
9 | width: 4em;
10 | height: 4em;
11 | `;
12 |
13 | const Image = styled.img`
14 | display: block;
15 | width: 100%;
16 | height: auto;
17 |
18 | background-color: ${({ theme }) => theme.colors.secondary};
19 |
20 | border-radius: 1em;
21 | `;
22 |
23 | const Person = styled.div`
24 | display: flex;
25 | flex-direction: row;
26 | flex-wrap: wrap;
27 | align-items: center;
28 | margin: -0.5em;
29 |
30 | & > * {
31 | margin: 0.5em;
32 | }
33 | `;
34 |
35 | const PersonDescription = styled.div``;
36 |
37 | const Title = styled.h3`
38 | margin: 0 0 0.5em;
39 |
40 | color: ${({ theme }) => theme.colors.text};
41 | font-weight: bold;
42 | `;
43 |
44 | const SubTitle = styled.p`
45 | margin: 0;
46 |
47 | color: ${({ theme }) => theme.colors.paleText};
48 | `;
49 |
50 | const Wrapper = styled(Card)``;
51 |
52 | const Feedback = styled.p``;
53 |
54 | const FeedbackCard = ({ title, teamTime, feedback, photoAlt, photos }) => {
55 | return (
56 |
57 |
58 |
59 |
63 |
67 |
75 |
76 |
77 | {title}
78 | {teamTime}
79 |
80 |
81 | {feedback}
82 |
83 | );
84 | };
85 |
86 | export default FeedbackCard;
87 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { EnvironmentPlugin } = require('webpack');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const { CleanWebpackPlugin } = require('clean-webpack-plugin');
5 | const CopyPlugin = require('copy-webpack-plugin');
6 | const PatchHtmlWebpackPluginPlugin = require('./PatchHtmlWebpackPluginPlugin');
7 |
8 | const NODE_ENV = process.env.NODE_ENV || 'development';
9 |
10 | module.exports = {
11 | context: path.resolve(__dirname, 'src'),
12 | entry: { 'index-en': './index-en.jsx' },
13 | mode: NODE_ENV,
14 | devtool: NODE_ENV === 'production' ? 'source-map' : 'eval-source-map',
15 | resolve: {
16 | extensions: ['.js', '.jsx', '.json'],
17 | alias: {
18 | react: 'preact/compat',
19 | 'react-dom/test-utils': 'preact/test-utils',
20 | 'react-dom': 'preact/compat',
21 | 'react/jsx-runtime': 'preact/jsx-runtime',
22 | },
23 | },
24 | output: {
25 | filename: '[name].[contenthash].js',
26 | path: path.resolve(__dirname, 'dist'),
27 | },
28 | module: {
29 | rules: [
30 | { test: /\.jsx?$/, use: 'babel-loader', exclude: /node_modules/ },
31 | {
32 | oneOf: [
33 | {
34 | test: /\.(jpe?g|png|gif|svg|webp|avif)$/,
35 | resourceQuery: /inline/,
36 | type: 'asset/inline',
37 | },
38 | {
39 | test: /\.(jpe?g|png|gif|svg|webp|avif)$/,
40 | type: 'asset/resource',
41 | },
42 | ],
43 | },
44 | ],
45 | },
46 | plugins: [
47 | // About CONTEXT: https://docs.netlify.com/configure-builds/environment-variables/#build-metadata
48 | new EnvironmentPlugin({ NODE_ENV: 'development', CONTEXT: 'development' }),
49 | new PatchHtmlWebpackPluginPlugin(),
50 | new HtmlWebpackPlugin({
51 | filename: 'index.html',
52 | template: `./webpack-templates/index.js`,
53 | chunks: ['index-en'],
54 | }),
55 | new CleanWebpackPlugin(),
56 | new CopyPlugin({
57 | patterns: [{ from: 'static', to: '../dist' }],
58 | }),
59 | ],
60 | devServer: {
61 | host: 'localhost',
62 | open: true,
63 | historyApiFallback: {
64 | rewrites: [
65 | { from: /./, to: '/404.html' },
66 | ],
67 | },
68 | },
69 | };
70 |
--------------------------------------------------------------------------------
/src/components/molecules/Modal.jsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import { Dialog } from '@headlessui/react';
3 | import Button, { variants as buttonVariants } from '../atoms/Button';
4 | import CloseIcon from '../icons/CloseIcon';
5 |
6 | const Wrapper = styled(Dialog)`
7 | position: fixed;
8 | top: 0;
9 | right: 0;
10 | bottom: 0;
11 | left: 0;
12 | z-index: 1;
13 |
14 | overflow: hidden;
15 | overflow-y: auto;
16 | `;
17 |
18 | const WindowWrapper = styled.div`
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | min-height: 100%;
23 | padding: 1em;
24 | `;
25 |
26 | const Window = styled.div`
27 | position: relative;
28 |
29 | width: 40em;
30 | max-width: 100%;
31 | padding: 1em;
32 |
33 | background-color: #ffffff;
34 | border-radius: 1em;
35 | box-shadow: rgba(0, 0, 0, 0.5) 0 0.5em 1.5em;
36 | ${({ centered }) =>
37 | centered &&
38 | css`
39 | text-align: center;
40 | `}
41 | `;
42 |
43 | const Overlay = styled(Dialog.Overlay)`
44 | position: fixed;
45 | top: 0;
46 | right: 0;
47 | bottom: 0;
48 | left: 0;
49 |
50 | background-color: #000000;
51 | opacity: 0.3;
52 | `;
53 |
54 | const CloseButton = styled(Button)`
55 | position: absolute;
56 | top: -0.7em;
57 | right: -0.7em;
58 |
59 | display: flex;
60 | align-items: center;
61 | justify-content: center;
62 | width: 2em;
63 | height: 2em;
64 | padding: 0;
65 |
66 | border-radius: 100%;
67 | `;
68 |
69 | const StyledCloseIcon = styled(CloseIcon)`
70 | position: absolute;
71 | top: 50%;
72 | left: 50%;
73 |
74 | transform: translate(-50%, -50%);
75 | `;
76 |
77 | const Title = styled(Dialog.Title)`
78 | margin: 0 0 1em;
79 |
80 | font-size: 1.5em;
81 | `;
82 |
83 | const Modal = ({ isOpen, onClose, title, centered, children }) => (
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | {title}
93 |
94 | {children}
95 |
96 |
97 |
98 | );
99 |
100 | export default Modal;
101 |
--------------------------------------------------------------------------------
/src/components/organisms/Warsaw/Content.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useL10n } from '../../L10nContext';
3 |
4 | import Section from '../../molecules/Section';
5 | import imagePathAvif700 from './office_700.avif';
6 | import imagePathAvif1400 from './office_1400.avif';
7 | import imagePathWebp700 from './office_700.webp';
8 | import imagePathWebp1400 from './office_1400.webp';
9 | import imagePathJpg700 from './office_700.jpg';
10 | import imagePathJpg1400 from './office_1400.jpg';
11 |
12 | const Wrapper = styled.div`
13 | display: flex;
14 | flex-wrap: wrap;
15 | margin: -1em;
16 | `;
17 |
18 | const Figure = styled.figure`
19 | display: block;
20 | flex: 1 0 20em;
21 | max-width: calc(100% - 2em);
22 | margin: 1em;
23 | padding: 0;
24 |
25 | border: none;
26 | `;
27 |
28 | const Figcaption = styled.figcaption`
29 | margin: 1em 0 0 0;
30 |
31 | color: ${({ theme }) => theme.colors.paleText};
32 | font-size: 0.9em;
33 | font-style: italic;
34 | `;
35 |
36 | const Picture = styled.picture`
37 | display: block;
38 | width: 100%;
39 | `;
40 |
41 | const Image = styled.img`
42 | display: block;
43 | width: 100%;
44 | height: auto;
45 |
46 | border-radius: 1em;
47 | `;
48 |
49 | const TextWrapper = styled.div`
50 | flex: 1 0 20em;
51 | max-width: calc(100% - 2em);
52 | margin: 1em;
53 | `;
54 |
55 | const Content = () => {
56 | const { t } = useL10n();
57 |
58 | return (
59 |
60 |
61 | {t('warsaw.title')}
62 | {t('warsaw.description')}
63 |
64 |
65 |
66 |
70 |
74 |
82 |
83 | {t('warsaw.address')}
84 |
85 |
86 | );
87 | };
88 |
89 | export default Content;
90 |
--------------------------------------------------------------------------------
/src/components/organisms/Hero/Content.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import titleBgJpgPath from './title-bg.jpg?inline';
3 | import titleBgWebpPath from './title-bg.webp?inline';
4 | import { useL10n } from '../../L10nContext';
5 |
6 | const Title = styled.h1`
7 | /* depend on viewport width, but not bigger than 5em and not smaller than 3em */
8 | margin: 0 0 0.3em;
9 |
10 | font-weight: 900;
11 | font-size: clamp(3em, 15vw, 5em);
12 | line-height: 1;
13 |
14 | background-repeat: no-repeat;
15 | background-position: center center;
16 | background-size: cover;
17 | filter: brightness(70%);
18 |
19 | body.no-webp & {
20 | background-image: url('${titleBgJpgPath}');
21 | /* stylelint-disable-next-line */
22 | -webkit-background-clip: text;
23 | -webkit-text-fill-color: transparent;
24 | }
25 |
26 | body.webp & {
27 | background-image: url('${titleBgWebpPath}');
28 | /* stylelint-disable-next-line */
29 | -webkit-background-clip: text;
30 | -webkit-text-fill-color: transparent;
31 | }
32 | `;
33 |
34 | const Subtitle = styled.span`
35 | /* depend on viewport width, but not bigger than 2em and not smaller than 1.25em */
36 | font-size: clamp(1.25em, 6vw, 2em);
37 | `;
38 |
39 | const SubtitleHighlight = styled.span`
40 | color: ${({ theme }) => theme.colors.accent};
41 | `;
42 |
43 | const TechnologiesWrapper = styled.div`
44 | display: flex;
45 | flex-wrap: wrap;
46 | max-width: 36em;
47 | margin: 1.75em -0.25em -0.25em;
48 |
49 | & > * {
50 | margin: 0.25em;
51 | }
52 | `;
53 |
54 | const Technology = styled.div`
55 | padding: 0.3em 0.8em;
56 |
57 | font-size: 1em;
58 |
59 | background-color: ${({ theme }) => theme.colors.primary};
60 | border-radius: 0.7em;
61 | `;
62 |
63 | const technologies = [
64 | 'Ruby on Rails',
65 | 'PostgreSQL',
66 | 'React.js',
67 | 'REST',
68 | 'TDD',
69 | 'Docker',
70 | 'AWS',
71 | 'Terraform',
72 | 'TypeScript',
73 | 'Serverless',
74 | ];
75 |
76 | const Content = () => {
77 | const { t } = useL10n();
78 |
79 | return (
80 | <>
81 | Fullstack Developer
82 |
83 | {t('hero.subtitle', { Highlight: SubtitleHighlight })}
84 |
85 | {t('hero.salary')}
86 |
87 | {technologies.map((technology) => (
88 | {technology}
89 | ))}
90 |
91 | >
92 | );
93 | };
94 |
95 | export default Content;
96 |
--------------------------------------------------------------------------------
/src/components/organisms/Vacancy/Content.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import config from '../../../config';
3 | import { useL10n } from '../../L10nContext';
4 | import Card, { variants as cardVariants } from '../../atoms/Card';
5 | import ButtonLink, {
6 | paddingVariants as buttonPaddingVariants,
7 | variants as buttonLinkVariants,
8 | } from '../../atoms/ButtonLink';
9 | import List from '../../atoms/List';
10 |
11 | const StyledCard = styled(Card)`
12 | margin-bottom: 2em;
13 | `;
14 |
15 | const VacancyTitle = styled.h2`
16 | max-width: 40rem;
17 | margin: 0 0 1em 0;
18 |
19 | font-weight: bold;
20 | font-size: clamp(1.5em, 5vw, 2em);
21 | `;
22 |
23 | const VacancyDescription = styled.p`
24 | max-width: 40rem;
25 | margin: 0;
26 | `;
27 |
28 | const CardContent = styled.div`
29 | display: flex;
30 | flex-wrap: wrap;
31 | margin: -1rem;
32 | `;
33 |
34 | const CardTitle = styled.h3`
35 | flex: 0 0 15rem;
36 | max-width: calc(100% - 2rem);
37 | margin: 1rem;
38 |
39 | font-weight: bold;
40 | font-size: clamp(1.5em, 5vw, 2em);
41 | `;
42 |
43 | const StyledList = styled(List)`
44 | flex: 1 1 20em;
45 | max-width: calc(100% - 2rem);
46 | margin: 1rem;
47 | `;
48 |
49 | const ButtonWrapper = styled.div`
50 | display: flex;
51 | justify-content: center;
52 | `;
53 |
54 | const BLOCKS = ['role', 'workOrganisation', 'requirements', 'benefits'];
55 |
56 | const Content = () => {
57 | const { t } = useL10n();
58 |
59 | return (
60 | <>
61 |
62 | {t('vacancy.vacancyDescription')}
63 |
64 | {t('vacancy.aboutProduct.description')}
65 |
66 |
67 | {t(`vacancy.aboutProduct.items`).map((item) => (
68 | {item}
69 | ))}
70 |
71 |
72 | {BLOCKS.map((block) => (
73 |
74 |
75 | {t(`vacancy.${block}.title`)}
76 |
77 | {t(`vacancy.${block}.items`).map((item) => (
78 | {item}
79 | ))}
80 |
81 |
82 |
83 | ))}
84 |
85 |
90 | {t('vacancy.apply')}
91 |
92 |
93 | >
94 | );
95 | };
96 |
97 | export default Content;
98 |
--------------------------------------------------------------------------------
/src/components/organisms/Header/Content.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useL10n } from '../../L10nContext';
3 | import config from '../../../config';
4 | import Link, { underlineTypes } from '../../atoms/Link';
5 | import Container from '../../atoms/Container';
6 | import LogoSvg from './LogoSvg';
7 |
8 | const InnerWrapper = styled.div`
9 | display: flex;
10 | flex-wrap: wrap;
11 | align-items: center;
12 | justify-content: space-between;
13 | margin: 0.5rem -1em -1em;
14 | `;
15 |
16 | const StyledLogo = styled(LogoSvg)`
17 | flex: none;
18 | margin: 1em;
19 | `;
20 |
21 | const Menu = styled.nav``;
22 |
23 | const Ul = styled.ul`
24 | display: flex;
25 | flex-direction: row;
26 | margin: 0;
27 | padding: 0;
28 |
29 | font-size: 1.25em;
30 |
31 | list-style-type: none;
32 | `;
33 |
34 | const Li = styled.li`
35 | margin-right: 0.8em;
36 |
37 | &:last-of-type {
38 | margin-right: 0;
39 | }
40 | `;
41 |
42 | const RightCol = styled.div`
43 | display: flex;
44 | flex-direction: row;
45 | align-items: baseline;
46 | margin: 0.5em;
47 | padding: 0.5em;
48 | overflow: auto;
49 | `;
50 |
51 | const Content = () => {
52 | const { t } = useL10n();
53 |
54 | return (
55 |
56 |
57 |
58 |
59 |
61 | -
62 |
66 | {t('menu.vacancy')}
67 |
68 |
69 | -
70 |
74 | {t('menu.company')}
75 |
76 |
77 | -
78 |
82 | {t('menu.relocation')}
83 |
84 |
85 | -
86 |
90 | {t('menu.feedbacks')}
91 |
92 |
93 | -
94 |
98 | {t('menu.warsaw')}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | );
107 | };
108 |
109 | export default Content;
110 |
--------------------------------------------------------------------------------
/src/components/organisms/Footer/Content.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import Link from '../../atoms/Link';
3 | import InstagramIcon from '../../icons/InstagramIcon';
4 | import MediumIcon from '../../icons/MediumIcon';
5 | import FacebookIcon from '../../icons/FacebookIcon';
6 | import LinkedInIcon from '../../icons/LinkedInIcon';
7 | import TwitterIcon from '../../icons/TwitterIcon';
8 | import GitHubIcon from '../../icons/GitHubIcon';
9 |
10 | const InnerWrapper = styled.div`
11 | display: flex;
12 | flex-wrap: wrap;
13 | justify-content: space-between;
14 | margin: -1em;
15 | `;
16 |
17 | const CopyrightText = styled.p`
18 | margin: 1em;
19 | `;
20 |
21 | const SocialLinksWrapper = styled.div`
22 | display: flex;
23 | flex-wrap: wrap;
24 | margin: 0.5em;
25 |
26 | & > * {
27 | margin: 0.5em;
28 | }
29 | `;
30 |
31 | const StyledIcon = styled(({ icon: Icon, ...props }) => )`
32 | display: block;
33 |
34 | color: ${({ theme }) => theme.colors.secondary};
35 | font-size: 1.5em;
36 | `;
37 |
38 | const Content = () => {
39 | return (
40 |
41 | © 2021 Flatstack LLC
42 |
43 |
50 |
51 |
52 |
59 |
60 |
61 |
68 |
69 |
70 |
77 |
78 |
79 |
86 |
87 |
88 |
95 |
96 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default Content;
103 |
--------------------------------------------------------------------------------
/src/components/organisms/Feedbacks/Content.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import FeedbackCard from './FeedbackCard';
3 | import { useL10n } from '../../L10nContext';
4 | import alinaAvif128 from './alina_128.avif';
5 | import alinaWebp128 from './alina_128.webp';
6 | import alinaJpg128 from './alina_128.jpg';
7 | import alinaAvif64 from './alina_64.avif';
8 | import alinaWebp64 from './alina_64.webp';
9 | import alinaJpg64 from './alina_64.jpg';
10 | import dmitryAvif128 from './dmitry_128.avif';
11 | import dmitryWebp128 from './dmitry_128.webp';
12 | import dmitryJpg128 from './dmitry_128.jpg';
13 | import dmitryAvif64 from './dmitry_64.avif';
14 | import dmitryWebp64 from './dmitry_64.webp';
15 | import dmitryJpg64 from './dmitry_64.jpg';
16 | import arkadiiAvif128 from './arkadey_128.avif';
17 | import arkadiiWebp128 from './arkadey_128.webp';
18 | import arkadiiJpg128 from './arkadey_128.jpg';
19 | import arkadiiAvif64 from './arkadey_64.avif';
20 | import arkadiiWebp64 from './arkadey_64.webp';
21 | import arkadiiJpg64 from './arkadey_64.jpg';
22 | import askarAvif128 from './askar_128.avif';
23 | import askarWebp128 from './askar_128.webp';
24 | import askarJpg128 from './askar_128.jpg';
25 | import askarAvif64 from './askar_64.avif';
26 | import askarWebp64 from './askar_64.webp';
27 | import askarJpg64 from './askar_64.jpg';
28 |
29 | const alinaPhotos = {
30 | avifSmall: alinaAvif64,
31 | avifBig: alinaAvif128,
32 | webpSmall: alinaWebp64,
33 | webpBig: alinaWebp128,
34 | jpgSmall: alinaJpg64,
35 | jpgBig: alinaJpg128,
36 | };
37 |
38 | const dmitryPhotos = {
39 | avifSmall: dmitryAvif64,
40 | avifBig: dmitryAvif128,
41 | webpSmall: dmitryWebp64,
42 | webpBig: dmitryWebp128,
43 | jpgSmall: dmitryJpg64,
44 | jpgBig: dmitryJpg128,
45 | };
46 |
47 | const arkadiiPhotos = {
48 | avifSmall: arkadiiAvif64,
49 | avifBig: arkadiiAvif128,
50 | webpSmall: arkadiiWebp64,
51 | webpBig: arkadiiWebp128,
52 | jpgSmall: arkadiiJpg64,
53 | jpgBig: arkadiiJpg128,
54 | };
55 |
56 | const askarPhotos = {
57 | avifSmall: askarAvif64,
58 | avifBig: askarAvif128,
59 | webpSmall: askarWebp64,
60 | webpBig: askarWebp128,
61 | jpgSmall: askarJpg64,
62 | jpgBig: askarJpg128,
63 | };
64 |
65 | const CardsWrapper = styled.div`
66 | columns: 25em auto;
67 | column-gap: 2em;
68 | `;
69 |
70 | const CardWrapper = styled.div`
71 | break-inside: avoid;
72 |
73 | max-width: 100%;
74 |
75 | margin-bottom: 2em;
76 |
77 | page-break-inside: avoid;
78 | `;
79 |
80 | const Content = () => {
81 | const { t } = useL10n();
82 | return (
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
95 |
96 |
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | export default Content;
104 |
--------------------------------------------------------------------------------
/src/components/molecules/RecommendButton/RecommendForm.jsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react';
2 | import styled from 'styled-components';
3 | import Button, { variants as buttonVariants } from '../../atoms/Button';
4 | import Link from '../../atoms/Link';
5 | import Input from '../../atoms/Input';
6 | import LoadingIcon from '../../icons/LoadingIcon';
7 | import { useL10n } from '../../L10nContext';
8 | import RequiredFormText from '../../atoms/RequiredFormText';
9 |
10 | const Agreement = styled.p`
11 | color: ${({ theme }) => theme.colors.paleText};
12 | `;
13 |
14 | const Fieldset = styled.fieldset`
15 | display: block;
16 | margin: 0 0 2em 0;
17 | padding: 0;
18 |
19 | border: none;
20 | `;
21 |
22 | const FieldsetLegend = styled.legend`
23 | display: block;
24 | margin: 0 0 1em 0;
25 | padding: 0;
26 |
27 | font-weight: bold;
28 | font-size: 1em;
29 | `;
30 |
31 | const StyledInput = styled(Input)`
32 | margin: 0 0 1em 0;
33 | `;
34 |
35 | const StyledTextarea = styled(({ className, ...restProps }) => (
36 |
37 | )).attrs(() => ({
38 | textarea: true,
39 | }))`
40 | min-height: 5em;
41 |
42 | resize: vertical;
43 | `;
44 |
45 | const ErrorMessage = styled.p`
46 | color: red;
47 | `;
48 |
49 | const StyledLoadingIcon = styled(LoadingIcon)`
50 | margin: -0.5em -1.5em -0.5em 0.5em;
51 |
52 | font-size: 1.5em;
53 | `;
54 |
55 | export const statuses = {
56 | IDLE: 'IDLE',
57 | SUBMITTING: 'SUBMITTING',
58 | SUCCESS: 'SUCCESS',
59 | FAIL: 'FAIL',
60 | };
61 |
62 | const RecommendForm = forwardRef(({ onSubmit, status }, ref) => {
63 | const { t } = useL10n();
64 |
65 | return (
66 |
137 | );
138 | });
139 |
140 | export default RecommendForm;
141 |
--------------------------------------------------------------------------------
/src/components/molecules/ShareButton/ShareModal.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useMemo } from 'react';
2 | import styled from 'styled-components';
3 | import { useL10n } from '../../L10nContext';
4 | import ButtonLink, {
5 | variants as buttonVariants,
6 | paddingVariants as buttonPaddingVariants,
7 | } from '../../atoms/ButtonLink';
8 | import Button from '../../atoms/Button';
9 | import Input from '../../atoms/Input';
10 | import FacebookIcon from '../../icons/FacebookIcon';
11 | import TelegramIcon from '../../icons/TelegramIcon';
12 | import TwitterIcon from '../../icons/TwitterIcon';
13 | import LinkedInIcon from '../../icons/LinkedInIcon';
14 | import Modal from '../Modal';
15 |
16 | const LinksWrapper = styled.div`
17 | display: flex;
18 | flex-wrap: wrap;
19 | margin: -0.5em -0.5em 0.5em -0.5em;
20 |
21 | & > * {
22 | flex: 1 0 15em;
23 | max-width: calc(100% - 1em);
24 | margin: 0.5em;
25 | }
26 | `;
27 |
28 | const StyledIcon = styled(({ icon: Icon, ...props }) => )`
29 | flex: none;
30 | margin-right: 0.4em;
31 |
32 | font-size: 1.5em;
33 | `;
34 |
35 | const Link = styled(ButtonLink)`
36 | display: flex;
37 | align-items: center;
38 | `;
39 |
40 | const CopyRow = styled.div`
41 | display: flex;
42 | `;
43 |
44 | const CopyInput = styled(Input)`
45 | flex: 1 1 auto;
46 | margin-right: 0.5em;
47 | `;
48 |
49 | const CopyButton = styled(Button)`
50 | flex: none;
51 | `;
52 |
53 | const ShareModal = ({ isOpen, onClose }) => {
54 | const { t } = useL10n();
55 | const [copyStatus, setCopyStatus] = useState(null);
56 |
57 | const selfLink = window.location.href;
58 |
59 | const handleCopy = async () => {
60 | if (typeof navigator.clipboard === 'undefined') {
61 | return;
62 | }
63 |
64 | try {
65 | await navigator.clipboard.writeText(selfLink);
66 | setCopyStatus({ value: true });
67 | } catch (e) {
68 | setCopyStatus({ value: false });
69 | }
70 | };
71 |
72 | useEffect(() => {
73 | const timeoutID = setTimeout(() => {
74 | setCopyStatus(null);
75 | }, 1000);
76 |
77 | return () => clearTimeout(timeoutID);
78 | }, [copyStatus]);
79 |
80 | const copyButtonText = useMemo(() => {
81 | switch (copyStatus?.value) {
82 | case true:
83 | return t('shareModal.copySuccess');
84 | case false:
85 | return t('shareModal.copyFailed');
86 | default:
87 | return t('shareModal.copy');
88 | }
89 | }, [copyStatus, t]);
90 |
91 | return (
92 |
93 |
94 |
103 |
104 | {t('shareModal.shareViaFacebook')}
105 |
106 |
113 |
114 | {t('shareModal.shareViaTelegram')}
115 |
116 |
125 |
126 | {t('shareModal.shareViaTwitter')}
127 |
128 |
137 |
138 | {t('shareModal.shareViaLinkedIn')}
139 |
140 |
141 |
142 |
143 |
144 | {copyButtonText}
145 |
146 |
147 | );
148 | };
149 |
150 | export default ShareModal;
151 |
--------------------------------------------------------------------------------
/PatchHtmlWebpackPluginPlugin.js:
--------------------------------------------------------------------------------
1 | // Temporary fix
2 | // See issue: https://github.com/jantimon/html-webpack-plugin/issues/1590
3 |
4 | const WebpackOptionsApply = require('webpack/lib/WebpackOptionsApply');
5 | const ResolverFactory = require('webpack/lib/ResolverFactory');
6 |
7 | class PatchHtmlWebpackPluginPlugin {
8 | apply(compiler) {
9 | compiler.hooks.compilation.tap(
10 | 'PatchHtmlWebpackPluginPlugin',
11 | (compilation) => {
12 | compilation.hooks.childCompiler.tap(
13 | 'PatchHtmlWebpackPluginPlugin',
14 | (childCompiler, compilerName) => {
15 | if (compilerName !== 'HtmlWebpackCompiler') {
16 | return;
17 | }
18 |
19 | const optionsCopy = { ...childCompiler.options };
20 | optionsCopy.optimization = {
21 | ...optionsCopy.optimization,
22 | concatenateModules: false,
23 | };
24 | optionsCopy.resolve = {
25 | ...optionsCopy.resolve,
26 | };
27 |
28 | optionsCopy.externalsPresets = {
29 | web: false,
30 | node: true,
31 | nwjs: false,
32 | electron: false,
33 | electronMain: false,
34 | electronPreload: false,
35 | electronRenderer: false,
36 | };
37 | optionsCopy.loader = {
38 | target: 'node',
39 | };
40 | optionsCopy.target = 'node';
41 | optionsCopy.node = {
42 | global: false,
43 | __filename: 'eval-only',
44 | __dirname: 'eval-only',
45 | };
46 | optionsCopy.output = {
47 | ...childCompiler.options.output,
48 | chunkFormat: 'commonjs',
49 | chunkLoading: 'require',
50 | enabledChunkLoadingTypes: ['require'],
51 | enabledWasmLoadingTypes: ['async-node'],
52 | globalObject: 'global',
53 | wasmLoading: 'async-node',
54 | workerChunkLoading: 'require',
55 | workerWasmLoading: 'async-node',
56 | };
57 | optionsCopy.resolve.conditionNames = [
58 | 'webpack',
59 | 'development',
60 | 'node',
61 | ];
62 | optionsCopy.resolve.byDependency = {
63 | wasm: {
64 | conditionNames: ['import', 'module', '...'],
65 | aliasFields: [],
66 | mainFields: ['module', '...'],
67 | },
68 | esm: {
69 | conditionNames: ['import', 'module', '...'],
70 | aliasFields: [],
71 | mainFields: ['module', '...'],
72 | },
73 | worker: {
74 | conditionNames: ['import', 'module', '...'],
75 | aliasFields: [],
76 | mainFields: ['module', '...'],
77 | preferRelative: true,
78 | },
79 | commonjs: {
80 | conditionNames: ['require', 'module', '...'],
81 | aliasFields: [],
82 | mainFields: ['module', '...'],
83 | },
84 | amd: {
85 | conditionNames: ['require', 'module', '...'],
86 | aliasFields: [],
87 | mainFields: ['module', '...'],
88 | },
89 | loader: {
90 | conditionNames: ['require', 'module', '...'],
91 | aliasFields: [],
92 | mainFields: ['module', '...'],
93 | },
94 | unknown: {
95 | conditionNames: ['require', 'module', '...'],
96 | aliasFields: [],
97 | mainFields: ['module', '...'],
98 | },
99 | undefined: {
100 | conditionNames: ['require', 'module', '...'],
101 | aliasFields: [],
102 | mainFields: ['module', '...'],
103 | },
104 | url: {
105 | preferRelative: true,
106 | },
107 | };
108 |
109 | // it is needed because `resolverFactory` already exist on childCompiler and it refers to old options.resolve object
110 | // so we need to reinitialize resolverFactory before compilation
111 | childCompiler.resolverFactory = new ResolverFactory();
112 |
113 | // but new resolverFactory should be initialized. And it is what WebpackOptionsApply for.
114 | childCompiler.options = new WebpackOptionsApply().process(
115 | optionsCopy,
116 | childCompiler,
117 | );
118 | },
119 | );
120 | },
121 | );
122 | }
123 | }
124 |
125 | module.exports = PatchHtmlWebpackPluginPlugin;
126 |
--------------------------------------------------------------------------------
/src/components/organisms/Header/LogoSvg.jsx:
--------------------------------------------------------------------------------
1 | import { useL10n } from '../../L10nContext';
2 |
3 | const LogoSvg = (props) => {
4 | const { t } = useL10n();
5 |
6 | return (
7 |
29 | );
30 | };
31 |
32 | export default LogoSvg;
33 |
--------------------------------------------------------------------------------
/src/components/organisms/Company/Content.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import { useL10n } from '../../L10nContext';
3 | import Section from '../../molecules/Section';
4 | import flatstackJpgPhotoPath from './flatstack.jpg';
5 | import flatstackWebpPhotoPath from './flatstack.webp';
6 | import flatstackAvifPhotoPath from './flatstack.avif';
7 | import flatstackAvif2xPhotoPath from './flatstack2x.avif';
8 | import flatstackWarsawJpgPhotoPath from './fs-warsaw.jpg';
9 | import flatstackWarsawWebpPhotoPath from './fs-warsaw.webp';
10 | import flatstackWarsawAvifPhotoPath from './fs-warsaw.avif';
11 | import flatstackNewOrleansJpgPhotoPath from './fs-no.jpg';
12 | import flatstackNewOrleansWebpPhotoPath from './fs-no.webp';
13 | import flatstackNewOrleansAvifPhotoPath from './fs-no.avif';
14 | import flatstackAntalyaJpgPhotoPath from './fs-antalya.jpg';
15 | import flatstackAntalyaWebpPhotoPath from './fs-antalya.webp';
16 | import flatstackAntalyaAvifPhotoPath from './fs-antalya.avif';
17 |
18 | const Row = styled.div`
19 | display: flex;
20 | flex-wrap: wrap;
21 | margin: -1em -1em 1em -1em;
22 | `;
23 |
24 | const TextWrapper = styled.div`
25 | flex: 1 0 30em;
26 | max-width: calc(100% - 2em);
27 | margin: 1em;
28 | `;
29 |
30 | const FlatstackPicture = styled.picture`
31 | display: block;
32 | flex: 1 1 40em;
33 | margin: 1em;
34 | `;
35 |
36 | const Image = styled.img`
37 | display: block;
38 | width: 100%;
39 | height: auto;
40 |
41 | border-radius: 1em;
42 | `;
43 |
44 | const Description = styled.p``;
45 |
46 | const ImagesRow = styled.div`
47 | display: grid;
48 | grid-gap: 2em;
49 | grid-template-columns: repeat(auto-fit, minmax(min(20em, 100%), 1fr));
50 | gap: 2em;
51 | margin-bottom: 2em;
52 | `;
53 |
54 | const OfficePicture = styled.picture`
55 | display: block;
56 | `;
57 |
58 | const Figure = styled.figure`
59 | display: block;
60 | margin: 0;
61 | padding: 0;
62 |
63 | border: none;
64 | `;
65 |
66 | const Figcaption = styled.figcaption`
67 | margin: 1em 0 0 0;
68 | `;
69 |
70 | const Content = () => {
71 | const { t } = useL10n();
72 | return (
73 | <>
74 |
75 |
76 | {t('company.title')}
77 | {t('company.description')}
78 |
79 |
80 |
84 |
85 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
106 |
107 | {t('company.flatstackWarsawImageAlt')}
108 |
109 |
110 |
111 |
115 |
119 |
126 |
127 | {t('company.flatstackNewOrleansImageAlt')}
128 |
129 |
130 |
131 |
132 |
133 |
140 |
141 | {t('company.flatstackAntalyaImageAlt')}
142 |
143 |
144 | >
145 | );
146 | };
147 |
148 | export default Content;
149 |
--------------------------------------------------------------------------------
/src/components/Page.jsx:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components';
2 | import { useL10n } from './L10nContext';
3 | import HiddenFormForNetlify from './molecules/RecommendButton/HiddenFormForNetlify';
4 | import Header from './organisms/Header';
5 | import Hero from './organisms/Hero';
6 | import Footer from './organisms/Footer';
7 | import Vacancy from './organisms/Vacancy';
8 | import Relocation from './organisms/Relocation';
9 | import Feedbacks from './organisms/Feedbacks';
10 | import Warsaw from './organisms/Warsaw';
11 | import Join from './organisms/Join';
12 | import Company from './organisms/Company';
13 |
14 | const Helmet =
15 | typeof window === 'undefined'
16 | ? require('react-helmet-async').Helmet
17 | : () => null;
18 |
19 | const GlobalStyle = createGlobalStyle`
20 | * {
21 | box-sizing: border-box;
22 | }
23 |
24 | html {
25 | scroll-behavior: smooth;
26 | }
27 |
28 | body {
29 | margin: 0;
30 |
31 | color: ${({ theme }) => theme.colors.text};
32 | font-size: 1rem;
33 | font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";
34 | line-height: 1.5;
35 |
36 | fill: currentColor;
37 | -webkit-font-smoothing: antialiased;
38 | }
39 |
40 | img {
41 | background-color: ${({ theme }) => theme.colors.secondary};
42 | }
43 | `;
44 |
45 | const Page = () => {
46 | const { t, locale } = useL10n();
47 |
48 | return (
49 | <>
50 |
51 | {typeof window === 'undefined' ? (
52 |
53 |
54 |
55 | {t('title')}
56 |
57 |
58 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
83 | {process.env.NODE_ENV === 'production' ? (
84 |
89 | ) : null}
90 |
91 | ) : null}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | >
103 | );
104 | };
105 |
106 | export default Page;
107 |
--------------------------------------------------------------------------------
/src/components/atoms/Button.jsx:
--------------------------------------------------------------------------------
1 | import styled, { css } from 'styled-components';
2 | import { shade, tint } from 'polished';
3 |
4 | export const variants = {
5 | ACCENT: 'ACCENT',
6 | TEXT: 'TEXT',
7 | ACCENT_TEXT: 'ACCENT_TEXT',
8 | BORDERED: 'BORDERED',
9 | PRIMARY: 'PRIMARY',
10 | };
11 |
12 | export const paddingVariants = {
13 | NORMAL: 'NORMAL',
14 | SYMMETRIC: 'SYMMETRIC',
15 | BIG: 'BIG',
16 | SYMMETRIC_BIG: 'SYMMETRIC_BIG',
17 | };
18 |
19 | const accentCss = css`
20 | color: ${({ theme }) => theme.colors.invertedText};
21 | font-weight: 600;
22 |
23 | background-color: ${({ theme }) => theme.colors.accent};
24 |
25 | &:focus {
26 | color: ${({ theme }) => theme.colors.invertedText};
27 |
28 | background-color: ${({ theme }) => shade(0.1, theme.colors.accent)};
29 | outline: none;
30 | }
31 |
32 | &:focus-visible {
33 | background-color: ${({ theme }) => theme.colors.accent};
34 | outline: ${({ theme }) => theme.colors.outline} solid 0.15em;
35 | }
36 |
37 | &:focus:not(:focus-visible) {
38 | background-color: ${({ theme }) => theme.colors.accent};
39 | }
40 |
41 | &&:hover {
42 | color: ${({ theme }) => theme.colors.invertedText};
43 |
44 | background-color: ${({ theme }) => shade(0.1, theme.colors.accent)};
45 | }
46 | `;
47 |
48 | const borderedCss = css`
49 | border: 1px solid ${({ theme }) => theme.colors.secondary};
50 |
51 | &:focus {
52 | outline: none;
53 | }
54 |
55 | &:focus-visible {
56 | background-color: transparent;
57 | outline: ${({ theme }) => theme.colors.outline} solid 0.15em;
58 | }
59 |
60 | &:focus:not(:focus-visible) {
61 | background-color: transparent;
62 | }
63 |
64 | &&:hover {
65 | background-color: ${({ theme }) => tint(0.5, theme.colors.primary)};
66 | }
67 | `;
68 |
69 | const primaryCss = css`
70 | background-color: ${({ theme }) => theme.colors.primary};
71 |
72 | &:focus {
73 | background-color: ${({ theme }) => shade(0.1, theme.colors.primary)};
74 | outline: none;
75 | }
76 |
77 | &:focus-visible {
78 | background-color: ${({ theme }) => theme.colors.primary};
79 | outline: ${({ theme }) => theme.colors.outline} solid 0.15em;
80 | }
81 |
82 | &:focus:not(:focus-visible) {
83 | background-color: ${({ theme }) => theme.colors.primary};
84 | }
85 |
86 | &&:hover {
87 | background-color: ${({ theme }) => shade(0.1, theme.colors.primary)};
88 | }
89 | `;
90 |
91 | const accentTextCss = css`
92 | color: ${({ theme }) => theme.colors.accent};
93 | font-weight: 600;
94 |
95 | &:focus {
96 | color: ${({ theme }) => shade(0.1, theme.colors.accent)};
97 |
98 | outline: none;
99 | }
100 |
101 | &:focus-visible {
102 | color: ${({ theme }) => theme.colors.accent};
103 |
104 | outline: ${({ theme }) => theme.colors.outline} solid 0.15em;
105 | }
106 |
107 | &:focus:not(:focus-visible) {
108 | color: ${({ theme }) => theme.colors.accent};
109 | }
110 |
111 | &&:hover {
112 | color: ${({ theme }) => shade(0.1, theme.colors.accent)};
113 | }
114 | `;
115 |
116 | const cssByConstant = {
117 | [variants.ACCENT]: accentCss,
118 | [variants.ACCENT_TEXT]: accentTextCss,
119 | [variants.BORDERED]: borderedCss,
120 | [variants.PRIMARY]: primaryCss,
121 | };
122 |
123 | const cssByPaddingVariant = {
124 | [paddingVariants.NORMAL]: css`
125 | padding: 0.5em 1.5em;
126 | ${({ negativeMargins }) =>
127 | negativeMargins &&
128 | css`
129 | margin: -0.5em -1.5em;
130 | `}
131 | `,
132 | [paddingVariants.BIG]: css`
133 | padding: 1em 2em;
134 | ${({ negativeMargins }) =>
135 | negativeMargins &&
136 | css`
137 | margin: -1em -2em;
138 | `}
139 | `,
140 | [paddingVariants.SYMMETRIC]: css`
141 | padding: 0.5em 0.8em;
142 | ${({ negativeMargins }) =>
143 | negativeMargins &&
144 | css`
145 | margin: -0.5em -0.8em;
146 | `}
147 | `,
148 | [paddingVariants.SYMMETRIC_BIG]: css`
149 | padding: 1em 1.2em;
150 | ${({ negativeMargins }) =>
151 | negativeMargins &&
152 | css`
153 | margin: -1em -1.2em;
154 | `}
155 | `,
156 | };
157 |
158 | const inlineCss = css`
159 | display: inline-block;
160 | `;
161 |
162 | const fullWidthCss = css`
163 | width: 100%;
164 | `;
165 |
166 | const Button = styled.button`
167 | display: flex;
168 | align-items: center;
169 | justify-content: flex-start;
170 | margin: 0;
171 |
172 | color: ${({ theme }) => theme.colors.text};
173 | font-weight: inherit;
174 | font-size: inherit;
175 | font-family: inherit;
176 | line-height: inherit;
177 | text-decoration: none;
178 |
179 | background: none;
180 | border: none;
181 | border-radius: 0.7em;
182 | outline-offset: 0.15em;
183 | cursor: pointer;
184 |
185 | user-select: none;
186 |
187 | ${({ paddingVariant }) => cssByPaddingVariant[paddingVariant]};
188 |
189 | ${({ centered }) =>
190 | centered &&
191 | css`
192 | text-align: center;
193 | justify-content: center;
194 | `};
195 |
196 | ${({ fullWidth }) => fullWidth && fullWidthCss};
197 |
198 | &:active,
199 | &:focus,
200 | &:hover {
201 | color: ${({ theme }) => theme.colors.text};
202 | text-decoration: none;
203 | }
204 |
205 | ${({ variant }) => cssByConstant[variant]};
206 |
207 | ${({ inline }) => inline && inlineCss};
208 | `;
209 |
210 | Button.defaultProps = {
211 | variant: variants.PRIMARY,
212 | centered: true,
213 | paddingVariant: paddingVariants.NORMAL,
214 | };
215 |
216 | export default Button;
217 |
--------------------------------------------------------------------------------
/src/components/organisms/Join/index.jsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState, lazy, Suspense } from 'react';
2 | import { useErrorBoundary } from 'preact/hooks';
3 | import styled from 'styled-components';
4 | import config from '../../../config';
5 | import Section from '../../molecules/Section';
6 | import { useL10n } from '../../L10nContext';
7 | import Input from '../../atoms/Input';
8 | import Button, {
9 | variants as buttonVariants,
10 | paddingVariants as buttonPaddingVariants,
11 | } from '../../atoms/Button';
12 | import Link from '../../atoms/Link';
13 | import Card from '../../atoms/Card';
14 | import useOnClickHydrate from '../../../hooks/useOnClickHydrate';
15 | import FacebookIcon from '../../icons/FacebookIcon';
16 | import TelegramIcon from '../../icons/TelegramIcon';
17 | import LoadingIcon from '../../icons/LoadingIcon';
18 | import EmailIcon from '../../icons/EmailIcon';
19 | import HrDecoratedPhoto from './HrDecoratedPhoto';
20 | import ShareButton from '../../molecules/ShareButton';
21 | import RecommendButton from '../../molecules/RecommendButton';
22 | import RequiredFormText from '../../atoms/RequiredFormText';
23 |
24 | const InnerWrapper = styled.div`
25 | display: flex;
26 | flex-direction: row;
27 | flex-wrap: wrap;
28 | margin: 3em -1em -10em;
29 |
30 | column-gap: calc(15% - 2.3em);
31 | `;
32 |
33 | const Form = styled.form`
34 | flex: 3 0 22em;
35 | max-width: calc(100% - 2em);
36 | margin: 0 1em 10em;
37 | `;
38 |
39 | const StyledInput = styled(Input)`
40 | margin: 0 0 1em 0;
41 | `;
42 |
43 | const StyledButton = styled(Button)`
44 | margin: 1.5rem 0;
45 | `;
46 |
47 | const Agreement = styled.span`
48 | color: ${({ theme }) => theme.colors.paleText};
49 | `;
50 |
51 | const StyledLoadingIcon = styled(LoadingIcon)`
52 | margin: -0.5em -1.5em -0.5em 0.5em;
53 |
54 | font-size: 1.5em;
55 | `;
56 |
57 | const ErrorMessage = styled.p`
58 | color: red;
59 | `;
60 |
61 | const RightCol = styled.div`
62 | display: flex;
63 | flex: 1 0 16em;
64 | flex-direction: column;
65 | max-width: calc(100% - 2em);
66 | margin: 0 1em 10em;
67 | `;
68 |
69 | const HrCard = styled(Card)`
70 | position: relative;
71 |
72 | margin-bottom: 1em;
73 | padding-top: 4.5em;
74 | `;
75 |
76 | const HrPicture = styled(HrDecoratedPhoto)`
77 | position: absolute;
78 | top: -4.5em;
79 | left: 50%;
80 |
81 | transform: translateX(-50%);
82 | `;
83 |
84 | const HrCardText = styled.p``;
85 |
86 | const ContactLinksWrapper = styled.div`
87 | display: flex;
88 | flex-direction: column;
89 | align-items: flex-start;
90 | margin: 0.5em 0;
91 | `;
92 |
93 | const ContactLink = styled(Link)`
94 | display: flex;
95 | align-items: center;
96 | margin: 0.5em 0;
97 | `;
98 |
99 | const StyledIcon = styled(({ icon: Icon, ...props }) => )`
100 | margin-right: 0.5em;
101 |
102 | font-size: 1.5em;
103 | `;
104 |
105 | const RecommendationCard = styled(Card)`
106 | display: flex;
107 | flex-direction: row;
108 | align-items: center;
109 | justify-content: space-between;
110 | margin-bottom: 1em;
111 | `;
112 |
113 | const RecommendationCardDescriptionWrapper = styled.div`
114 | display: flex;
115 | flex-direction: column;
116 | align-items: flex-start;
117 | `;
118 |
119 | const RecommendationDescription = styled.p``;
120 |
121 | const formStatuses = {
122 | IDLE: 'IDLE',
123 | SUBMITTING: 'SUBMITTING',
124 | SUCCESS: 'SUCCESS',
125 | FAIL: 'FAIL',
126 | };
127 |
128 | const SuccessModal = lazy(() => import('./SuccessModal'));
129 |
130 | const Join = () => {
131 | const { t } = useL10n();
132 | const { shouldBeHydrated, handleClick } = useOnClickHydrate();
133 | const formRef = useRef();
134 | const [formStatus, setFormStatus] = useState(formStatuses.IDLE);
135 | useErrorBoundary(() => setFormStatus(formStatuses.FAIL));
136 |
137 | const handleSubmit = async (event) => {
138 | if (
139 | typeof window.FormData === 'undefined' ||
140 | typeof window.fetch === 'undefined' ||
141 | typeof window.URLSearchParams === 'undefined'
142 | ) {
143 | return;
144 | }
145 |
146 | event.preventDefault();
147 |
148 | setFormStatus(formStatuses.SUBMITTING);
149 |
150 | const formData = new FormData(formRef.current);
151 |
152 | try {
153 | const { ok } = await fetch('/', {
154 | method: 'POST',
155 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
156 | body: new URLSearchParams(formData).toString(),
157 | });
158 | setFormStatus(ok ? formStatuses.SUCCESS : formStatuses.FAIL);
159 | } catch (err) {
160 | setFormStatus(formStatuses.FAIL);
161 | }
162 | };
163 |
164 | const handleClose = () => {
165 | setFormStatus(formStatuses.IDLE);
166 | };
167 |
168 | return (
169 |
170 | {t('join.title')}
171 |
172 |
249 |
250 |
251 |
252 | {t('join.hrMessage')}
253 |
254 |
260 | {t('join.contacts.telegram')}
261 |
262 |
268 | {t('join.contacts.facebook')}
269 |
270 |
271 | {t('join.contacts.email')}
272 |
273 |
274 |
275 |
281 |
282 |
283 | {t('join.recommendationDescription')}
284 |
285 |
290 |
291 |
292 |
293 |
294 |
295 |
296 | );
297 | };
298 |
299 | export default Join;
300 |
--------------------------------------------------------------------------------
/src/locales/en.js:
--------------------------------------------------------------------------------
1 | const translations = {
2 | 'og:title':
3 | 'Vacancy – Fullstack Developer (Rails, TypeScript), Warsaw (relocation)',
4 | 'og:description':
5 | 'Looking for a big, technically complex project with beautiful architecture? Join our team in Warsaw! Relocation support for RU&CIS, remote work from EU only. MacBook, 38 vacation days, med.insurance.',
6 | hero: {
7 | subtitle: ({ Highlight }) => (
8 | <>
9 | to Warsaw {with relocation}
10 | >
11 | ),
12 | salary: '$3,500–5,000 net',
13 | actions: {
14 | apply: 'Apply now',
15 | },
16 | },
17 | shareModal: {
18 | triggerButtonText: 'Share',
19 | title: 'Share',
20 | copy: 'Copy',
21 | copySuccess: 'Copied!',
22 | copyFailed: 'Failed!',
23 | shareViaFacebook: 'Share via Facebook',
24 | shareViaTelegram: 'Share via Telegram',
25 | shareViaTwitter: 'Share via Twitter',
26 | shareViaLinkedIn: 'Share via LinkedIn',
27 | },
28 | recommendModal: {
29 | triggerButtonText: 'Recommend',
30 | title: 'Recommendation',
31 | form: {
32 | submit: 'Submit',
33 | agreement: ({ LinkComponent }) => (
34 | <>
35 | {'By clicking the "Submit" button, you agree with our '}
36 |
37 | privacy policy
38 |
39 | >
40 | ),
41 | aboutYou: 'About you',
42 | aboutRecommendee: 'About a person you recommend',
43 | yourName: 'Name',
44 | yourEmail: 'Email',
45 | yourPhone: 'Phone (optional)',
46 | recommendeeName: 'Name',
47 | recommendeeEmail: 'Email',
48 | recommendeePhone: 'Phone (optional)',
49 | recommendeeComment: 'Comment (optional)',
50 | error: 'Unexpected error occurred',
51 | successMessage:
52 | 'Thank you for your recommendation. We will contact you and a person you recommended in near future.',
53 | },
54 | },
55 | join: {
56 | title: 'Join the team',
57 | form: {
58 | name: 'Name',
59 | email: 'Email',
60 | link: 'Link to your CV or profile in social media',
61 | cv: 'Attach CV',
62 | submit: 'Submit',
63 | agreement: ({ LinkComponent }) => (
64 | <>
65 | {'By clicking the "Submit" button, you agree with our '}
66 |
67 | Privacy Policy
68 |
69 | >
70 | ),
71 | error: 'Unexpected error occurred',
72 | successTitle: 'Thank you for your application!',
73 | successDescription:
74 | 'I will reply to you shortly. Looking forward to meet you on the interview!',
75 | successOkButton: 'Okay',
76 | },
77 | contacts: {
78 | telegram: 'Telegram',
79 | facebook: 'Facebook',
80 | email: 'join@flatstack.com',
81 | },
82 | hrMessage: 'Irina replies all the emails and message from you',
83 | hrPhotoAlt: 'Hr photo',
84 | shareLinkIconAlt: 'share link',
85 | recommendationDescription:
86 | 'Get a $700 - $1,700 cash bonus for successful recommendation',
87 | },
88 | form: {
89 | requiredText: ({ AsteriskWrapper }) => (
90 | <>
91 | * - required fields
92 | >
93 | ),
94 | },
95 | };
96 |
97 | // we need some values only during build-time while generating HTML
98 | // and we don't need them in browser
99 | if (typeof window === 'undefined') {
100 | Object.assign(translations, {
101 | title:
102 | 'Vacancy – Fullstack Developer (Rails, TypeScript), Warsaw (relocation)',
103 | metaDescription:
104 | 'Software Engineer vacancy at Flatstack in Warsaw office. We help with relocation for RU & CIS. Remote work is possible from EU only. Technically complex project, string team. Join us!',
105 | metaKeywords:
106 | 'flatstack, software engineer, fullstack developer, ruby on rails, typescript, aws, job in Warsaw, vacancy, relocate',
107 | logoAlt: 'Flatstack logo',
108 | menu: {
109 | vacancy: 'Vacancy',
110 | company: 'Company',
111 | relocation: 'Relocation',
112 | feedbacks: 'Feedbacks',
113 | warsaw: 'Warsaw',
114 | },
115 | vacancy: {
116 | vacancyDescription:
117 | 'We are looking for a Software Engineer experienced with a large codebase, complex architecture, Infrastructure as Code.',
118 | aboutProduct: {
119 | description:
120 | 'The product we are working on is from the area of GRC (Governance, Risk Management, Compliance):',
121 | items: [
122 | 'optimizes risk assessment processes',
123 | 'automates regulatory compliance review processes (SOX, GDPR, etc.)',
124 | 'optimizes the processes of developing corporate policies',
125 | ],
126 | },
127 | role: {
128 | title: 'Role',
129 | items: [
130 | 'Work on technically complex project with beautiful architecture and modern approach in development',
131 | 'Apply open mind and experience for non-typical solutions',
132 | 'Work in big international team of fullstackers',
133 | 'Participate in new SPA development and maintenance of existing ones',
134 | 'Divide monolith application into micro services',
135 | 'Cover everything with testing: unit (Jest), integration (Cypress)',
136 | 'Use different tools (infrastructure management, analytics, CI, and many others)',
137 | 'Learn new technologies (e.g. Serverless, Terraform), share your knowledge and support colleagues in their growth (via code-review, sharing-sessions etc.)',
138 | ],
139 | },
140 | workOrganisation: {
141 | title: 'Work organisation',
142 | items: [
143 | 'To dive deeper into the project you’ll have access to a library of video training about the project',
144 | 'Well-built processes will allow you to focus on important',
145 | 'Team’s support will make your integration smooth',
146 | 'We do not micro-manage but expect results timely and with good quality',
147 | 'You’ll get a feedback on the work done. Great work is always a matter of pride!',
148 | ],
149 | },
150 | requirements: {
151 | title: 'Skills and experience',
152 | items: [
153 | 'Stack: Rails, PostgreSQL, React.js., REST, TDD, Docker, AWS, Terraform, TypeScript, Serverless',
154 | 'You may not have all knowledges from the list, but should be eager to learn',
155 | 'You create a high quality code and understand how it appears and operates in production',
156 | 'Your experience in building robust, scalable, and secure RESTful APIs will be an advantage',
157 | 'You have creative ideas, a collaborative attitude, and a strong work ethic',
158 | 'Your English covers the need to communicate about business needs, work organisation and technical details. Speaking, writing, reading are a must',
159 | ],
160 | },
161 | benefits: {
162 | title: 'Benefits',
163 | items: [
164 | '38 PTO (paid time off) per year. You can use even 3 weeks in a row to travel, or 1-3 days to fix some household needs',
165 | 'MacBook for every employee',
166 | 'Health insurance and year budget for private medical care',
167 | 'Budget for additional benefits of your choice (conferences, courses, you can even buy a bicycle if you need it)',
168 | 'Modern office',
169 | 'Ability to work remotely (from Europe)',
170 | 'Relocation leadership and support if you need it (see details below)',
171 | ],
172 | },
173 | apply: 'Apply now',
174 | },
175 | company: {
176 | title: 'About Flatstack',
177 | description:
178 | 'We have been building software since 2006 for clients in the USA, Europe and other countries. Our projects make the difference and help people in real life. We are over 100 talented programmers, designers, project managers and QA, working together, creating awesomeness.',
179 | flatstackImageAlt: 'Flatstack team',
180 | flatstackWarsawImageAlt: 'Warsaw office',
181 | flatstackNewOrleansImageAlt: 'New Orleans office',
182 | flatstackAntalyaImageAlt: 'Antalya office',
183 | },
184 | relocation: {
185 | title: 'Relocation',
186 | imageAlt: 'Our team',
187 | alert:
188 | "To work on our cool project you must live in Europe or Turkey. This is a client's requirement.",
189 | description:
190 | 'Would you like to live in Europe? We can help you with relocation to Poland (Warsaw).',
191 | listTitle: 'And we do it with care:',
192 | listItems: [
193 | 'We help with visa, all relevant docs and its translation',
194 | 'We help with accommodation and transfer',
195 | 'We make a residence card for you',
196 | 'We connect you with a lawyer who supports you with legal details',
197 | 'We go through the whole process together to make it seamless and comfortable for you!',
198 | ],
199 | },
200 | feedbacks: {
201 | title: 'Feedbacks',
202 | items: {
203 | dmitry: {
204 | title: 'Dima, Fullstack Developer',
205 | teamTime: '1 year in the team',
206 | feedback:
207 | 'Young modern team of professionals, friendly atmosphere, you always feel support from the colleagues. \n' +
208 | 'Flatstack is associated to me with modern IT company, with newest technologies and newest approaches in development, with very interesting ways of people and project management. If you work here, you always find interesting start-ups, trainings for your skills development and cool corporate events.',
209 | photoAlt: 'Photo with Dima',
210 | },
211 | alina: {
212 | title: 'Alina, Fullstack Developer',
213 | teamTime: '3+ years in the team',
214 | feedback:
215 | 'Funny, tight-knit team, interesting "not-boring" job, newest technologies are in use. Good office space, I value my colleagues and the atmosphere very much! ',
216 | photoAlt: 'Photo with Alina',
217 | },
218 | arkadii: {
219 | title: 'Arkadii, Fullstack Developer',
220 | teamTime: '7+ years in the team',
221 | feedback:
222 | 'Cool client and the project. Cool cut-of-the-edge technologies. Cool processes that really work. \n' +
223 | 'Here you can grow easily and gain diverse and varied experience. Comfortable and well organised relocation to Poland. Overall everything is well democratic.',
224 | photoAlt: 'Photo with Arkadii',
225 | },
226 | askar: {
227 | title: 'Askar, Fullstack Developer',
228 | teamTime: '9+ years in the team',
229 | feedback:
230 | "Good office, good colleagues, fruits every Wednesday, democratic work organisation. The decisions made are justified and reasoned criticism will always be heard. That's what I experienced here. \n" +
231 | 'Flatstack is associated to me with people who I was lucky to work with, with professionals in their field and with smart guys.',
232 | photoAlt: 'Photo with Askar',
233 | },
234 | },
235 | },
236 | warsaw: {
237 | title: 'Visit us in Warsaw',
238 | description:
239 | 'We are working in the modern office, where you will always find the right atmosphere for your tasks, whether you need a solitude place or need to switch your mind. Here we have parking places for bikes, cafe zone, swing and even sleepy room with climate control.',
240 | address: 'Warsaw, Konstruktorska 11, 02-673',
241 | imageAlt: 'Our Warsaw office',
242 | },
243 | });
244 | }
245 |
246 | export default {
247 | translations,
248 | };
249 |
--------------------------------------------------------------------------------