(this.container = container)}
77 | style={{
78 | height: height || layout !== 'text' ? height + 'px' : '100%',
79 | width: width ? width + 'px' : '100%',
80 | }}
81 | />
82 | )
83 | }
84 | }
85 |
86 | export default connect(state => ({
87 | app: state.app,
88 | }))(Ace)
89 |
--------------------------------------------------------------------------------
/packages/front/src/routes.js:
--------------------------------------------------------------------------------
1 | import {
2 | Admin,
3 | Article,
4 | Blog,
5 | Contact,
6 | Contributor,
7 | Doc,
8 | Home,
9 | Login,
10 | FacebookLogin,
11 | GithubLogin,
12 | Register,
13 | Settings,
14 | Tag,
15 | Tags,
16 | Changelog,
17 | } from './views'
18 |
19 | import {compile} from 'path-to-regexp'
20 | import {requireAuthentication} from './helpers'
21 | import {matchPath} from './utils'
22 |
23 | const routes = {
24 | home: {
25 | path: '/',
26 | exact: true,
27 | component: Home,
28 | },
29 | login: {
30 | path: '/login',
31 | exact: true,
32 | component: Login,
33 | },
34 | facebookLogin: {
35 | path: '/login/facebook',
36 | exact: true,
37 | component: FacebookLogin,
38 | },
39 | githubLogin: {
40 | path: '/login/github',
41 | exact: true,
42 | component: GithubLogin,
43 | },
44 | register: {
45 | path: '/register',
46 | exact: true,
47 | component: Register,
48 | },
49 | contact: {
50 | path: '/contact',
51 | component: Contact,
52 | },
53 | doc: {
54 | path: '/docs/:slug?',
55 | component: Doc,
56 | },
57 | changelog: {
58 | path: '/changelog',
59 | component: Changelog,
60 | },
61 | contributor: {
62 | path: '/blog/contributors/:slug',
63 | component: Contributor,
64 | },
65 | tag: {
66 | path: '/blog/tags/:tag',
67 | component: Tag,
68 | },
69 | tags: {
70 | path: '/blog/tags',
71 | component: Tags,
72 | },
73 | article: {
74 | path: '/blog/:slug',
75 | component: Article,
76 | },
77 | blog: {
78 | path: '/blog',
79 | component: Blog,
80 | },
81 | settings: {
82 | path: '/settings',
83 | component: requireAuthentication(Settings),
84 | },
85 | admin: {
86 | path: '/admin',
87 | component: requireAuthentication(Admin, 'ROLE_SUPER_ADMIN'),
88 | },
89 | }
90 |
91 | export const pathTo = (route, params = {}) => {
92 | if (!(route in routes)) {
93 | throw new Error(`There is no such view as ${route}`)
94 | }
95 |
96 | return compile(routes[route].path)(params)
97 | }
98 |
99 | export const matchRoute = pathname => {
100 | let keys = Object.keys(routes)
101 |
102 | let match = null
103 |
104 | let route = null
105 |
106 | for (let i = 0; i < keys.length; i++) {
107 | route = keys[i]
108 | match = matchPath(pathname, {
109 | path: routes[route].path,
110 | exact: routes[route].exact,
111 | })
112 |
113 | if (match) {
114 | return {
115 | route: route,
116 | match: match,
117 | }
118 | }
119 | }
120 |
121 | return null
122 | }
123 |
124 | export default routes
125 |
--------------------------------------------------------------------------------
/packages/front/src/views/doc/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {Link} from 'gatsby'
3 | import {MDXRenderer} from 'gatsby-plugin-mdx'
4 | import {faArrowLeft, faArrowRight} from '@fortawesome/free-solid-svg-icons'
5 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
6 | import Navigation from './navigation'
7 | // import { faEdit } from '@fortawesome/free-regular-svg-icons'
8 | import {pathTo} from '../../routes'
9 | import {MDXProvider} from '../../components'
10 |
11 | class Index extends Component {
12 | render() {
13 | const {doc, docNav, previous, next} = this.props
14 |
15 | return (
16 |
17 |
18 |
19 |
20 | {doc.frontmatter.title}
21 |
22 |
38 |
39 |
40 |
41 | {previous && (
42 |
49 | {' '}
50 | {previous.frontmatter.title}
51 |
52 | )}
53 |
54 |
55 | {next && (
56 |
63 | {next.frontmatter.title}{' '}
64 |
65 |
66 | )}
67 |
68 |
69 |
70 |
71 |
72 | )
73 | }
74 | }
75 |
76 | export default Index
77 |
--------------------------------------------------------------------------------
/packages/api/src/Controller/Api/ConfigController.php:
--------------------------------------------------------------------------------
1 | configService = $configService;
29 | }
30 |
31 | /**
32 | * @Route("/api/config/get-config", name="api_config_get", methods={"GET"})
33 | */
34 | public function getConfig(Request $request): JsonResponse
35 | {
36 | /** @var User $user */
37 | $user = $this->getUser();
38 | if (!$user instanceof UserInterface) {
39 | throw new AccessDeniedException('This config does not have access to this section.');
40 | }
41 |
42 | $config = $this->configService->findOne();
43 | if (!$config) {
44 | $config = new Config();
45 | }
46 |
47 | return new JsonResponse($this->configService->getJson($config));
48 | }
49 |
50 | /**
51 | * @Route("/api/config/set-config", name="api_config_set", methods={"PUT"})
52 | */
53 | public function setConfig(Request $request): JsonResponse
54 | {
55 | /** @var User $user */
56 | $user = $this->getUser();
57 | if (!$user instanceof UserInterface) {
58 | throw new AccessDeniedException('This config does not have access to this section.');
59 | }
60 |
61 | $config = $this->configService->findOne();
62 | if (!$config) {
63 | $config = new Config();
64 | }
65 |
66 | $form = $this->createForm(ConfigType::class, $config, [
67 | 'csrf_protection' => false,
68 | ]);
69 |
70 | $content = $request->getContent();
71 | if (!empty($content)) {
72 | $data = json_decode($content, true);
73 | $form->submit($data);
74 | } else {
75 | $form->handleRequest($request);
76 | }
77 |
78 | if ($form->isValid()) {
79 | $this->configService->save($config);
80 |
81 | return new JsonResponse($this->configService->getJson($config));
82 | }
83 |
84 | return new JsonResponse([
85 | 'message' => $form->getErrors(true)->current()->getMessage(),
86 | ], Response::HTTP_BAD_REQUEST);
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/packages/front/src/assets/styles/_sidebar.scss:
--------------------------------------------------------------------------------
1 | .sidebar {
2 | z-index: 10;
3 | overflow: auto;
4 | position: relative;
5 | -webkit-overflow-scrolling: touch;
6 | width: 100%;
7 | padding-right: $grid-gutter-width / 2;
8 | padding-left: $grid-gutter-width / 2;
9 |
10 | @include media-breakpoint-up(sm) {
11 | position: sticky;
12 | border-right: 1px solid var(--border-color);
13 | top: 0;
14 | height: calc(100vh - 61px);
15 | width: 180px;
16 | }
17 |
18 | @include media-breakpoint-up(md) {
19 | width: 240px;
20 | }
21 |
22 | @include media-breakpoint-up(lg) {
23 | width: 300px;
24 | }
25 | }
26 |
27 | .sidebar-search {
28 | position: relative; // To contain the Algolia search
29 | padding: 1rem 15px;
30 | margin-right: -15px;
31 | margin-left: -15px;
32 | border-bottom: 1px solid var(--border-color);
33 | }
34 |
35 | .sidebar-nav {
36 | margin-right: -$grid-gutter-width / 2;
37 | margin-left: -$grid-gutter-width / 2;
38 | padding: $spacer;
39 |
40 | ul {
41 | padding-left: 0;
42 | list-style: none;
43 | }
44 |
45 | @include media-breakpoint-up(sm) {
46 | max-height: calc(100vh - 61px - 74px);
47 | overflow-y: auto;
48 | }
49 | }
50 |
51 | .sidebar-items {
52 | li {
53 | padding: 0;
54 | position: relative;
55 |
56 | @include hover-focus {
57 | span {
58 | color: var(--text-color) !important;
59 |
60 | &.link {
61 | color: var(--primary-hover-color) !important;
62 |
63 | &:before {
64 | background-color: var(--primary-hover-color) !important;
65 | }
66 | }
67 | }
68 | }
69 |
70 | &.active {
71 | padding: 0 0 0 0.8rem;
72 |
73 | span.link {
74 | position: relative;
75 | color: var(--primary-color);
76 |
77 | &:before {
78 | content: " ";
79 | width: 8px;
80 | height: 8px;
81 | border-radius: 10px;
82 | position: absolute;
83 | left: -.8rem;
84 | top: 55%;
85 | margin-top: -5px;
86 | background-color: var(--primary-color);
87 | animation: scaleIn .7s forwards;
88 | }
89 | }
90 | }
91 |
92 | & > span {
93 | display: block;
94 | color: var(--secondary-color);
95 |
96 | @include hover-focus {
97 | color: var(--secondary-color);
98 | }
99 |
100 | &.link {
101 | display: block;
102 | padding: .25em 0;
103 | text-decoration: none;
104 |
105 | @include hover-focus {
106 | color: var(--primary-color);
107 | }
108 | }
109 | }
110 |
111 | a {
112 | overflow: hidden;
113 | position: absolute;
114 | top: 0;
115 | right: 0;
116 | bottom: 0;
117 | left: 0;
118 | text-indent: -1000px;
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/packages/front/src/reducers/user/actions.js:
--------------------------------------------------------------------------------
1 | import request from 'axios'
2 | import server from '../../utils/server'
3 | import uniq from 'lodash/uniq'
4 | import {COMMIT_UPDATE_SETTINGS} from './actions-types'
5 | import {commitLogoutUser} from '../auth/actions'
6 | import {commitAddLog} from '../logs/actions'
7 |
8 | export const fetchSettings = token => {
9 | return dispatch => {
10 | return request
11 | .get(`${server.getBaseUrl()}/api/user/get-settings`, {
12 | headers: {
13 | 'ServerlessStarter-Authorization': `Bearer ${token}`,
14 | },
15 | })
16 | .then(response => {
17 | dispatch(commitUpdateSettings(response.data))
18 | })
19 | .catch(error => {
20 | if (error.request.status === 401) {
21 | dispatch(commitLogoutUser())
22 | } else {
23 | throw error
24 | }
25 | })
26 | }
27 | }
28 | export const updateSettings = (item, token) => {
29 | return dispatch => {
30 | let data = {
31 | firstname: item.firstname,
32 | lastname: item.lastname,
33 | username: item.username,
34 | apiKey: item.apiKey,
35 | facebookId: item.facebookId,
36 | githubId: item.githubId,
37 | }
38 |
39 | return request
40 | .put(`${server.getBaseUrl()}/api/user/set-settings`, data, {
41 | headers: {
42 | 'ServerlessStarter-Authorization': `Bearer ${token}`,
43 | },
44 | })
45 | .then(response => {
46 | dispatch(commitUpdateSettings(data))
47 |
48 | return data
49 | })
50 | .catch(error => {
51 | if (error.request.status === 400) {
52 | dispatch(commitAddLog(error.response.data.message))
53 | } else if (error.request.status === 401) {
54 | dispatch(commitLogoutUser())
55 | } else {
56 | throw error
57 | }
58 | })
59 | }
60 | }
61 | export const commitUpdateSettings = user => {
62 | return dispatch => {
63 | dispatch({
64 | type: COMMIT_UPDATE_SETTINGS,
65 | user,
66 | })
67 | return Promise.resolve()
68 | }
69 | }
70 | export const isGranted = (user, attributes) => {
71 | if (!Array.isArray(attributes)) {
72 | attributes = [attributes]
73 | }
74 |
75 | let roles = ['ROLE_USER']
76 | for (let i = 0; i < user.roles.length; i++) {
77 | let role = user.roles[i]
78 | if (role === 'ROLE_SUPER_ADMIN') {
79 | roles.push('ROLE_USER')
80 | roles.push('ROLE_SUPER_ADMIN')
81 | } else {
82 | roles.push(role)
83 | }
84 | }
85 | roles = uniq(roles)
86 |
87 | for (let i = 0; i < attributes.length; i++) {
88 | let attribute = attributes[i]
89 | for (let j = 0; j < roles.length; j++) {
90 | let role = roles[j]
91 |
92 | if (attribute === role) {
93 | return true
94 | }
95 | }
96 | }
97 |
98 | return false
99 | }
100 |
--------------------------------------------------------------------------------
/packages/deploy/Makefile:
--------------------------------------------------------------------------------
1 | ##
2 | ##usage :
3 | ##-------
4 |
5 | COMPOSER = composer
6 |
7 | ensure-stage-variable:
8 | @if [ -z $(STAGE) ]; then \
9 | echo "You must prefix your command with the STAGE variable.\n"; \
10 | exit 1; \
11 | fi
12 | if [[ "$(STAGE)" != "prod" && "$(STAGE)" != "preprod" ]]; then \
13 | echo "You must prefix your command with the STAGE=[prod|preprod] variable.\n"; \
14 | exit 1; \
15 | fi
16 |
17 | clean:
18 | rm -rf build
19 |
20 | install: clean ## install
21 | cp .env.api.development ../api/.env.dev.local
22 | cp .env.front.development ../front/.env.development
23 |
24 | pack: ensure-stage-variable ## STAGE=[prod|preprod] pack
25 | $(MAKE) pack-api
26 | $(MAKE) pack-front
27 |
28 | pack-api: ensure-stage-variable
29 | rm -rf ./pack/$(STAGE)/api
30 | mkdir -p ./pack/$(STAGE)/api
31 | rsync -avr --exclude='vendor' --exclude='.env.*.local' --exclude='var' ../api ./pack/$(STAGE)
32 |
33 | rm -rf ./pack/$(STAGE)/api/.env.*
34 | cp .env.api.$(STAGE) ./pack/$(STAGE)/api/.env.local
35 |
36 | (cd ./pack/$(STAGE)/api && rm -rf ./config/jwt)
37 | (cd ./pack/$(STAGE)/api && mkdir -p ./config/jwt)
38 | (cd ./pack/$(STAGE)/api && openssl genrsa -out ./config/jwt/private.pem -aes256 -passout pass:serverless-starter 4096)
39 | (cd ./pack/$(STAGE)/api && openssl rsa -pubout -in ./config/jwt/private.pem -out ./config/jwt/public.pem -passin pass:serverless-starter)
40 |
41 | (cd ./pack/$(STAGE)/api && rm -rf var vendor)
42 | (cd ./pack/$(STAGE)/api && $(COMPOSER) install --prefer-dist --optimize-autoloader --no-dev)
43 |
44 | pack-front: ensure-stage-variable
45 | rm -rf ./pack/$(STAGE)/front
46 | mkdir -p ./pack/$(STAGE)/front
47 | rsync -avr --exclude='node_modules' --exclude='public' --exclude='.cache' --exclude='.env.*' ../front ./pack/$(STAGE)
48 |
49 | rm -rf ./pack/$(STAGE)/front/.env.*
50 | cp .env.front.$(STAGE) ./pack/$(STAGE)/front/.env.local
51 |
52 | (cd ./pack/$(STAGE)/front && rm -rf .cache dist public node_modules)
53 | (cd ./pack/$(STAGE)/front && npm install)
54 | (cd ./pack/$(STAGE)/front && DOCS_PATH=../../../../../docs GATSBY_ACTIVE_ENV=local ./node_modules/.bin/gatsby build)
55 |
56 | deploy: ensure-stage-variable ## STAGE=[prod|preprod] deploy
57 | $(MAKE) deploy-api
58 | $(MAKE) deploy-front
59 |
60 | deploy-api: ensure-stage-variable pack-api
61 | rm -rf .serverless
62 | serverless deploy -s $(STAGE)
63 |
64 | BUCKET_NAME=$$(serverless info -v -s $(STAGE) | grep ApiBucketOutput | sed s/ApiBucketOutput\:\ //g) && \
65 | aws s3 sync ./pack/$(STAGE)/api/public s3://$$BUCKET_NAME/ --exclude="*.php" --cache-control "public, max-age=300"
66 |
67 | deploy-front: ensure-stage-variable pack-front
68 | BUCKET_NAME=$$(serverless info -v -s $(STAGE) | grep FrontBucketOutput | sed s/FrontBucketOutput\:\ //g) && \
69 | aws s3 sync ./pack/$(STAGE)/front/public s3://$$BUCKET_NAME/ && \
70 | aws s3 cp "s3://$$BUCKET_NAME/index.html" "s3://$$BUCKET_NAME/index.html" --metadata-directive REPLACE --content-type "text/html" --cache-control max-age=0
71 |
72 | .SILENT: ensure-stage-variable
73 |
74 | # DEFAULT
75 | .DEFAULT_GOAL := help
76 | help:
77 | @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/'
78 |
79 | ##
80 |
--------------------------------------------------------------------------------
/packages/api/src/Controller/Api/ContactController.php:
--------------------------------------------------------------------------------
1 | contactService = $contactService;
44 | $this->twig = $twig;
45 | $this->mailer = $mailer;
46 | }
47 |
48 | private function send(string $templateName, array $context, string $fromEmail, string $toEmail): void
49 | {
50 | $template = $this->twig->load($templateName);
51 | $subject = $template->renderBlock('subject', $context);
52 | $textBody = $template->renderBlock('body_text', $context);
53 | $htmlBody = $template->renderBlock('body_html', $context);
54 |
55 | $message = (new Email())
56 | ->from($fromEmail)
57 | ->to($toEmail)
58 | ->subject($subject);
59 |
60 | if (!empty($htmlBody)) {
61 | $message
62 | ->html($htmlBody)
63 | ->text($textBody);
64 | } else {
65 | $message->html($textBody);
66 | }
67 |
68 | $this->mailer->send($message);
69 | }
70 |
71 | /**
72 | * @Route("/api/contact/create", name="api_contact_set", methods={"POST"})
73 | */
74 | public function create(Request $request): JsonResponse
75 | {
76 | $contact = new Contact();
77 |
78 | $form = $this->createForm(ContactType::class, $contact, [
79 | 'csrf_protection' => false,
80 | ]);
81 |
82 | $content = $request->getContent();
83 | if (!empty($content)) {
84 | $data = json_decode($content, true);
85 | $form->submit($data);
86 | } else {
87 | $form->handleRequest($request);
88 | }
89 |
90 | if ($form->isValid()) {
91 | $this->contactService->save($contact);
92 |
93 | $this->send('mails/contact.html.twig', [
94 | 'contact' => $contact,
95 | ], 'contact@serverless-starter.com', 'contact@serverless-starter.com');
96 |
97 | return new JsonResponse(true);
98 | }
99 |
100 | return new JsonResponse([
101 | 'message' => $form->getErrors(true)->current()->getMessage(),
102 | ], Response::HTTP_BAD_REQUEST);
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/packages/api/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "project",
3 | "license": "MIT",
4 | "require": {
5 | "php": ">=7.2.5",
6 | "ext-ctype": "*",
7 | "ext-iconv": "*",
8 | "api-platform/core": "^2.1",
9 | "bref/bref": "^0.5.29",
10 | "composer/package-versions-deprecated": "1.10.99.1",
11 | "doctrine/annotations": "^1.0",
12 | "doctrine/doctrine-bundle": "^1.6 || ^2.0",
13 | "doctrine/doctrine-migrations-bundle": "^3.0",
14 | "doctrine/orm": "^2.4.5",
15 | "gedmo/doctrine-extensions": "^2.4",
16 | "lexik/jwt-authentication-bundle": "^2.8",
17 | "nelmio/cors-bundle": "^1.5 || ^2.0",
18 | "phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0",
19 | "sensio/framework-extra-bundle": "^5.1",
20 | "symfony/asset": "5.1.*",
21 | "symfony/console": "5.1.*",
22 | "symfony/dotenv": "5.1.*",
23 | "symfony/expression-language": "5.1.*",
24 | "symfony/flex": "^1.3.1",
25 | "symfony/form": "5.1.*",
26 | "symfony/framework-bundle": "5.1.*",
27 | "symfony/http-client": "5.1.*",
28 | "symfony/intl": "5.1.*",
29 | "symfony/mailer": "5.1.*",
30 | "symfony/mime": "5.1.*",
31 | "symfony/monolog-bundle": "^3.1",
32 | "symfony/notifier": "5.1.*",
33 | "symfony/process": "5.1.*",
34 | "symfony/property-access": "5.1.*",
35 | "symfony/property-info": "5.1.*",
36 | "symfony/security-bundle": "5.1.*",
37 | "symfony/serializer": "5.1.*",
38 | "symfony/string": "5.1.*",
39 | "symfony/translation": "5.1.*",
40 | "symfony/twig-bundle": "5.1.*",
41 | "symfony/validator": "5.1.*",
42 | "symfony/web-link": "5.1.*",
43 | "symfony/yaml": "5.1.*",
44 | "twig/extra-bundle": "^2.12|^3.0",
45 | "twig/twig": "^2.12|^3.0"
46 | },
47 | "require-dev": {
48 | "doctrine/doctrine-fixtures-bundle": "^3.3",
49 | "symfony/browser-kit": "^5.1",
50 | "symfony/css-selector": "^5.1",
51 | "symfony/debug-bundle": "^5.1",
52 | "symfony/maker-bundle": "^1.0",
53 | "symfony/monolog-bundle": "^3.0",
54 | "symfony/phpunit-bridge": "^5.1",
55 | "symfony/stopwatch": "^5.1",
56 | "symfony/twig-bundle": "^5.1",
57 | "symfony/var-dumper": "^5.1",
58 | "symfony/web-profiler-bundle": "^5.1"
59 | },
60 | "config": {
61 | "optimize-autoloader": true,
62 | "preferred-install": {
63 | "*": "dist"
64 | },
65 | "sort-packages": true
66 | },
67 | "autoload": {
68 | "psr-4": {
69 | "App\\": "src/"
70 | }
71 | },
72 | "autoload-dev": {
73 | "psr-4": {
74 | "App\\Tests\\": "tests/"
75 | }
76 | },
77 | "replace": {
78 | "paragonie/random_compat": "2.*",
79 | "symfony/polyfill-ctype": "*",
80 | "symfony/polyfill-iconv": "*",
81 | "symfony/polyfill-php72": "*",
82 | "symfony/polyfill-php71": "*",
83 | "symfony/polyfill-php70": "*",
84 | "symfony/polyfill-php56": "*"
85 | },
86 | "scripts": {
87 | "auto-scripts": {
88 | "cache:clear": "symfony-cmd",
89 | "assets:install %PUBLIC_DIR%": "symfony-cmd"
90 | },
91 | "post-install-cmd": [
92 | "@auto-scripts"
93 | ],
94 | "post-update-cmd": [
95 | "@auto-scripts"
96 | ]
97 | },
98 | "conflict": {
99 | "symfony/symfony": "*"
100 | },
101 | "extra": {
102 | "symfony": {
103 | "allow-contrib": false,
104 | "require": "5.1.*"
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/packages/front/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@serverless-starter-com/serverless-starter-front",
3 | "version": "0.0.1",
4 | "description": "ServerlessStarter client",
5 | "author": {
6 | "name": "Mathieu Ledru",
7 | "email": "matyo91@gmail.com"
8 | },
9 | "bugs": {
10 | "url": "https://github.com/darkwood-fr/serverless-starter/issues"
11 | },
12 | "homepage": "https://github.com/darkwood-fr/serverless-starter/tree/master/library/serverless-starter-front#readme",
13 | "keywords": [
14 | "serverless-starter",
15 | "serverless-starter-front"
16 | ],
17 | "license": "MIT",
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/darkwood-fr/serverless-starter.git",
21 | "directory": "library/serverless-starter-front"
22 | },
23 | "publishConfig": {
24 | "access": "public"
25 | },
26 | "files": [
27 | "bin",
28 | "dist",
29 | "oclif.manifest.json",
30 | "public"
31 | ],
32 | "oclif": {
33 | "commands": "./dist/commands",
34 | "bin": "serverless-starter-front"
35 | },
36 | "bin": "./bin/serverless-starter-front",
37 | "scripts": {
38 | "prepack": "make build",
39 | "start": "run-script-os",
40 | "start:default": "cd bin && ./serverless-starter-front",
41 | "start:windows": "cd bin && serverless-starter-front"
42 | },
43 | "dependencies": {
44 | "@fortawesome/fontawesome-svg-core": "^1.2.30",
45 | "@fortawesome/free-brands-svg-icons": "^5.14.0",
46 | "@fortawesome/free-regular-svg-icons": "^5.14.0",
47 | "@fortawesome/free-solid-svg-icons": "^5.14.0",
48 | "@fortawesome/react-fontawesome": "^0.1.11",
49 | "@mdx-js/mdx": "^1.6.16",
50 | "@mdx-js/react": "^1.6.16",
51 | "@oclif/command": "^1.8.0",
52 | "@oclif/errors": "^1.3.3",
53 | "axios": "^0.20.0",
54 | "bootstrap": "^4.5.2",
55 | "brace": "^0.11.1",
56 | "express": "^4.17.1",
57 | "gatsby": "^2.24.50",
58 | "gatsby-image": "^2.4.16",
59 | "gatsby-plugin-feed": "^2.5.11",
60 | "gatsby-plugin-gtag": "^1.0.13",
61 | "gatsby-plugin-manifest": "^2.4.24",
62 | "gatsby-plugin-mdx": "^1.2.35",
63 | "gatsby-plugin-offline": "^3.2.24",
64 | "gatsby-plugin-react-helmet": "^3.3.10",
65 | "gatsby-plugin-sass": "^2.3.12",
66 | "gatsby-plugin-sharp": "^2.6.28",
67 | "gatsby-plugin-sitemap": "^2.4.12",
68 | "gatsby-remark-images": "^3.3.26",
69 | "gatsby-source-filesystem": "^2.3.25",
70 | "gatsby-transformer-sharp": "^2.5.13",
71 | "gatsby-transformer-yaml": "^2.4.10",
72 | "imports-loader": "^1.1.0",
73 | "jwt-decode": "^2.2.0",
74 | "lodash": "^4.17.20",
75 | "moment": "^2.27.0",
76 | "node-sass": "^4.14.1",
77 | "open": "^7.2.0",
78 | "path-to-regexp": "^6.1.0",
79 | "promise-events": "^0.1.8",
80 | "react": "^16.13.1",
81 | "react-dom": "^16.13.1",
82 | "react-helmet": "^6.1.0",
83 | "react-redux": "^7.2.1",
84 | "react-router-dom": "^5.2.0",
85 | "react-select": "^3.1.0",
86 | "redux": "^4.0.5",
87 | "redux-thunk": "^2.3.0",
88 | "vm-browserify": "^1.1.2"
89 | },
90 | "devDependencies": {
91 | "@oclif/dev-cli": "^1.22.2",
92 | "@types/convict": "^5.2.1",
93 | "@types/dotenv": "^8.2.0",
94 | "@types/express": "^4.17.7",
95 | "@types/node": "^14.6.0",
96 | "run-script-os": "^1.1.1",
97 | "ts-node": "^9.0.0",
98 | "typescript": "^4.0.2"
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/packages/front/src/views/blog/articleItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {pathTo} from '../../routes'
3 | import {Link, graphql} from 'gatsby'
4 | import Img from 'gatsby-image'
5 | import {faTag} from '@fortawesome/free-solid-svg-icons'
6 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
7 |
8 | const ArticleItem = ({article}) => (
9 |
10 |
14 | Read More
15 |
16 | {(article.frontmatter.cover.childImageSharp && (
17 |
![Cover]()
24 | )) ||
25 | (article.frontmatter.cover.extension === 'svg' && (
26 |

32 | ))}
33 |
34 |
{article.frontmatter.title}
35 |
{article.excerpt}
36 |
37 |
38 |
39 | Posted {article.frontmatter.date} by
40 |
48 |
53 | {article.frontmatter.author.name}
54 |
55 | -
56 | {article.timeToRead} min read
57 |
58 |
59 |
60 | {article.frontmatter.tags.map((tag, k) => (
61 |
66 | {tag}
67 |
68 | ))}
69 |
70 |
71 |
72 |
73 | )
74 |
75 | export const query = graphql`
76 | fragment ArticleItemFragment on Mdx {
77 | fields {
78 | slug
79 | }
80 | excerpt
81 | frontmatter {
82 | title
83 | author {
84 | fields {
85 | slug
86 | }
87 | name
88 | image {
89 | childImageSharp {
90 | fixed(width: 36, height: 36) {
91 | ...GatsbyImageSharpFixed
92 | }
93 | }
94 | }
95 | }
96 | cover {
97 | childImageSharp {
98 | fluid {
99 | ...GatsbyImageSharpFluid
100 | }
101 | }
102 | extension
103 | publicURL
104 | }
105 | tags
106 | date(formatString: "MMMM Do YYYY")
107 | }
108 | timeToRead
109 | }
110 | `
111 |
112 | export default ArticleItem
113 |
--------------------------------------------------------------------------------
/packages/front/src/views/doc/navigation.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {Link} from 'gatsby'
3 | import {faBars} from '@fortawesome/free-solid-svg-icons'
4 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
5 | import {pathTo} from '../../routes'
6 |
7 | class Navigation extends Component {
8 | state = {
9 | search: '',
10 | collapse: true,
11 | }
12 |
13 | onSearch = event => {
14 | this.setState({search: event.target.value})
15 | }
16 |
17 | onToggle = event => {
18 | this.setState({collapse: !this.state.collapse})
19 | }
20 |
21 | onSubmit = event => {
22 | event.preventDefault()
23 | }
24 |
25 | itemPathTo = item => {
26 | const slug = item.link.slice(6)
27 | return pathTo('doc', {slug: slug ? slug : null})
28 | }
29 |
30 | isActive = (item, slug) => {
31 | return item.link === `/docs${slug ? '/' + slug : ''}` ? 'active' : null
32 | }
33 |
34 | filterNav = (nav, search) => {
35 | return nav.reduce((value, section) => {
36 | const items = section.items.filter(item => {
37 | let words = item.title
38 | words = words.toLowerCase()
39 |
40 | return words.indexOf(search) !== -1
41 | })
42 |
43 | if (items.length > 0) {
44 | value.push({
45 | ...section,
46 | items: items,
47 | })
48 | }
49 |
50 | return value
51 | }, [])
52 | }
53 |
54 | render() {
55 | const {docNav, slug} = this.props
56 |
57 | return (
58 |
59 |
82 |
108 |
109 | )
110 | }
111 | }
112 |
113 | export default Navigation
114 |
--------------------------------------------------------------------------------
/packages/front/src/helpers/with-page.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {connect} from 'react-redux'
3 | import {commitSetPage} from '../reducers/app/actions'
4 | import Helmet from 'react-helmet'
5 |
6 | export default function withPage(Component, page, seo) {
7 | class PageHelper extends React.Component {
8 | componentDidMount() {
9 | this.props.dispatch(commitSetPage(page))
10 | }
11 |
12 | render() {
13 | const {env} = this.props
14 |
15 | return (
16 | <>
17 | {seo && (
18 |
19 | {seo.title}
20 |
24 | {seo.description && (
25 |
26 | )}
27 |
28 |
29 | {seo.description && (
30 |
31 | )}
32 |
36 | {seo.type && }
37 | {seo.title && }
38 | {seo.image &&
39 | typeof seo.image === 'string' && [
40 | ,
45 | ,
50 | ,
55 | ]}
56 | {seo.image &&
57 | typeof seo.image !== 'string' && [
58 | ,
63 | ,
68 | ,
73 | ]}
74 |
75 | {seo.title && }
76 | {seo.description && (
77 |
78 | )}
79 | {seo.image && typeof seo.image === 'string' && (
80 |
81 | )}
82 | {seo.image && typeof seo.image !== 'string' && (
83 |
87 | )}
88 |
89 | )}
90 |
91 | >
92 | )
93 | }
94 | }
95 |
96 | return connect(state => ({
97 | env: state.env,
98 | app: state.app,
99 | }))(PageHelper)
100 | }
101 |
--------------------------------------------------------------------------------
/packages/front/src/commands/start.ts:
--------------------------------------------------------------------------------
1 | import {Command, flags} from '@oclif/command';
2 | import * as open from 'open';
3 |
4 | import config from '../config';
5 | import app from "../server";
6 |
7 | let processExistCode = 0;
8 |
9 | export class Start extends Command {
10 | static description = 'Starts serverless-starter client.';
11 |
12 | static examples = [
13 | `$ serverless-starter-front start`,
14 | `$ serverless-starter-front start -o`,
15 | ];
16 |
17 | static flags = {
18 | help: flags.help({char: 'h'}),
19 | open: flags.boolean({
20 | char: 'o',
21 | description: 'opens the UI automatically in browser',
22 | }),
23 | };
24 |
25 | /**
26 | * Opens the UI in browser
27 | */
28 | static openBrowser() {
29 | const url = `http://localhost:${config.get('port')}`;
30 |
31 | open(url, {wait: true})
32 | .catch((error: Error) => {
33 | console.log(`\nWas not able to open URL in browser. Please open manually by visiting:\n${url}\n`);
34 | });
35 | }
36 |
37 | /**
38 | * Stoppes serverless-starter in a graceful way.
39 | */
40 | static async stopProcess() {
41 | console.log(`\nStopping serverless-starter...`);
42 |
43 | setTimeout(() => {
44 | // In case that something goes wrong with shutdown we
45 | // kill after max. 30 seconds no matter what
46 | process.exit(processExistCode);
47 | }, 30000);
48 |
49 | process.exit(processExistCode);
50 | }
51 |
52 | async run() {
53 | // Make sure that serverless-starter shuts down gracefully if possible
54 | process.on('SIGTERM', Start.stopProcess);
55 | process.on('SIGINT', Start.stopProcess);
56 |
57 | const {flags} = this.parse(Start);
58 |
59 | // Wrap that the process does not close but we can still use async
60 | (async () => {
61 | try {
62 | await app();
63 |
64 | const url = `http://localhost:${config.get('port')}`;
65 | this.log(`\nServerlessStarter client v${require('../../package.json').version} is ready on:\n${url}`);
66 |
67 | // Allow to open serverless-starter editor by pressing "o"
68 | if (Boolean(process.stdout.isTTY) && process.stdin.setRawMode) {
69 | process.stdin.setRawMode(true);
70 | process.stdin.resume();
71 | process.stdin.setEncoding('utf8');
72 | let inputText = '';
73 |
74 | if (flags.open) {
75 | Start.openBrowser();
76 | }
77 | this.log(`\nPress "o" to open in Browser.`);
78 | process.stdin.on("data", (key: string) => {
79 | if (key === 'o') {
80 | Start.openBrowser();
81 | inputText = '';
82 | } else if (key.charCodeAt(0) === 3) {
83 | // Ctrl + c got pressed
84 | Start.stopProcess();
85 | } else {
86 | // When anything else got pressed, record it and send it on enter into the child process
87 | if (key.charCodeAt(0) === 13) {
88 | // send to child process and print in terminal
89 | process.stdout.write('\n');
90 | inputText = '';
91 | } else {
92 | // record it and write into terminal
93 | inputText += key;
94 | process.stdout.write(key);
95 | }
96 | }
97 | });
98 | }
99 | } catch (error) {
100 | this.error(`There was an error: ${error.message}`);
101 |
102 | processExistCode = 1;
103 | // @ts-ignore
104 | process.emit('SIGINT');
105 | }
106 | })();
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/packages/front/src/assets/images/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/front/src/views/blog/article.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {Link} from 'gatsby'
3 | import {MDXRenderer} from 'gatsby-plugin-mdx'
4 | import Img from 'gatsby-image'
5 | import {faArrowLeft, faArrowRight} from '@fortawesome/free-solid-svg-icons'
6 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
7 | import {pathTo} from '../../routes'
8 | import {MDXProvider} from '../../components'
9 |
10 | class Article extends Component {
11 | render() {
12 | const {article, previous, next} = this.props
13 |
14 | return (
15 |
16 |
17 | {article.frontmatter.cover.childImageSharp && (
18 |
23 | )}
24 | {article.frontmatter.cover.extension === 'svg' && (
25 |
30 | )}
31 |
32 | credit {article.frontmatter.coverAuthor}
35 |
36 |
37 |
38 | {article.frontmatter.title}
39 |
40 |
41 |
42 |
43 | Posted {article.frontmatter.date} by
44 |
52 |
57 | {article.frontmatter.author.name}
58 |
59 | -
60 | {article.timeToRead} min read
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | {article.body}
69 |
70 |
71 |
72 |
73 |
74 |
75 | {previous && (
76 |
80 | {' '}
81 | {previous.frontmatter.title}
82 |
83 | )}
84 |
85 |
86 | {next && (
87 |
91 | {next.frontmatter.title}
92 |
93 | )}
94 |
95 |
96 |
97 | )
98 | }
99 | }
100 |
101 | export default Article
102 |
--------------------------------------------------------------------------------
/packages/front/src/components/select.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {default as ReactSelect} from 'react-select'
3 | import {default as ReactCreatableSelect} from 'react-select/creatable'
4 | import {connect} from 'react-redux'
5 |
6 | class Select extends Component {
7 | onChange = data => {
8 | const {onChange, multiple} = this.props
9 | if (onChange) {
10 | if (multiple === true) {
11 | onChange(
12 | (data || []).map(option => {
13 | return option.value
14 | })
15 | )
16 | } else {
17 | onChange(data.value)
18 | }
19 | }
20 | }
21 |
22 | render() {
23 | const {value, options, edit, multiple, app} = this.props
24 | const customStyles = {
25 | light: {
26 | menu: provided => ({
27 | ...provided,
28 | zIndex: 10,
29 | }),
30 | multiValue: styles => ({
31 | ...styles,
32 | backgroundColor: '#6c757d',
33 | }),
34 | multiValueLabel: styles => ({
35 | ...styles,
36 | color: '#FFFFFF',
37 | }),
38 | },
39 | dark: {
40 | menu: provided => ({
41 | ...provided,
42 | zIndex: 10,
43 | }),
44 | multiValue: styles => ({
45 | ...styles,
46 | backgroundColor: '#576068',
47 | }),
48 | multiValueLabel: styles => ({
49 | ...styles,
50 | color: '#ebf4f1',
51 | }),
52 | },
53 | sepia: {
54 | menu: provided => ({
55 | ...provided,
56 | zIndex: 10,
57 | }),
58 | multiValue: styles => ({
59 | ...styles,
60 | backgroundColor: '#876944',
61 | }),
62 | multiValueLabel: styles => ({
63 | ...styles,
64 | color: '#eadec2',
65 | }),
66 | },
67 | }
68 | const themes = {
69 | light: theme => theme,
70 | dark: theme => ({
71 | ...theme,
72 | colors: {
73 | ...theme.colors,
74 | primary25: '#0056b3',
75 | neutral0: '#0e2233',
76 | neutral5: '#0f2b3d',
77 | neutral10: '#576068',
78 | neutral20: '#002651',
79 | neutral70: '#002651',
80 | neutral80: '#495057',
81 | dangerLight: '#d59b8b',
82 | },
83 | }),
84 | sepia: theme => ({
85 | ...theme,
86 | colors: {
87 | ...theme.colors,
88 | primary25: '#0056b3',
89 | neutral0: '#eadec2',
90 | neutral5: '#0f2b3d',
91 | neutral10: '#876944',
92 | neutral20: '#ded0bf',
93 | neutral70: '#002651',
94 | neutral80: '#495057',
95 | dangerLight: '#d59b8b',
96 | },
97 | }),
98 | }
99 |
100 | let selectOptions = options || []
101 | let selectValue = undefined
102 |
103 | if (multiple === true) {
104 | selectValue = []
105 | if (value) {
106 | selectValue = value.map(data => {
107 | return {value: data, label: data}
108 | })
109 | }
110 | } else {
111 | selectValue = selectOptions.filter(option => {
112 | return option.value === value
113 | })
114 | }
115 |
116 | const DisplaySelect = edit === true ? ReactCreatableSelect : ReactSelect
117 |
118 | return (
119 |
127 | )
128 | }
129 | }
130 |
131 | export default connect(state => ({
132 | app: state.app,
133 | }))(Select)
134 |
--------------------------------------------------------------------------------
/packages/front/src/views/contact.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import {connect} from 'react-redux'
3 | import {contact} from '../reducers/contact/actions'
4 |
5 | class Contact extends Component {
6 | state = {
7 | email: null,
8 | emailError: false,
9 | message: null,
10 | messageError: false,
11 | sent: false,
12 | }
13 |
14 | onChangeEmail = event => {
15 | this.setState({email: event.target.value})
16 | }
17 |
18 | onChangeMessage = event => {
19 | this.setState({message: event.target.value})
20 | }
21 |
22 | onSubmit = e => {
23 | e.preventDefault()
24 |
25 | let valid = true
26 | if (!this.state.email) {
27 | valid = false
28 | this.setState({emailError: true})
29 | } else {
30 | this.setState({emailError: false})
31 | }
32 |
33 | if (!this.state.message) {
34 | valid = false
35 | this.setState({messageError: true})
36 | } else {
37 | this.setState({messageError: false})
38 | }
39 |
40 | if (valid) {
41 | this.props
42 | .dispatch(contact(this.state.email, this.state.message))
43 | .then(data => {
44 | if (data === true) {
45 | this.setState({sent: true})
46 | }
47 | })
48 | }
49 | }
50 |
51 | render() {
52 | const {email, emailError, message, messageError, sent} = this.state
53 |
54 | return (
55 |
56 |
57 |
58 |
Contact
59 | {sent && (
60 |
Your message has been sent
61 | )}
62 | {!sent && [
63 |
64 | You got a question about ServerlessStarter, write more here
65 |
66 | It will be a pleasure to respond
67 |
,
68 |
,
122 | ]}
123 |
124 |
125 |
126 | )
127 | }
128 | }
129 |
130 | export default connect(state => {
131 | return {
132 | env: state.env,
133 | }
134 | })(Contact)
135 |
--------------------------------------------------------------------------------