= ({ siteTitle = '' }) => (
11 |
12 |
13 |
14 |
15 | {siteTitle}
16 |
17 |
18 |
19 |
20 | );
21 |
22 | export default Header;
23 |
--------------------------------------------------------------------------------
/src/assets/styles/base/mixins/_spacing.scss:
--------------------------------------------------------------------------------
1 | @mixin spaceUnit($space-unit) {
2 | --space-unit: #{$space-unit};
3 | --space-xxxxs: calc(0.125 * #{$space-unit});
4 | --space-xxxs: calc(0.25 * #{$space-unit});
5 | --space-xxs: calc(0.375 * #{$space-unit});
6 | --space-xs: calc(0.5 * #{$space-unit});
7 | --space-sm: calc(0.75 * #{$space-unit});
8 | --space-md: calc(1.25 * #{$space-unit});
9 | --space-lg: calc(2 * #{$space-unit});
10 | --space-xl: calc(3.25 * #{$space-unit});
11 | --space-xxl: calc(5.25 * #{$space-unit});
12 | --space-xxxl: calc(8.5 * #{$space-unit});
13 | --space-xxxxl: calc(13.75 * #{$space-unit});
14 | }
15 |
--------------------------------------------------------------------------------
/src/assets/languages/whitelists/whitelist_en.json:
--------------------------------------------------------------------------------
1 | [
2 | "language.selector.option.english",
3 | "language.selector.option.spanish",
4 | "language.selector.option.portuguese",
5 | "language.selector.label",
6 | "layout.build-with",
7 | "photos.pagetitle",
8 | "photos.loadmore",
9 | "photos.loading",
10 | "photo.pagetitle",
11 | "photo.not-found",
12 | "photo.return-to-photos",
13 | "404.title",
14 | "404.message",
15 | "home.pagetitle",
16 | "home.greeting",
17 | "home.welcome",
18 | "home.gobuild",
19 | "home.link-page2",
20 | "home.link-photos",
21 | "page2.title",
22 | "page2.welcome",
23 | "page2.link"
24 | ]
--------------------------------------------------------------------------------
/src/store/helpers/definitions.ts:
--------------------------------------------------------------------------------
1 | import { AnyAction, Action, Reducer } from 'redux';
2 |
3 | export interface IAction extends Action {
4 | payload?: P;
5 | }
6 |
7 | export interface IReducerOptions {
8 | state: S;
9 | action: A;
10 | }
11 |
12 | export type TReducer = (state: S, action: A) => S;
13 |
14 | export interface IReducerMap {
15 | [key: string]: TReducer;
16 | }
17 |
18 | export type TReducerMapFunction = (reducerMap: IReducerMap) => Reducer;
19 |
--------------------------------------------------------------------------------
/src/store/state/photos/definitions.ts:
--------------------------------------------------------------------------------
1 | import { IAction } from 'store/helpers/definitions';
2 | import IPhotosModel from 'data/models/Photos';
3 | import { IPhotosParams } from 'data/api/photos/definitions';
4 |
5 | export enum PhotosActionTypes {
6 | FETCHING = 'photos/FETCHING',
7 | LOADED = 'photos/LOADED',
8 | FETCH_ERROR = 'photos/FETCH_ERROR',
9 | RESET = 'photos/RESET',
10 | }
11 |
12 | export interface IPhotosState {
13 | list: IPhotosModel[];
14 | hasMore: boolean;
15 | isFetching: boolean;
16 | }
17 |
18 | export interface IPhotosAction extends IAction {}
19 |
20 | export interface IPhotosLoadParams extends IPhotosParams {}
21 |
--------------------------------------------------------------------------------
/src/assets/styles/base/mixins/_aspect-ratio.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Mixin that allows to define a fixed aspect
3 |
4 | the default value will generate a square aspect
5 | */
6 | @mixin aspect-fixed($height: 100%) {
7 | position: relative;
8 | &:before {
9 | display: block;
10 | content: '';
11 | width: 100%;
12 | padding-top: $height;
13 | }
14 | }
15 |
16 | /*
17 | Mixin to set aspect ratio.
18 |
19 | 1, 1 - square
20 | 16, 9 - rectangle
21 |
22 | Eg: To set an aspect ratio of 16:9 include the mixin like;
23 | @include aspect-ratio(16, 9);
24 | */
25 | @mixin aspect-ratio($width, $height) {
26 | @include aspect-fixed(($height / $width) * 100%);
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/LanguageSelector/_styles.scss:
--------------------------------------------------------------------------------
1 | @import 'assets/styles/base/_mixins.scss';
2 |
3 | .languageselector {
4 | display: flex;
5 | flex-direction: row;
6 | align-items: center;
7 |
8 | &__label {
9 | margin-right: var(--languageselector-label-margin-right, var(--space-md));
10 | }
11 |
12 | &__select {
13 | color: var(--languageselector-select-color, initial);
14 | background: var(--languageselector-select-bg, transparent);
15 | border: 0;
16 |
17 | &container {
18 | border-radius: var(--languageselector-border-radius, var(--default-border-radius));
19 | border: 1px solid var(--languageselector-select-border, initial);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/helpers/fetch/readImageBlobAsDataURL.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * transform a binary image to its base64 image string
3 | *
4 | * @param {Blob} imageBlob - image binary
5 | * @return {string} image as a base64 string
6 | */
7 | export const readImageBlobAsDataURL = async (imageBlob: Blob): Promise => {
8 | const imageReader = new FileReader();
9 | return new Promise((resolve) => {
10 | const load = () => {
11 | imageReader.removeEventListener('load', load);
12 | resolve(imageReader.result as string);
13 | };
14 | imageReader.addEventListener('load', load, false);
15 | imageReader.readAsDataURL(imageBlob);
16 | });
17 | };
18 |
19 | export default readImageBlobAsDataURL;
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "*": ["*", "src/*"]
6 | },
7 | "module": "commonjs",
8 | "target": "esnext",
9 | "jsx": "preserve",
10 | "lib": ["dom", "es2015", "es2017"],
11 | "strict": true,
12 | "noEmit": true,
13 | "isolatedModules": true,
14 | "esModuleInterop": true,
15 | "skipLibCheck": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "removeComments": false,
19 | "preserveConstEnums": true,
20 | "resolveJsonModule": true,
21 | "typeRoots": ["node_modules/@types"]
22 | },
23 | "include": ["index.d.ts", "src/**/*"],
24 | "exclude": ["node_modules"]
25 | }
26 |
--------------------------------------------------------------------------------
/gatsby-browser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Implement Gatsby's Browser APIs in this file.
3 | *
4 | * See: https://www.gatsbyjs.org/docs/browser-apis/
5 | */
6 | import 'assets/styles/global.scss';
7 |
8 | import 'whatwg-fetch';
9 |
10 | import './src/helpers/console';
11 | import cssVariableTransform from './src/helpers/cssVariableTransform';
12 | import withWrapper from './gatsby-wrap-root-element';
13 |
14 | // eslint-disable-next-line import/prefer-default-export
15 | export const onClientEntry = () => {
16 | console.log('gatsby browser > onClientEntry > start');
17 | return cssVariableTransform().finally(() => console.log('gatsby browser > onClientEntry > finished'));
18 | };
19 |
20 | export const wrapRootElement = withWrapper;
21 |
--------------------------------------------------------------------------------
/src/components/ui/ImageCard/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import cn from 'clsx';
3 |
4 | import './_styles.scss';
5 |
6 | export enum EImageCard {
7 | SQUARE = 'square',
8 | RECTANGLE = 'rectangle',
9 | }
10 |
11 | interface IImageCardProps {
12 | url: string;
13 | type?: EImageCard;
14 | className?: string;
15 | }
16 |
17 | const ImageCard: React.FunctionComponent = ({ url, type = EImageCard.RECTANGLE, className }) => {
18 | const props = {
19 | className: cn(className, 'imagecard', `imagecard--${type}`),
20 | style: {
21 | backgroundImage: `url('${url}')`,
22 | },
23 | };
24 |
25 | return ;
26 | };
27 |
28 | export default ImageCard;
29 |
--------------------------------------------------------------------------------
/src/assets/styles/base/mixins/_colors.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Function to convert
3 | */
4 | @function color($color-name, $alpha: null) {
5 | @if ($alpha != null) {
6 | @return #{'rgba('}var(--#{$color-name}), $alpha#{')'};
7 | }
8 | @return #{'rgb('}var(--#{$color-name}) #{')'};
9 | }
10 |
11 | /*
12 | Mixin to convert sass vars to css variables
13 | */
14 | @mixin convertToCssColors($colors) {
15 | @each $name, $color in $colors {
16 | --#{$name}: #{red($color)}, #{green($color)}, #{blue($color)};
17 | }
18 | }
19 |
20 | /*
21 | Mixin to convert sass vars to css variables with the colors
22 | */
23 | @mixin convertToCssColorsValues($colors) {
24 | @each $name, $color in $colors {
25 | --#{$name}: #{$color};
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/store/state/language/reducers.ts:
--------------------------------------------------------------------------------
1 | import { browserLocale } from 'helpers/language';
2 | import createReducer from 'store/helpers/createReducer';
3 | import { IReducerMap } from 'store/helpers/definitions';
4 | import { LanguageActionTypes as Types, ILanguageAction, ILanguageState } from './definitions';
5 |
6 | export const initialState: ILanguageState = {
7 | locale: browserLocale,
8 | };
9 |
10 | const reducerMap: IReducerMap = {
11 | [Types.UPDATE]: (_, action) => {
12 | const { payload } = action;
13 | return payload || initialState;
14 | },
15 | [Types.RESET]: () => initialState,
16 | };
17 |
18 | const language = createReducer(initialState)(reducerMap);
19 |
20 | export default language;
21 |
--------------------------------------------------------------------------------
/src/store/state/language/persistTransform.ts:
--------------------------------------------------------------------------------
1 | import { createTransform } from 'redux-persist';
2 |
3 | import { ILanguageState } from './definitions';
4 | import { initialState } from './reducers';
5 |
6 | const serialize = (/* inboundState */ { locale }: ILanguageState) => {
7 | return { locale };
8 | };
9 |
10 | const unserialize = (outboundState: any) => {
11 | let loadedState = {};
12 | if ('locale' in outboundState) {
13 | loadedState = {
14 | locale: outboundState.locale,
15 | };
16 | }
17 | return { ...initialState, ...loadedState };
18 | };
19 |
20 | const name = 'language';
21 | const transform = createTransform(serialize, unserialize, { whitelist: [name] });
22 |
23 | export default {
24 | name,
25 | transform,
26 | };
27 |
--------------------------------------------------------------------------------
/scripts/load-dotenv-config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const dotenv = require('dotenv');
3 |
4 | const activeEnv = process.env.ACTIVE_ENV || process.env.NODE_ENV || 'development';
5 | const dotenvPath = path.resolve(__dirname, '..', `.env.${activeEnv}`);
6 |
7 | //www.gatsbyjs.org/docs/environment-variables/
8 | const dotenvConfig = dotenv.config({
9 | path: dotenvPath,
10 | });
11 |
12 | /*
13 | if (dotenvConfig.error) {
14 | console.log(`\n\n\ndotenv file not found: '${dotenvPath}'\n\n\n`);
15 | }
16 | */
17 |
18 | const { parsed: dotenvParsed } = dotenvConfig;
19 |
20 | if (dotenvParsed) {
21 | console.log(`\n\ndotenv file '${dotenvPath}' loaded\n${JSON.stringify(dotenvParsed, null, 2)}\n\n`);
22 | }
23 |
24 | module.exports = {
25 | activeEnv,
26 | dotenvParsed,
27 | };
28 |
--------------------------------------------------------------------------------
/src/components/Wrapper/Intl/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { IntlProvider } from 'react-intl';
4 | import { selectLanguage } from 'store/state/language/selectors';
5 | import { changeLanguage } from 'store/state/language/operations';
6 |
7 | export const IntlWrapper: React.FunctionComponent = ({ children }) => {
8 | const dispatch = useDispatch();
9 | const language = useSelector(selectLanguage);
10 | const { locale } = language;
11 |
12 | React.useEffect(() => {
13 | if (!language.messages) {
14 | dispatch(changeLanguage(locale));
15 | }
16 | }, []);
17 |
18 | return ;
19 | };
20 |
21 | export default IntlWrapper;
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | ISC License
3 |
4 | Copyright (c) 2021, Erko Bridee de Almeida Cabrera
5 |
6 | Permission to use, copy, modify, and/or distribute this software for any
7 | purpose with or without fee is hereby granted, provided that the above
8 | copyright notice and this permission notice appear in all copies.
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17 |
--------------------------------------------------------------------------------
/src/assets/styles/base/mixins/_breakpoints.scss:
--------------------------------------------------------------------------------
1 | $breakpoints: (
2 | xxs: 400px,
3 | xs: 512px,
4 | sm: 768px,
5 | md: 1024px,
6 | lg: 1280px,
7 | xl: 1440px,
8 | );
9 |
10 | @mixin breakpoint($breakpoint, $logic: false) {
11 | @if ($logic) {
12 | @media #{$logic} and (min-width: map-get($map: $breakpoints, $key: $breakpoint)) {
13 | @content;
14 | }
15 | } @else {
16 | @media (min-width: map-get($map: $breakpoints, $key: $breakpoint)) {
17 | @content;
18 | }
19 | }
20 | }
21 |
22 | @mixin breakpoint-max($breakpoint, $logic: false) {
23 | @if ($logic) {
24 | @media #{$logic} and (max-width: map-get($map: $breakpoints, $key: $breakpoint)) {
25 | @content;
26 | }
27 | } @else {
28 | @media (max-width: map-get($map: $breakpoints, $key: $breakpoint)) {
29 | @content;
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/Layout/Footer/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 |
4 | import FontIcon from 'components/ui/FontIcon';
5 | import LanguageSelector from 'components/LanguageSelector';
6 |
7 | import './_styles.scss';
8 |
9 | const Footer: React.FunctionComponent = () => (
10 |
22 | );
23 |
24 | export default Footer;
25 |
--------------------------------------------------------------------------------
/src/assets/styles/base/_z-index.scss:
--------------------------------------------------------------------------------
1 | /* z-index pyramid */
2 | $sun: -1;
3 | $mercury: 1;
4 | $venus: 10;
5 | $earth: 20;
6 | $mars: 30;
7 | $jupiter: 40;
8 | $saturn: 50;
9 | $uranus: 60;
10 | $neptune: 70;
11 | $solarsytem: 100;
12 | $milkwaygalaxy: 300;
13 | $andromedagalaxy: 500;
14 |
15 | $z-index-astros: (
16 | sun: -1,
17 | mercory: 1,
18 | venus: 10,
19 | earth: 20,
20 | mars: 30,
21 | jupiter: 40,
22 | saturn: 50,
23 | uranus: 60,
24 | neptune: 70,
25 | solarsystem: 100,
26 | milkwaygalaxy: 300,
27 | andromedagalaxy: 500,
28 | );
29 |
30 | @function zindex($name: mercory) {
31 | @return map-get($map: $z-index-astros, $key: $name);
32 | }
33 |
34 | @mixin zindexToCss() {
35 | @each $name, $value in $z-index-astros {
36 | --zindex-#{$name}: #{$value};
37 | }
38 | }
39 |
40 | :root {
41 | @include zindexToCss();
42 | }
43 |
--------------------------------------------------------------------------------
/src/helpers/fetch/buildCancelableRequest.ts:
--------------------------------------------------------------------------------
1 | import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
2 | import { IAPIResponse, ICancelableBaseResponse, IRequestOptions } from 'helpers/definitions';
3 | import buildRequest from './buildRequest';
4 |
5 | /**
6 | * Build and handle a cancelable fetch request
7 | *
8 | * @param {IRequestOptions} options
9 | *
10 | * @returns {Promise} promise
11 | */
12 | export function buildCancelableRequest(
13 | options: IRequestOptions
14 | ): ICancelableBaseResponse {
15 | const controller = new AbortController();
16 | const signal = controller.signal;
17 |
18 | return {
19 | request: () =>
20 | buildRequest({
21 | ...options,
22 | signal,
23 | }),
24 | controller,
25 | };
26 | }
27 |
--------------------------------------------------------------------------------
/src/domains/Photos/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { withPrefix } from 'gatsby';
3 | import { Router, RouteComponentProps } from '@reach/router';
4 | import { useDispatch } from 'react-redux';
5 |
6 | import { resetPhotos } from 'store/state/photos/operations';
7 |
8 | import PhotosList from './List';
9 | import PhotoView from './View';
10 |
11 | export interface IPhotos extends RouteComponentProps {}
12 |
13 | const Photos: React.FunctionComponent = () => {
14 | const dispatch = useDispatch();
15 |
16 | React.useEffect(() => {
17 | return () => {
18 | dispatch(resetPhotos());
19 | };
20 | }, []);
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default Photos;
31 |
--------------------------------------------------------------------------------
/src/components/ui/Tiles/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import cn from 'clsx';
3 |
4 | export { default as Tile } from './Tile';
5 |
6 | import './_styles.scss';
7 |
8 | export interface ITilesProps {
9 | className?: string;
10 | gutter?: string;
11 | children: React.ReactNode;
12 | }
13 |
14 | export const Tiles: React.FunctionComponent = ({ gutter, className, children }) => {
15 | const tilesRef = React.useRef(null);
16 |
17 | React.useEffect(() => {
18 | const { current: tiles } = tilesRef;
19 | if (tiles && !className && gutter) {
20 | tiles.style.setProperty('--tiles-gutter', gutter);
21 | }
22 | }, [gutter, className]);
23 |
24 | return (
25 |
26 | {children}
27 |
28 | );
29 | };
30 |
31 | export default Tiles;
32 |
--------------------------------------------------------------------------------
/src/assets/styles/custom/_default-theme.scss:
--------------------------------------------------------------------------------
1 | /*
2 | defined on:
3 | https://material.io/tools/color/#!/?view.left=0&view.right=1&primary.color=663399&secondary.color=F9A825
4 | */
5 | $md-colors: (
6 | color-primary: #6d2f9c,
7 | color-primary-light: #9e5dce,
8 | color-primary-dark: #3d006d,
9 | color-on-primary: #ffffff,
10 | color-on-primary-light: #000000,
11 | color-on-primary-dark: #ffffff,
12 | color-secundary: #ffae1f,
13 | color-secundary-light: #ffe058,
14 | color-secundary-dark: #c77f00,
15 | color-on-secundary: #000000,
16 | color-on-secundary-light: #000000,
17 | color-on-secundary-dark: #000000,
18 | color-error: #b00020,
19 | color-on-error: #ffffff,
20 | color-background: #ffffff,
21 | color-on-background: #000000,
22 | color-surface: #ffffff,
23 | color-on-surface: #000000,
24 | );
25 |
26 | :root {
27 | @include convertToCssColors($md-colors);
28 | }
29 |
--------------------------------------------------------------------------------
/src/store/helpers/createReducer.ts:
--------------------------------------------------------------------------------
1 | import { AnyAction, Action } from 'redux';
2 | import { isFunction } from 'helpers/check';
3 | import { TReducerMapFunction } from './definitions';
4 |
5 | /**
6 | * Define a reducer with a initial value and a map of reducer functions by the action type,
7 | * when there is no type matching the reducer will return the state value
8 | *
9 | * @param {S} initialState
10 | * @return {TReducerMapFunction} reducer function
11 | */
12 | export function createReducer(initialState: S): TReducerMapFunction {
13 | return (reducerMap) => (state = initialState, action) => {
14 | const { type = '__[[__not__defined__]]__' } = action || {};
15 | const reducer = reducerMap[type];
16 | return isFunction(reducer) ? reducer(state, action) : state;
17 | };
18 | }
19 |
20 | export default createReducer;
21 |
--------------------------------------------------------------------------------
/src/components/ui/Tiles/Tile/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import cn from 'clsx';
3 | import { TFunction, IDictionary } from 'helpers/definitions';
4 |
5 | interface ITileProps {
6 | key?: string | number;
7 | colSpan?: number;
8 | rowSpan?: number;
9 | className?: string;
10 | onClick?: TFunction;
11 | }
12 |
13 | const Tile: React.FunctionComponent = ({ children, colSpan, rowSpan, className, ...otherProps }) => {
14 | const style: IDictionary = {};
15 | if (colSpan) {
16 | style.gridColumn = `span ${colSpan} / auto`;
17 | }
18 | if (rowSpan) {
19 | style.gridRow = `span ${rowSpan} / auto`;
20 | }
21 | return (
22 |
29 | {children}
30 |
31 | );
32 | };
33 |
34 | export default Tile;
35 |
--------------------------------------------------------------------------------
/gatsby-node.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | const { dotenvParsed } = require('./scripts/load-dotenv-config');
5 |
6 | exports.onCreateWebpackConfig = ({ actions }) => {
7 | // make sure to have the process.env configs from the .env file available on the /src/* files
8 | const plugins = [];
9 | if (dotenvParsed) {
10 | const webpackDefinePluginConfig = {};
11 | Object.keys(dotenvParsed).forEach((key) => {
12 | webpackDefinePluginConfig[`process.env.${key}`] = JSON.stringify(process.env[key]);
13 | });
14 | plugins.push(new webpack.DefinePlugin(webpackDefinePluginConfig));
15 | }
16 |
17 | actions.setWebpackConfig({
18 | resolve: {
19 | // https://www.gatsbyjs.org/docs/add-custom-webpack-config/#absolute-imports
20 | modules: [path.resolve(__dirname, 'src'), 'node_modules'],
21 | },
22 | plugins: plugins,
23 | });
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/GatsbyAstronoutImage/index.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | https://www.gatsbyjs.com/docs/reference/release-notes/image-migration-guide/
3 | https://www.gatsbyjs.com/plugins/gatsby-plugin-image/
4 | https://www.gatsbyjs.com/docs/reference/built-in-components/gatsby-plugin-image/
5 | */
6 | import React from 'react';
7 | import { useStaticQuery, graphql } from 'gatsby';
8 | import { GatsbyImage } from 'gatsby-plugin-image';
9 |
10 | const GatsbyAstronoutImage = () => {
11 | const data = useStaticQuery(graphql`
12 | query {
13 | file(relativePath: { eq: "gatsby-astronaut.png" }) {
14 | childImageSharp {
15 | gatsbyImageData(layout: CONSTRAINED, placeholder: BLURRED, formats: [AUTO, WEBP, AVIF])
16 | }
17 | }
18 | }
19 | `);
20 |
21 | return ;
22 | };
23 |
24 | export default GatsbyAstronoutImage;
25 |
--------------------------------------------------------------------------------
/src/helpers/cssVariableTransform.ts:
--------------------------------------------------------------------------------
1 | import cssVars from 'css-vars-ponyfill';
2 | import { isBrowser, hasCssVariablesSupport } from './check';
3 |
4 | export const cssVariableTransform = () =>
5 | new Promise((resolve, reject) => {
6 | if (!isBrowser || hasCssVariablesSupport) {
7 | console.log('cssVariableTransform ', { isBrowser, hasCssVariablesSupport });
8 | return resolve('done');
9 | }
10 |
11 | console.log('cssVariableTransform start');
12 | cssVars({
13 | onWarning: (message) => console.log('cssVariableTransform warning: ', message),
14 | onError: (message, elm, xhr, url) => {
15 | console.log('cssVariableTransform error: ', { message, elm, xhr, url });
16 | reject();
17 | },
18 | onComplete: () => {
19 | console.log('cssVariableTransform completed');
20 | resolve('done');
21 | },
22 | });
23 | });
24 |
25 | export default cssVariableTransform;
26 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | // Cache the returned value forever and don't call this function again.
3 | api.cache(true);
4 |
5 | const isTranslationsFlow = Boolean(process.env.TRANSLATIONS);
6 | if (isTranslationsFlow) {
7 | return {
8 | presets: ['@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript'],
9 | plugins: [
10 | '@babel/plugin-syntax-dynamic-import',
11 | [
12 | 'react-intl',
13 | {
14 | messagesDir: '.build/i18nExtractedMessages/',
15 | },
16 | ],
17 | ],
18 | };
19 | }
20 |
21 | return {
22 | presets: ['babel-preset-gatsby'],
23 | };
24 |
25 | /*
26 | const presets = [];
27 | const plugins = [
28 | [
29 | 'react-intl',
30 | {
31 | messagesDir: '.i18nExtractedMessages/',
32 | },
33 | ],
34 | ];
35 |
36 | console.log('HERE');
37 |
38 | return {
39 | presets,
40 | plugins,
41 | };
42 | */
43 | };
44 |
--------------------------------------------------------------------------------
/src/assets/styles/_skin.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --body-bg: rgb(var(--color-background));
3 | --body-color: rgba(var(--color-on-background), 0.8);
4 |
5 | --header-bg: rgb(var(--color-primary));
6 | --header-color: rgb(var(--color-on-primary));
7 |
8 | --footer-bg: rgb(var(--color-primary));
9 | --footer-color: rgb(var(--color-on-primary));
10 |
11 | --link: rgba(var(--color-primary), var(--text-opacity-active));
12 | --link-hover: rgba(var(--color-primary), var(--text-opacity-inactive));
13 | --link-active: rgba(var(--color-primary), var(--text-opacity-active));
14 | --link-visited: rgba(var(--color-primary), var(--text-opacity-visited));
15 |
16 | --link-inverted: rgba(var(--color-on-primary), var(--text-opacity-inactive));
17 | --link-inverted-hover: rgba(var(--color-on-primary), var(--text-opacity-active));
18 | --link-inverted-active: rgba(var(--color-on-primary), var(--text-opacity-active));
19 | --link-inverted-visited: rgba(var(--color-on-primary), var(--text-opacity-visited));
20 | }
21 |
--------------------------------------------------------------------------------
/src/data/models/Photos/index.ts:
--------------------------------------------------------------------------------
1 | import { IBaseModel } from 'helpers/definitions';
2 | import { IPhotoEntity } from 'data/schemas/JsonPlaceholder';
3 |
4 | export interface IPhotoProps extends IPhotoEntity {}
5 |
6 | export interface IPhotoModel extends IBaseModel {
7 | readonly id: number;
8 | readonly title: string;
9 | readonly url: string;
10 | }
11 |
12 | export default class PhotoModel implements IPhotoModel {
13 | private props: IPhotoProps;
14 |
15 | constructor(props: IPhotoProps) {
16 | this.props = props;
17 | }
18 |
19 | get __innerprops__() {
20 | return this.props;
21 | }
22 |
23 | get id() {
24 | return this.props.id;
25 | }
26 |
27 | get title() {
28 | return this.props.title;
29 | }
30 |
31 | get url() {
32 | return `//lorempixel.com/600/400/?_=${this.props.id}}`;
33 | }
34 |
35 | toJSON() {
36 | return {
37 | id: this.id,
38 | title: this.title,
39 | url: this.url,
40 | };
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/store/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | manages all the application states through the react-redux
3 | */
4 | import { applyMiddleware, compose, createStore, combineReducers } from 'redux';
5 | import { persistReducer, persistStore } from 'redux-persist';
6 | import thunkMiddleware from 'redux-thunk';
7 | import { isBrowser } from 'helpers/check';
8 | import * as reducers from './state';
9 | import persistConfig from './state/persistConfig';
10 |
11 | const rootReducer = combineReducers(reducers);
12 | const persistedReducer = persistReducer(persistConfig, rootReducer);
13 |
14 | const composeEnhancers = (isBrowser ? (window as any) : {}).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
15 | const middlewares = [thunkMiddleware];
16 |
17 | export const store = createStore(persistedReducer, composeEnhancers(applyMiddleware(...middlewares)));
18 | export const persistor = persistStore(store);
19 |
20 | export const dispatch = store.dispatch;
21 | export const getState = () => store.getState();
22 |
23 | export default store;
24 |
--------------------------------------------------------------------------------
/scripts/proxied-http-server.js:
--------------------------------------------------------------------------------
1 | /*
2 | based on:
3 | https://github.com/gatsbyjs/gatsby/blob/2.13.30/packages/gatsby/src/commands/develop.js#L237
4 | */
5 | const express = require('express');
6 | const request = require('request');
7 |
8 | const gatsbyConfig = require('../gatsby-config');
9 | const { proxy } = gatsbyConfig;
10 |
11 | const app = express();
12 |
13 | app.use(express.static('public'));
14 |
15 | if (proxy) {
16 | const { prefix, url } = proxy;
17 | app.use(`${prefix}/*`, (req, res) => {
18 | const proxiedUrl = url + req.originalUrl;
19 | req
20 | .pipe(
21 | request(proxiedUrl).on(`error`, (err) => {
22 | const message = `Error when trying to proxy request "${req.originalUrl}" to "${proxiedUrl}"`;
23 |
24 | report.error(message, err);
25 | res.status(500).end();
26 | })
27 | )
28 | .pipe(res);
29 | });
30 | }
31 |
32 | const port = process.env.PORT || 9000;
33 | app.listen(port, () => console.log(`server started, listening on port ${port}`));
34 |
--------------------------------------------------------------------------------
/src/assets/styles/base/mixins/_typography.scss:
--------------------------------------------------------------------------------
1 | // edit font rendering -> tip: use for light text on dark backgrounds
2 | @mixin fontSmooth {
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | }
6 |
7 | //----------------------------------------------------------------------------//
8 |
9 | // https://css-tricks.com/snippets/css/fluid-typography/
10 | @mixin fluid-type($min-vw, $max-vw, $min-font-size, $max-font-size) {
11 | $u1: unit($min-vw);
12 | $u2: unit($max-vw);
13 | $u3: unit($min-font-size);
14 | $u4: unit($max-font-size);
15 |
16 | @if $u1 == $u2 and $u1 == $u3 and $u1 == $u4 {
17 | & {
18 | --text-base-size: #{$min-font-size};
19 | @media screen and (min-width: $min-vw) {
20 | --text-base-size: calc(
21 | #{$min-font-size} + #{strip-unit($max-font-size - $min-font-size)} *
22 | ((100vw - #{$min-vw}) / #{strip-unit($max-vw - $min-vw)})
23 | );
24 | }
25 | @media screen and (min-width: $max-vw) {
26 | --text-base-size: #{$max-font-size};
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/Layout/Header/_styles.scss:
--------------------------------------------------------------------------------
1 | @import 'assets/styles/base/_mixins.scss';
2 |
3 | .layoutheader {
4 | a,
5 | .link {
6 | --link: var(--link-inverted-active);
7 | --link-hover: var(--link-inverted-hover);
8 | --link-active: var(--link-inverted-active);
9 | --link-visited: var(--link-inverted-active);
10 | }
11 |
12 | &,
13 | &__content {
14 | display: flex;
15 | flex-direction: row;
16 | width: 100%;
17 | }
18 |
19 | justify-content: center;
20 | background-color: var(--header-bg);
21 | color: var(--header-color);
22 |
23 | &__content {
24 | justify-content: space-between;
25 | max-width: var(--max-app-width);
26 | padding: var(--space-md);
27 |
28 | @include breakpoint-max(sm) {
29 | padding: var(--space-lg) var(--space-md);
30 | }
31 | }
32 |
33 | &__title {
34 | font-size: var(--text-xxl);
35 | font-weight: var(--font-weight-bold);
36 |
37 | @include breakpoint-max(sm) {
38 | font-size: var(--text-xl);
39 | }
40 |
41 | @include breakpoint-max(xs) {
42 | font-size: var(--text-lg);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/assets/languages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "404.message": "You just hit a route that doesn't exist... the sadness.",
3 | "404.title": "404: Not found",
4 | "home.gobuild": "Now go build something great.",
5 | "home.greeting": "Hi people",
6 | "home.link-page2": "Go to page 2",
7 | "home.link-photos": "Go to photos",
8 | "home.pagetitle": "Home",
9 | "home.welcome": "Welcome to your new Gatsby site.",
10 | "language.selector.label": "Select another available language",
11 | "language.selector.option.english": "English",
12 | "language.selector.option.portuguese": "Portuguese",
13 | "language.selector.option.spanish": "Spanish",
14 | "layout.build-with": "Built with",
15 | "page2.link": "Go back to the homepage",
16 | "page2.title": "Hi from the second page",
17 | "page2.welcome": "Welcome to page 2",
18 | "photo.not-found": "Photo: {id} - Not Found",
19 | "photo.pagetitle": "Photo {id}",
20 | "photo.return-to-photos": "Return to photos",
21 | "photos.loading": "Loading...",
22 | "photos.loadmore": "Load more",
23 | "photos.pagetitle": "Photos"
24 | }
--------------------------------------------------------------------------------
/src/components/Layout/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Layout component that queries for data
3 | * with Gatsby's useStaticQuery component
4 | *
5 | * See: https://www.gatsbyjs.org/docs/use-static-query/
6 | */
7 |
8 | import * as React from 'react';
9 | import { useStaticQuery, graphql } from 'gatsby';
10 |
11 | import Header from './Header';
12 | import Footer from './Footer';
13 |
14 | import './_styles.scss';
15 |
16 | interface ILayout {
17 | children: React.ReactNode;
18 | displayFooter?: boolean;
19 | }
20 |
21 | const Layout: React.FunctionComponent = ({ children, displayFooter = true }) => {
22 | const data = useStaticQuery(graphql`
23 | query SiteTitleQuery {
24 | site {
25 | siteMetadata {
26 | title
27 | }
28 | }
29 | }
30 | `);
31 |
32 | return (
33 |
34 |
35 |
{children}
36 | {displayFooter &&
}
37 |
38 | );
39 | };
40 |
41 | export default Layout;
42 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 |
4 | import Layout from 'components/Layout';
5 | import SEO from 'components/SEO';
6 |
7 | const NotFoundPage: React.FunctionComponent = () => {
8 | const [isInitialized, setIsInitialized] = React.useState(false);
9 | React.useEffect(() => {
10 | setIsInitialized(true);
11 | });
12 | return (
13 |
14 | {isInitialized && (
15 | <>
16 |
17 | {(text) => }
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 | >
29 | )}
30 |
31 | );
32 | };
33 |
34 | export default NotFoundPage;
35 |
--------------------------------------------------------------------------------
/src/store/state/photos/operations.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch } from 'redux';
2 |
3 | import { updatePaginationAttributes, hasMoreData } from 'helpers/fetch/pagination';
4 |
5 | import * as API from 'data/api/photos';
6 |
7 | import { photosFetching, photosFetchError, photosLoaded, photosReset } from './actions';
8 | import { IPhotosAction, IPhotosLoadParams } from './definitions';
9 |
10 | export const loadPhotos = (params?: IPhotosLoadParams) => async (dispatch: Dispatch) => {
11 | const { offset, page, countPerPage } = updatePaginationAttributes(params || { countPerPage: 50 });
12 | dispatch(photosFetching());
13 | try {
14 | const responseObject = await API.loadPhotos({ offset, page, countPerPage });
15 | const { data: list, totalCount = 0 } = responseObject;
16 | const { length } = list;
17 | const hasMore = hasMoreData({ length, totalCount });
18 | dispatch(photosLoaded({ list, hasMore, isFetching: false }));
19 | } catch (e) {
20 | dispatch(photosFetchError());
21 | console.log(e);
22 | }
23 | };
24 |
25 | export const resetPhotos = photosReset;
26 |
--------------------------------------------------------------------------------
/src/assets/languages/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "404.message": "Acabas de llegar a una ruta que no existe... que tristeza.",
3 | "404.title": "404: No encontrado",
4 | "home.gobuild": "Ahora va a construir algo grande.",
5 | "home.greeting": "Hola gente",
6 | "home.link-page2": "Ir a la página 2",
7 | "home.link-photos": "Ir a las fotos",
8 | "home.pagetitle": "Casa",
9 | "home.welcome": "Bienvenido a tu nuevo sitio de Gatsby.",
10 | "language.selector.label": "Seleccione otro idioma disponible",
11 | "language.selector.option.english": "Inglés",
12 | "language.selector.option.portuguese": "Portugués",
13 | "language.selector.option.spanish": "Español",
14 | "layout.build-with": "Construido con",
15 | "page2.link": "Volver a la página de inicio",
16 | "page2.title": "Hola desde la segunda pagina",
17 | "page2.welcome": "Bienvenido a la página 2",
18 | "photo.not-found": "Foto: {id} - No encontrado",
19 | "photo.pagetitle": "Foto {id}",
20 | "photo.return-to-photos": "Volver a las fotos",
21 | "photos.loading": "Cargando...",
22 | "photos.loadmore": "Carga más",
23 | "photos.pagetitle": "Fotos"
24 | }
--------------------------------------------------------------------------------
/src/assets/languages/pt.json:
--------------------------------------------------------------------------------
1 | {
2 | "404.message": "Você acabou de acertar uma rota que não existe... lamento.",
3 | "404.title": "404: Não encontrado...",
4 | "home.gobuild": "Agora vá construir algo ótimo.",
5 | "home.greeting": "Olá pessoal",
6 | "home.link-page2": "Vá para a página 2",
7 | "home.link-photos": "Ir para fotos",
8 | "home.pagetitle": "Home",
9 | "home.welcome": "Bem-vindo ao seu novo site do Gatsby.",
10 | "language.selector.label": "Selecione outro idioma disponível",
11 | "language.selector.option.english": "Inglês",
12 | "language.selector.option.portuguese": "Português",
13 | "language.selector.option.spanish": "Espanhol",
14 | "layout.build-with": "Construído com",
15 | "page2.link": "Volte para a página inicial",
16 | "page2.title": "Oi da segunda página",
17 | "page2.welcome": "Bem vindo a pagina 2",
18 | "photo.not-found": "Foto: {id} - Não encontrada",
19 | "photo.pagetitle": "Foto {id}",
20 | "photo.return-to-photos": "Retornar para fotos",
21 | "photos.loading": "Carregando...",
22 | "photos.loadmore": "Carregar mais",
23 | "photos.pagetitle": "Fotos"
24 | }
--------------------------------------------------------------------------------
/src/components/Layout/Footer/_styles.scss:
--------------------------------------------------------------------------------
1 | @import 'assets/styles/base/_mixins.scss';
2 |
3 | .layoutfooter {
4 | background-color: var(--footer-bg);
5 | color: var(--footer-color);
6 |
7 | a,
8 | .link {
9 | --link: var(--link-inverted);
10 | --link-hover: var(--link-inverted-hover);
11 | --link-active: var(--link-inverted-active);
12 | --link-visited: var(--link-inverted-visited);
13 | }
14 | &,
15 | &__content {
16 | display: flex;
17 | flex-direction: row;
18 | width: 100%;
19 | }
20 |
21 | justify-content: center;
22 |
23 | &__content {
24 | justify-content: space-between;
25 | max-width: var(--max-app-width);
26 | padding: var(--space-md);
27 |
28 | @include breakpoint-max(sm) {
29 | flex-direction: column;
30 | justify-content: flex-start;
31 | font-size: var(--text-sm);
32 | padding: var(--space-lg) var(--space-md);
33 |
34 | & > *:not(:first-child) {
35 | margin-top: var(--space-sm);
36 | }
37 | }
38 | }
39 |
40 | &__select {
41 | --languageselector-select-color: var(--footer-color);
42 | --languageselector-select-border: var(--footer-color);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/store/state/photos/reducers.ts:
--------------------------------------------------------------------------------
1 | import createReducer from 'store/helpers/createReducer';
2 | import { IReducerMap } from 'store/helpers/definitions';
3 | import { arrayMerge } from 'helpers/arrays';
4 | import { PhotosActionTypes as Types, IPhotosAction, IPhotosState } from './definitions';
5 |
6 | const initialState: IPhotosState = {
7 | list: [],
8 | hasMore: true,
9 | isFetching: false,
10 | };
11 |
12 | const reducerMap: IReducerMap = {
13 | [Types.LOADED]: ({ list: previousList }, action) => {
14 | const { payload: { list, hasMore, isFetching } = initialState } = action;
15 | return {
16 | list: arrayMerge(previousList, list, 'id'),
17 | hasMore,
18 | isFetching,
19 | };
20 | },
21 | [Types.FETCHING]: (state) => {
22 | return {
23 | ...state,
24 | isFetching: true,
25 | };
26 | },
27 | [Types.FETCH_ERROR]: (state) => {
28 | return {
29 | ...state,
30 | isFetching: false,
31 | };
32 | },
33 | [Types.RESET]: () => initialState,
34 | };
35 |
36 | const language = createReducer(initialState)(reducerMap);
37 |
38 | export default language;
39 |
--------------------------------------------------------------------------------
/src/html.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default function HTML(props) {
5 | return (
6 |
7 |
8 |
9 |
10 |
14 | {props.headComponents}
15 |
16 |
17 | {props.preBodyComponents}
18 |
21 |
22 | {props.postBodyComponents}
23 |
24 |
25 | );
26 | }
27 |
28 | HTML.propTypes = {
29 | htmlAttributes: PropTypes.object,
30 | headComponents: PropTypes.array,
31 | bodyAttributes: PropTypes.object,
32 | preBodyComponents: PropTypes.array,
33 | body: PropTypes.string,
34 | postBodyComponents: PropTypes.array,
35 | };
36 |
--------------------------------------------------------------------------------
/src/helpers/console.ts:
--------------------------------------------------------------------------------
1 | //----------------------------------------------------------------------------//
2 | // Opting out of the "[React Intl] Missing message" in developement #465
3 | // https://github.com/formatjs/react-intl/issues/465#issuecomment-369566628
4 |
5 | // Disable missing translation message as translations will be added later.
6 | // We can add a toggle for this later when we have most translations.
7 |
8 | type TConsoleError = (message?: any, ...optionalParams: any[]) => void;
9 |
10 | // eslint-disable-next-line
11 | const consoleError: TConsoleError = console.error.bind(console);
12 | // eslint-disable-next-line
13 | console.error = (message: any, ...args: any[]) => {
14 | const strMessage = String(message);
15 | if (
16 | strMessage.startsWith('Error: [@formatjs/intl Error MISSING_TRANSLATION] Missing message:') ||
17 | strMessage.startsWith('Error: [@formatjs/intl Error MISSING_TRANSLATION] Missing locale data for locale:')
18 | ) {
19 | return;
20 | }
21 | consoleError(message, ...args);
22 | };
23 |
24 | //----------------------------------------------------------------------------//
25 |
26 | // redefine the console object
27 | if (process.env.NODE_ENV === 'production') {
28 | console.log = () => undefined;
29 | }
30 |
31 | export default console;
32 |
--------------------------------------------------------------------------------
/src/pages/page-2.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Link } from 'gatsby';
3 | import { FormattedMessage } from 'react-intl';
4 |
5 | import Layout from 'components/Layout';
6 | import SEO from 'components/SEO';
7 |
8 | const toJSON = (value: any) => JSON.stringify(value, null, 2);
9 |
10 | const SecondPage: React.FunctionComponent = () => {
11 | const [message, setMessage] = React.useState('');
12 |
13 | const loadMessage = async () => {
14 | const newMessage = await import(
15 | /* webpackMode: "lazy", webpackChunkName: "hello-world" */ `assets/hello-world.json`
16 | );
17 | setMessage(toJSON(newMessage.default));
18 | };
19 |
20 | React.useEffect(() => {
21 | loadMessage();
22 | }, []);
23 |
24 | return (
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {message}
35 |
36 |
37 |
38 |
39 |
40 | );
41 | };
42 |
43 | export default SecondPage;
44 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | // Specifies the ESLint parser
4 | parser: '@typescript-eslint/parser',
5 | extends: ['plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
6 | settings: {
7 | react: {
8 | version: 'detect',
9 | },
10 | },
11 | env: {
12 | browser: true,
13 | node: true,
14 | es6: true,
15 | },
16 | plugins: ['@typescript-eslint', 'react', 'prettier'],
17 | parserOptions: {
18 | ecmaFeatures: {
19 | jsx: true,
20 | },
21 | // Allows for the parsing of modern ECMAScript features
22 | ecmaVersion: 2018,
23 | // Allows for the use of imports
24 | sourceType: 'module',
25 | },
26 | rules: {
27 | // Disable prop-types as we use TypeScript for type checking
28 | 'react/prop-types': 'off',
29 | 'prettier/prettier': 'error',
30 | '@typescript-eslint/explicit-function-return-type': 'off',
31 | '@typescript-eslint/explicit-module-boundary-types': 'off',
32 | '@typescript-eslint/interface-name-prefix': 'off',
33 | '@typescript-eslint/no-explicit-any': 'off',
34 | '@typescript-eslint/ban-ts-ignore': 'off',
35 | '@typescript-eslint/no-empty-interface': 'off',
36 | // needed for NextJS's jsx without react import
37 | 'react/react-in-jsx-scope': 'off',
38 | },
39 | globals: { React: 'writable' },
40 | };
41 |
--------------------------------------------------------------------------------
/scripts/translation-runner.js:
--------------------------------------------------------------------------------
1 | const { readFileSync } = require('fs');
2 | const {
3 | default: manageTranslations,
4 | readMessageFiles,
5 | getDefaultMessages,
6 | } = require('react-intl-translations-manager');
7 |
8 | const messagesDirectory = '.build/i18nExtractedMessages';
9 | const defaultLanguage = 'en';
10 |
11 | const readFile = (filename) => {
12 | try {
13 | return readFileSync(filename);
14 | } catch (error) {
15 | return undefined;
16 | }
17 | };
18 |
19 | manageTranslations({
20 | messagesDirectory: messagesDirectory,
21 | translationsDirectory: 'src/assets/languages/',
22 | whitelistsDirectory: 'src/assets/languages/whitelists/',
23 | languages: [defaultLanguage, 'es', 'pt'],
24 | overrideCoreMethods: {
25 | // based on:
26 | // https://github.com/GertjanReynaert/react-intl-translations-manager/issues/76#issuecomment-283186721
27 | provideWhitelistFile: (langResults) => {
28 | if (langResults.lang === defaultLanguage) {
29 | const messageFiles = readMessageFiles(messagesDirectory);
30 | const messages = getDefaultMessages(messageFiles).messages;
31 | return Object.keys(messages);
32 | }
33 | const jsonFile = readFile(langResults.whitelistFilepath);
34 | return jsonFile ? JSON.parse(jsonFile) : undefined;
35 | },
36 | },
37 | });
38 |
39 | console.log('\ntranslation runner done\n\n');
40 |
--------------------------------------------------------------------------------
/src/assets/styles/base/_typography.scss:
--------------------------------------------------------------------------------
1 | $min_width: 500px;
2 | $max_width: 1280px;
3 | $min_font: 14px;
4 | $max_font: 16px;
5 |
6 | $text_opacity_active: 1;
7 | $text_opacity_visited: 0.7;
8 | $text_opacity_inactive: 0.6;
9 | $text_opacity_disable: 0.38;
10 | $text_opacity_error: 1;
11 |
12 | :root {
13 | @include fluid-type($min_width, $max_width, $min_font, $max_font);
14 |
15 | // https://websemantics.uk/articles/font-size-conversion/
16 | --text-xxxl: #{pixels2rem(42px)};
17 | --text-xxl: #{pixels2rem(32px)};
18 | --text-xl: #{pixels2rem(24px)};
19 | --text-lg: #{pixels2rem(18px)};
20 | --text-md: #{pixels2rem(16px)};
21 | --text-sm: #{pixels2rem(14px)};
22 | --text-xs: #{pixels2rem(12px)};
23 |
24 | --text-opacity-active: #{$text_opacity_active};
25 | --text-opacity-visited: #{$text_opacity_visited};
26 | --text-opacity-inactive: #{$text-opacity-inactive};
27 | --text-opacity-disable: #{$text-opacity-disable};
28 | --text-opacity-error: #{$text-opacity-error};
29 |
30 | --heading-line-height: 1.2;
31 |
32 | --font-family: -apple-system, BlinkMacSystemFont, Roboto, 'Segoe UI', Helvetica, Arial, sans-serif,
33 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
34 |
35 | --font-family-code: 'SFMono-Regular', Consolas, 'Roboto Mono', 'Droid Sans Mono', 'Liberation Mono', Menlo, Courier,
36 | monospace;
37 |
38 | --font-weight-normal: 400;
39 | --font-weight-bold: 700;
40 | }
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # .vscode
2 | .build
3 | static/generated
4 | src/assets/styles/generated
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 | yarn-debug.log*
11 | yarn-error.log*
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (http://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # Typescript v1 declaration files
45 | typings/
46 |
47 | # Optional npm cache directory
48 | .npm
49 |
50 | # Optional eslint cache
51 | .eslintcache
52 |
53 | # Optional REPL history
54 | .node_repl_history
55 |
56 | # Output of 'npm pack'
57 | *.tgz
58 |
59 | # dotenv environment variables file
60 | .env
61 | .env.*
62 | !.env.example
63 |
64 | # gatsby files
65 | public
66 | .cache
67 |
68 | .husky
69 |
70 | # Mac files
71 | .DS_Store
72 |
73 | # Yarn
74 | yarn-error.log
75 | .pnp/
76 | .pnp.js
77 | # Yarn Integrity file
78 | .yarn-integrity
79 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | moduleNameMapper: {
4 | '.+\\.(css|styl|less|sass|scss)$': `identity-obj-proxy`,
5 | '.+\\.svg$': `/__mocks__/svgr-mock.js`,
6 | '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': `/__mocks__/file-mock.js`,
7 | },
8 | moduleDirectories: ['node_modules', 'src'],
9 | testPathIgnorePatterns: ['node_modules', '.cache', 'public'],
10 | transformIgnorePatterns: ['node_modules/(?!(gatsby)/)'],
11 | globals: {
12 | __PATH_PREFIX__: ``,
13 | },
14 | testURL: `http://localhost`,
15 | setupFilesAfterEnv: [`/jest-helpers/jest.setup.js`],
16 | setupFiles: [`/jest-helpers/loadershim.js`],
17 | cacheDirectory: `/.build/jest-temp/`,
18 | globals: {
19 | // we must specify a custom tsconfig for tests because we need the typescript transform
20 | // to transform jsx into js rather than leaving it jsx such as the next build requires. you
21 | // can see this setting in tsconfig.jest.json -> "jsx": "react"
22 | 'ts-jest': {
23 | tsConfig: `/jest-helpers/tsconfig.jest.json`,
24 | },
25 | },
26 | collectCoverage: true,
27 | collectCoverageFrom: [
28 | 'src/**/*.{ts,tsx}',
29 | '!**/node_modules/**',
30 | '!**/vendor/**',
31 | '!src/helpers/{cssVariableTransform,language,console}.ts',
32 | ],
33 | coverageReporters: ['lcov', 'text-summary'],
34 | reporters: ['default', 'jest-junit'],
35 | };
36 |
--------------------------------------------------------------------------------
/src/data/api/photos/index.ts:
--------------------------------------------------------------------------------
1 | import buildRequest from 'helpers/fetch/buildRequest';
2 | import { isNumber } from 'helpers/check';
3 | import { IAPIResponse, APIResponseStatus, DEFAULT_PAGE_SIZE } from 'helpers/definitions';
4 | import PhotoModel from 'data/models/Photos';
5 | import { IPhotosParams, IPhotosAPIResponse, IPhotosResponse } from './definitions';
6 |
7 | export const loadPhotos = async (parameters?: IPhotosParams): Promise => {
8 | let apiParams;
9 | if (parameters) {
10 | const { offset, page = 0, countPerPage = DEFAULT_PAGE_SIZE } = parameters;
11 | const _start = isNumber(offset) ? offset : page * countPerPage;
12 | apiParams = { _start, _limit: countPerPage };
13 | }
14 |
15 | try {
16 | const responseObject = await buildRequest({
17 | host: 'jsonplaceholder.typicode.com',
18 | urlPath: 'photos',
19 | parameters: apiParams,
20 | });
21 |
22 | const { statusCode, headers, body } = responseObject;
23 | const xTotalCount = headers.get('x-total-count') || 0;
24 | const totalCount = Number(xTotalCount);
25 | const data = body.map((entity) => new PhotoModel(entity));
26 | return {
27 | status: APIResponseStatus.SUCCESS,
28 | statusCode,
29 | data,
30 | totalCount,
31 | };
32 | } catch (e) {
33 | const errorObject: IAPIResponse = e;
34 | const { statusCode, body } = errorObject;
35 | throw {
36 | status: APIResponseStatus.ERROR,
37 | statusCode,
38 | body,
39 | };
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import ReactDOMServer from 'react-dom/server';
3 |
4 | import { Link } from 'gatsby';
5 | import { FormattedMessage } from 'react-intl';
6 |
7 | import Layout from 'components/Layout';
8 | import GatsbyAstronoutImage from 'components/GatsbyAstronoutImage';
9 | import SEO from 'components/SEO';
10 |
11 | const IndexPage: React.FunctionComponent = () => {
12 | return (
13 |
14 |
15 | {(text) => {
16 | const title = ReactDOMServer.renderToString(<>{text}>);
17 | return ;
18 | }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {' '}
36 | {' | '}
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default IndexPage;
46 |
--------------------------------------------------------------------------------
/src/helpers/__tests__/console.ts:
--------------------------------------------------------------------------------
1 | describe('console', () => {
2 | describe('error', () => {
3 | beforeAll(() => {
4 | require('../console');
5 | global.console = { ...global.console, error: jest.fn() };
6 | });
7 |
8 | it('should be call', () => {
9 | const message = 'hello';
10 | console.error(message);
11 | expect(console.error).toBeCalledWith(message);
12 |
13 | console.error(message, 1, 2, 3);
14 | expect(console.error).toBeCalledWith(message, 1, 2, 3);
15 | });
16 |
17 | it('should not be called', () => {
18 | expect(console.error('[React Intl] Missing message: ???')).toBeUndefined();
19 | expect(console.error('[React Intl] Missing locale data for locale: ???')).toBeUndefined();
20 | });
21 | });
22 |
23 | describe('log', () => {
24 | const OLD_ENV = process.env;
25 |
26 | beforeEach(() => {
27 | jest.resetModules(); // this is important - it clears the cache
28 | process.env = { ...OLD_ENV };
29 | delete process.env.NODE_ENV;
30 | });
31 |
32 | afterEach(() => {
33 | process.env = OLD_ENV;
34 | });
35 |
36 | it('should not be called', () => {
37 | process.env.NODE_ENV = 'production';
38 | require('../console');
39 | expect(console.log()).toBeUndefined();
40 | });
41 |
42 | it('should call', () => {
43 | require('../console');
44 | global.console = { ...global.console, log: jest.fn() };
45 | const message = 'hello';
46 | console.log(message);
47 | expect(console.log).toBeCalledWith(message);
48 | });
49 | });
50 | });
51 |
52 | export {};
53 |
--------------------------------------------------------------------------------
/src/domains/Photos/View/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import ReactDOMServer from 'react-dom/server';
3 |
4 | import { FormattedMessage } from 'react-intl';
5 | import { Link } from 'gatsby';
6 | import { RouteComponentProps /*, Link*/ } from '@reach/router';
7 | import { useSelector } from 'react-redux';
8 |
9 | import { selectPhotoById } from 'store/state/photos/selectors';
10 |
11 | import SEO from 'components/SEO';
12 |
13 | import PhotoCard from 'domains/Photos/Card';
14 |
15 | import './_styles.scss';
16 |
17 | interface IPhotoViewParams {
18 | id: string;
19 | }
20 |
21 | export interface IPhotoView extends RouteComponentProps {}
22 |
23 | const PhotoView: React.FunctionComponent = ({ id }) => {
24 | const photo = useSelector(selectPhotoById(Number(id)));
25 | const info = photo ? photo.id : 'Not Found';
26 | return (
27 | <>
28 |
29 | {(text) => {
30 | const title = ReactDOMServer.renderToString(<>{text}>);
31 | return ;
32 | }}
33 |
34 |
35 | {photo ? (
36 |
37 | ) : (
38 |
39 | )}
40 |
41 |
42 |
43 |
44 | >
45 | );
46 | };
47 |
48 | export default PhotoView;
49 |
--------------------------------------------------------------------------------
/scripts/font-generator.js:
--------------------------------------------------------------------------------
1 | const args = require('yargs').options({
2 | fontname: { alias: 'f', describe: 'define the fontname', demandOption: true },
3 | cssfontsurl: {
4 | alias: 'c',
5 | describe: "define the css fonts urls defined on the output styles file (default value: 'assets/icons/generated/')",
6 | demandOption: false,
7 | },
8 | }).argv;
9 |
10 | const pkg = require('../package.json');
11 | const pathPrefix = require('./get-path-prefix')(pkg);
12 |
13 | const [source] = args._;
14 | const { fontname, cssfontsurl = '/generated/fonticons/' } = args;
15 |
16 | if (!source || !fontname) {
17 | console.log(`Usage: ${args.$0} [source] -f [fontname]`);
18 | process.exit();
19 | }
20 |
21 | const webfontsGenerator = require('webfonts-generator');
22 | const fs = require('fs');
23 | const path = require('path');
24 |
25 | const svgRegExp = /\.svg$/;
26 | const destinationFolder = path.resolve(source);
27 | const iconsFolder = path.resolve('.build/icons/', 'compressed');
28 | const destinationCss = path.join(destinationFolder, `${fontname}.scss`);
29 |
30 | fs.readdir(iconsFolder, (err, files) => {
31 | files = files.filter((file) => file.match(svgRegExp));
32 | files = files.map((file) => path.join(iconsFolder, file));
33 |
34 | if (err) {
35 | console.log(err);
36 | process.exit();
37 | }
38 |
39 | webfontsGenerator(
40 | {
41 | files,
42 | dest: destinationFolder,
43 | fontName: fontname,
44 | cssDest: destinationCss,
45 | cssFontsUrl: `${pathPrefix}${cssfontsurl}`,
46 | },
47 | function (err) {
48 | if (err) {
49 | console.log('Font generation failed.', err);
50 | } else {
51 | console.log('Font generation succesful.');
52 | }
53 | }
54 | );
55 | });
56 |
--------------------------------------------------------------------------------
/src/components/LanguageSelector/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import cn from 'clsx';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
5 | import { changeLanguage } from 'store/state/language/operations';
6 | import { selectLocale } from 'store/state/language/selectors';
7 |
8 | import './_styles.scss';
9 |
10 | const translations = defineMessages({
11 | en: { id: 'language.selector.option.english', defaultMessage: 'English' },
12 | es: { id: 'language.selector.option.spanish', defaultMessage: 'Spanish' },
13 | pt: { id: 'language.selector.option.portuguese', defaultMessage: 'Portuguese' },
14 | });
15 |
16 | interface ILanguageSelectorProps {
17 | className?: string;
18 | label?: React.ReactNode;
19 | }
20 |
21 | const LanguageSelector: React.FunctionComponent = ({
22 | className,
23 | label = ,
24 | }) => {
25 | const intl = useIntl();
26 | const dispatch = useDispatch();
27 | const locale = useSelector(selectLocale);
28 |
29 | const handleChange = (event: React.ChangeEvent) => {
30 | dispatch(changeLanguage(event.target.value));
31 | };
32 |
33 | return (
34 |
35 | {label &&
{label}
}
36 |
37 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default LanguageSelector;
52 |
--------------------------------------------------------------------------------
/src/components/SEO/index.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * SEO component that queries for data with
3 | * Gatsby's useStaticQuery React hook
4 | *
5 | * See: https://www.gatsbyjs.org/docs/use-static-query/
6 | */
7 |
8 | import * as React from 'react';
9 | import Helmet from 'react-helmet';
10 | import { useStaticQuery, graphql } from 'gatsby';
11 |
12 | type TMetaProps = JSX.IntrinsicElements['meta'];
13 |
14 | interface ISEO {
15 | title: string;
16 | description?: string;
17 | lang?: string;
18 | meta?: TMetaProps[];
19 | }
20 |
21 | const SEO: React.FunctionComponent = ({ description, lang, meta = [], title }) => {
22 | const { site } = useStaticQuery(
23 | graphql`
24 | query {
25 | site {
26 | siteMetadata {
27 | title
28 | description
29 | author
30 | }
31 | }
32 | }
33 | `
34 | );
35 |
36 | const metaDescription = description || site.siteMetadata.description;
37 |
38 | const baseMeta: TMetaProps[] = [
39 | {
40 | name: `description`,
41 | content: metaDescription,
42 | },
43 | {
44 | property: `og:title`,
45 | content: title,
46 | },
47 | {
48 | property: `og:description`,
49 | content: metaDescription,
50 | },
51 | {
52 | property: `og:type`,
53 | content: `website`,
54 | },
55 | {
56 | name: `twitter:card`,
57 | content: `summary`,
58 | },
59 | {
60 | name: `twitter:creator`,
61 | content: site.siteMetadata.author,
62 | },
63 | {
64 | name: `twitter:title`,
65 | content: title,
66 | },
67 | {
68 | name: `twitter:description`,
69 | content: metaDescription,
70 | },
71 | ];
72 |
73 | return (
74 |
82 | );
83 | };
84 |
85 | SEO.defaultProps = {
86 | lang: `en`,
87 | meta: [] as TMetaProps[],
88 | description: ``,
89 | };
90 |
91 | export default SEO;
92 |
--------------------------------------------------------------------------------
/src/helpers/fetch/pagination.ts:
--------------------------------------------------------------------------------
1 | import { isUndefined, isNumber } from 'helpers/check';
2 | import { IPaginationRequestParams } from 'helpers/definitions';
3 |
4 | export interface IHasMoreDataOptions {
5 | offset?: number;
6 | previousLength?: number;
7 | length: number;
8 | totalCount: number;
9 | }
10 |
11 | /**
12 | * Check if there is more available data to be fetched on the API, the previous amount could be a previous count or
13 | * an offset value, if the offset is present that will be used to do the check
14 | *
15 | * @param {IHasMoreDataOptions} options
16 | *
17 | * @returns {boolean} boolean
18 | */
19 | export const hasMoreData = (options: IHasMoreDataOptions): boolean => {
20 | const { offset, previousLength, length, totalCount } = options;
21 |
22 | if (isUndefined(offset) && isUndefined(previousLength) && isNumber(length) && isNumber(totalCount)) {
23 | return length > 0 && length < totalCount - 1; // totalCount - 1, because the JS Array first index is 0
24 | }
25 |
26 | if (isNumber(length) && isNumber(totalCount) && (isNumber(offset) || isNumber(previousLength))) {
27 | const currentTotal = (offset ? offset : previousLength || 0) + length;
28 | return length > 0 && currentTotal < totalCount - 1; // totalCount - 1, because the JS Array first index is 0
29 | }
30 |
31 | return false;
32 | };
33 |
34 | /**
35 | * Process and update the pagination params attributes
36 | *
37 | * @param {T extends IPaginationRequestParams = IPaginationRequestParams} params
38 | *
39 | * @returns {T} processed params
40 | */
41 | export const updatePaginationAttributes = (
42 | params: T
43 | ): T => {
44 | const { countPerPage, previousTotalCount } = params;
45 | const { offset, page } = params;
46 |
47 | if (offset || page) {
48 | return params;
49 | }
50 |
51 | if (isNumber(previousTotalCount)) {
52 | if (isUndefined(offset) && isUndefined(page)) {
53 | params.page = Math.floor(previousTotalCount / countPerPage);
54 | return params;
55 | }
56 | }
57 |
58 | return {
59 | ...params,
60 | page: 0,
61 | };
62 | };
63 |
64 | export default {
65 | updatePaginationAttributes,
66 | hasMoreData,
67 | };
68 |
--------------------------------------------------------------------------------
/src/helpers/check.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { IProjectWindow, TRUTHY, TO_STRING, JSTypeof, TFunction } from './definitions';
3 |
4 | // ---------------------------------------------------------------------------- //
5 | // @begin: pre defined values
6 |
7 | export const isProduction = process.env.NODE_ENV === 'production';
8 |
9 | export const isDevelopment = process.env.NODE_ENV === 'development';
10 |
11 | export const isBrowser = typeof window !== 'undefined';
12 |
13 | const localWindow = isBrowser ? window : {};
14 | const { CSS = {} } = localWindow as IProjectWindow;
15 |
16 | export const hasCssVariablesSupport = isBrowser && CSS && CSS.supports && CSS.supports('(--a: 0)');
17 |
18 | // @end: pre defined values
19 | // ---------------------------------------------------------------------------- //
20 | // @begin: check values
21 |
22 | export const isFunction = (value: unknown): value is T =>
23 | value !== null && typeof value === JSTypeof.FUNCTION;
24 |
25 | export const isUndefined = (value: unknown): value is T =>
26 | value !== null && typeof value === JSTypeof.UNDEFINED;
27 |
28 | const isObjectBasicCheck = (value: unknown): value is T =>
29 | value !== null && typeof value === JSTypeof.OBJECT;
30 |
31 | export const isObject = (value: unknown): value is T =>
32 | isObjectBasicCheck(value) && TO_STRING.call(value) === '[object Object]';
33 |
34 | export const isArray = (value: unknown): value is T => Array.isArray(value);
35 |
36 | export const isString = (value: unknown): value is T =>
37 | value !== null && typeof value === JSTypeof.STRING && TO_STRING.call(value) === '[object String]';
38 |
39 | export const isNumber = (value: unknown): value is T =>
40 | value !== null && typeof value === JSTypeof.NUMBER;
41 |
42 | export const isTrue = (value: unknown): boolean => TRUTHY.test(String(value)) && !!value;
43 |
44 | export const isReactElement = (value: unknown): value is T =>
45 | /* eslint-disable-next-line */ isObjectBasicCheck(value) && '$$typeof' in (value as any);
46 |
47 | export const isEmptyChildren = (value: React.ReactNode) => React.Children.count(value) === 0;
48 |
49 | // @end: check values
50 | // ---------------------------------------------------------------------------- //
51 |
--------------------------------------------------------------------------------
/src/helpers/language.ts:
--------------------------------------------------------------------------------
1 | /*
2 | https://formatjs.io/docs/polyfills/intl-relativetimeformat/
3 | */
4 | import { shouldPolyfill } from '@formatjs/intl-relativetimeformat/should-polyfill';
5 |
6 | import { ILocale, IProjectWindow } from './definitions';
7 | import { isBrowser } from './check';
8 |
9 | export const defaultLocale = 'en';
10 |
11 | const { navigator = { language: defaultLocale } } = isBrowser ? (window as IProjectWindow) : {};
12 |
13 | export const browserLanguage = navigator.language;
14 |
15 | export const browserLocale = (() => {
16 | let output = browserLanguage;
17 | if (/-|_/.test(output)) {
18 | ['-', '_'].forEach((languageSparator) => {
19 | if (output.lastIndexOf(languageSparator) !== -1) {
20 | output = output.split(languageSparator)[0];
21 | }
22 | });
23 | }
24 | return output;
25 | })();
26 |
27 | const polyfill = async (locale?: string) => {
28 | if (!shouldPolyfill()) return;
29 |
30 | await import('@formatjs/intl-relativetimeformat/polyfill');
31 |
32 | switch (locale) {
33 | case 'pt':
34 | await import(
35 | /* webpackMode: "lazy", webpackChunkName: "language_pt" */ `@formatjs/intl-relativetimeformat/locale-data/pt`
36 | );
37 | break;
38 | case 'es':
39 | await import(
40 | /* webpackMode: "lazy", webpackChunkName: "language_es" */ `@formatjs/intl-relativetimeformat/locale-data/es`
41 | );
42 | break;
43 | case 'en':
44 | default:
45 | await import(
46 | /* webpackMode: "lazy", webpackChunkName: "language_en" */ `@formatjs/intl-relativetimeformat/locale-data/en`
47 | );
48 | break;
49 | }
50 | };
51 |
52 | export async function loadLocale(locale?: string): Promise {
53 | let messages;
54 |
55 | await polyfill(locale);
56 |
57 | switch (locale) {
58 | case 'pt':
59 | messages = (await import(/* webpackMode: "lazy", webpackChunkName: "language_pt" */ `assets/languages/pt.json`))
60 | .default;
61 | return { locale, messages };
62 | case 'es':
63 | messages = (await import(/* webpackMode: "lazy", webpackChunkName: "language_es" */ `assets/languages/es.json`))
64 | .default;
65 | return { locale, messages };
66 | case 'en':
67 | default:
68 | locale = 'en';
69 | messages = (await import(/* webpackMode: "lazy", webpackChunkName: "language_en" */ `assets/languages/en.json`))
70 | .default;
71 | return { locale, messages };
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | const postcssPresetEnv = require('postcss-preset-env');
2 |
3 | const { activeEnv } = require('./scripts/load-dotenv-config');
4 |
5 | const gatsbyConfig = {
6 | siteMetadata: {
7 | title: `Gatsby TypeScript Application Starter`,
8 | description: `Kick off your next, great Gatsby typescript application project with this default starter. This barebones starter ships with the main Gatsby configuration files you might need.`,
9 | author: `@erkobridee`,
10 | },
11 | plugins: [
12 | `gatsby-plugin-react-helmet`,
13 | `gatsby-plugin-image`,
14 | {
15 | resolve: `gatsby-source-filesystem`,
16 | options: {
17 | name: `images`,
18 | path: `${__dirname}/src/assets/images`,
19 | },
20 | },
21 | `gatsby-plugin-sharp`,
22 | `gatsby-transformer-sharp`,
23 | {
24 | resolve: `gatsby-plugin-manifest`,
25 | options: {
26 | name: `gatsby-typescript-app-starter`,
27 | short_name: `ts-starter`,
28 | start_url: `.`,
29 | background_color: `#ffffff`,
30 | theme_color: `#6d2f9c`,
31 | display: `minimal-ui`,
32 | icon: `src/assets/images/gatsby-icon.png`, // This path is relative to the root of the site.
33 | },
34 | },
35 | {
36 | resolve: `gatsby-plugin-sass`,
37 | options: {
38 | postCssPlugins: [postcssPresetEnv()],
39 | },
40 | },
41 | {
42 | resolve: `gatsby-plugin-create-client-paths`,
43 | options: { prefixes: [`/photos/*`] },
44 | },
45 | ],
46 | };
47 |
48 | const pkg = require('./package.json');
49 | const pathPrefix = require('./scripts/get-path-prefix')(pkg);
50 | if (pathPrefix) {
51 | gatsbyConfig.pathPrefix = pathPrefix;
52 |
53 | // this (optional) plugin enables Progressive Web App + Offline functionality
54 | gatsbyConfig.plugins.push({
55 | resolve: `gatsby-plugin-offline`,
56 | options: {
57 | modifyUrlPrefix: {
58 | '/': `${pathPrefix}/`,
59 | },
60 | },
61 | });
62 | } else {
63 | // this (optional) plugin enables Progressive Web App + Offline functionality
64 | gatsbyConfig.plugins.push(`gatsby-plugin-offline`);
65 | }
66 |
67 | const isDevelopment = activeEnv === 'dev' || activeEnv === 'development';
68 | const proxyUrl = process.env.PROXY_URL || undefined;
69 | if (isDevelopment && proxyUrl) {
70 | const proxyPrefix = process.env.API_PREFIX || 'api';
71 | gatsbyConfig.proxy = {
72 | prefix: `/${proxyPrefix}`,
73 | url: proxyUrl,
74 | };
75 | }
76 |
77 | module.exports = gatsbyConfig;
78 |
--------------------------------------------------------------------------------
/src/domains/Photos/List/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import ReactDOMServer from 'react-dom/server';
3 |
4 | import { useSelector, useDispatch } from 'react-redux';
5 | import { FormattedMessage } from 'react-intl';
6 | import { RouteComponentProps /*, navigate*/ } from '@reach/router';
7 | import { /*withPrefix,*/ navigate } from 'gatsby';
8 |
9 | import { selectPhotosList, selectHasMore, selectIsFetching } from 'store/state/photos/selectors';
10 | import { loadPhotos } from 'store/state/photos/operations';
11 | import PhotoModel from 'data/models/Photos';
12 |
13 | import SEO from 'components/SEO';
14 | import { Tiles, Tile } from 'components/ui/Tiles';
15 |
16 | import PhotoCard from 'domains/Photos/Card';
17 |
18 | import './_styles.scss';
19 |
20 | const countPerPage = 18;
21 |
22 | export interface IPhotosList extends RouteComponentProps {}
23 |
24 | const PhotosList: React.FunctionComponent = () => {
25 | const dispatch = useDispatch();
26 | const photosList = useSelector(selectPhotosList);
27 | const hasMore = useSelector(selectHasMore);
28 | const isFetching = useSelector(selectIsFetching);
29 |
30 | React.useEffect(() => {
31 | dispatch(loadPhotos({ page: 0, countPerPage, previousTotalCount: photosList.length }));
32 | }, []);
33 |
34 | // const navigateToPhoto = (id: number) => () => navigate(`/gatsby-typescript-app-starter/photos/${id}`);
35 | const navigateToPhoto = (id: number) => () => navigate(`/photos/${id}`);
36 |
37 | const buildTileElement = (item: PhotoModel, index: number) => (
38 |
39 |
40 |
41 | );
42 |
43 | const onLoadMoreClick = () => {
44 | dispatch(loadPhotos({ countPerPage, previousTotalCount: photosList.length }));
45 | };
46 |
47 | return (
48 | <>
49 |
50 | {(text) => {
51 | const title = ReactDOMServer.renderToString(<>{text}>);
52 | return ;
53 | }}
54 |
55 |
56 |
57 |
{photosList.map(buildTileElement)}
58 |
59 | {!isFetching && hasMore && (
60 |
61 |
64 |
65 | )}
66 |
67 | {isFetching && (
68 |
69 |
70 |
71 | )}
72 |
73 | >
74 | );
75 | };
76 |
77 | export default PhotosList;
78 |
--------------------------------------------------------------------------------
/src/helpers/values.ts:
--------------------------------------------------------------------------------
1 | import { isObject, isArray } from './check';
2 |
3 | //----------------------------------------------------------------------------//
4 |
5 | export const getPropertyValue = (
6 | object: TObject,
7 | propertyName: Key
8 | ): TObject[Key] => object[propertyName];
9 |
10 | export const setPropertyValue = (
11 | object: TObject,
12 | propertyName: Key,
13 | value: TValue
14 | ): TObject => {
15 | object[propertyName] = value;
16 | return object;
17 | };
18 |
19 | //----------------------------------------------------------------------------//
20 |
21 | export const randomFloat = (min: number, max: number): number => Math.random() * (max - min) + min;
22 |
23 | export const randomDecimalString = (min: number, max: number, fixed = 2): string =>
24 | randomFloat(min, max).toFixed(fixed);
25 |
26 | export const randomDecimal = (min: number, max: number, fixed = 2): number => +randomDecimalString(min, max, fixed);
27 |
28 | export const randomInt = (min: number, max: number): number => Math.floor(randomFloat(min, max));
29 |
30 | export const randomIntAsString = (min: number, max: number, padStart = 2): string => {
31 | return String(randomInt(min, max)).padStart(padStart, '0');
32 | };
33 |
34 | export const randomBoolean = (): boolean => randomInt(0, 1) === 0;
35 |
36 | export const randomValue = (options: T | T[], inNumber?: number): T | undefined => {
37 | if (!isObject(options) && !isArray(options)) {
38 | return;
39 | }
40 | if (isObject(options)) {
41 | options = Object.values(options as any);
42 | }
43 | inNumber = Math.min(inNumber || Number.MAX_SAFE_INTEGER, options.length);
44 | return options[randomInt(0, inNumber - 1)];
45 | };
46 |
47 | //----------------------------------------------------------------------------//
48 |
49 | export const calculateAvailablePages = (data: T[], pageSize = 25): number =>
50 | Math.ceil(data.length / pageSize);
51 |
52 | export const paginate = (data: T[], start = 0, pageSize = 25): T[] => {
53 | const { length } = data;
54 | if (length === 0 || start > length) {
55 | return [];
56 | }
57 |
58 | if (start + pageSize > length) {
59 | return data.slice(start, length);
60 | }
61 |
62 | return data.slice(start, start + pageSize);
63 | };
64 |
65 | export const randomSetOfData = (data: T[], amount: number = randomInt(1, 10)): T[] => {
66 | const pagesAmount = calculateAvailablePages(data, amount);
67 | const randomPage = randomInt(1, pagesAmount);
68 | const pageStart = (randomPage - 1) * amount;
69 | return paginate(data, pageStart, amount);
70 | };
71 |
72 | export const randomAmountOfData = (data: T[], amount: number = randomInt(1, 10)): T[] => {
73 | const output: T[] = [];
74 | if (data.length === 0) {
75 | return output;
76 | }
77 | data = [...data];
78 | while (output.length < amount) {
79 | const index = randomInt(0, data.length - 1);
80 | output.push(data[index]);
81 | data.splice(index, 1);
82 | data = [...data];
83 | }
84 | return output;
85 | };
86 |
87 | //----------------------------------------------------------------------------//
88 |
--------------------------------------------------------------------------------
/src/assets/styles/base/_reset.scss:
--------------------------------------------------------------------------------
1 | * {
2 | &,
3 | &:before,
4 | &:after {
5 | box-sizing: inherit;
6 | }
7 |
8 | font: inherit;
9 | outline: none;
10 | }
11 |
12 | html,
13 | body,
14 | div,
15 | span,
16 | applet,
17 | object,
18 | iframe,
19 | h1,
20 | h2,
21 | h3,
22 | h4,
23 | h5,
24 | h6,
25 | p,
26 | blockquote,
27 | pre,
28 | a,
29 | abbr,
30 | acronym,
31 | address,
32 | big,
33 | cite,
34 | code,
35 | del,
36 | dfn,
37 | em,
38 | img,
39 | ins,
40 | kbd,
41 | q,
42 | s,
43 | samp,
44 | small,
45 | strike,
46 | strong,
47 | sub,
48 | sup,
49 | tt,
50 | var,
51 | b,
52 | u,
53 | i,
54 | center,
55 | dl,
56 | dt,
57 | dd,
58 | ol,
59 | ul,
60 | li,
61 | fieldset,
62 | form,
63 | label,
64 | legend,
65 | table,
66 | caption,
67 | tbody,
68 | tfoot,
69 | thead,
70 | tr,
71 | th,
72 | td,
73 | article,
74 | aside,
75 | canvas,
76 | details,
77 | embed,
78 | figure,
79 | figcaption,
80 | footer,
81 | header,
82 | hgroup,
83 | menu,
84 | nav,
85 | output,
86 | ruby,
87 | section,
88 | summary,
89 | time,
90 | mark,
91 | audio,
92 | video,
93 | hr {
94 | margin: 0;
95 | padding: 0;
96 | border: 0;
97 | }
98 |
99 | html {
100 | box-sizing: border-box;
101 | }
102 |
103 | body {
104 | background-color: var(--body-bg, white);
105 | }
106 |
107 | article,
108 | aside,
109 | details,
110 | figcaption,
111 | figure,
112 | footer,
113 | header,
114 | hgroup,
115 | menu,
116 | nav,
117 | section,
118 | main,
119 | form legend {
120 | display: block;
121 | }
122 |
123 | ol,
124 | ul {
125 | list-style: none;
126 | }
127 |
128 | blockquote,
129 | q {
130 | quotes: none;
131 | }
132 |
133 | button,
134 | input,
135 | textarea,
136 | select {
137 | margin: 0;
138 | }
139 |
140 | .btn,
141 | .form-control,
142 | .link,
143 | .reset {
144 | @include reset;
145 | }
146 |
147 | select.form-control::-ms-expand {
148 | display: none; // hide Select default icon on IE
149 | }
150 |
151 | textarea {
152 | resize: vertical;
153 | overflow: auto;
154 | vertical-align: top;
155 | }
156 |
157 | input::-ms-clear {
158 | display: none; // hide X icon in IE and Edge
159 | }
160 |
161 | table {
162 | border-collapse: collapse;
163 | border-spacing: 0;
164 | }
165 |
166 | img,
167 | video,
168 | svg {
169 | max-width: 100%;
170 | }
171 |
172 | //----------------------------------------------------------------------------//
173 |
174 | [type='reset'],
175 | [type='submit'],
176 | button,
177 | html [type='button'] {
178 | -webkit-appearance: button;
179 | }
180 |
181 | [type='button']::-moz-focus-inner,
182 | [type='reset']::-moz-focus-inner,
183 | [type='submit']::-moz-focus-inner,
184 | button::-moz-focus-inner {
185 | border-style: none;
186 | padding: 0;
187 | }
188 |
189 | [type='button']:-moz-focusring,
190 | [type='reset']:-moz-focusring,
191 | [type='submit']:-moz-focusring,
192 | button:-moz-focusring {
193 | outline: 1px dotted ButtonText;
194 | }
195 |
196 | [type='checkbox'],
197 | [type='radio'] {
198 | box-sizing: border-box;
199 | padding: 0;
200 | }
201 |
202 | [type='number']::-webkit-inner-spin-button,
203 | [type='number']::-webkit-outer-spin-button {
204 | height: auto;
205 | }
206 |
207 | [type='search'] {
208 | -webkit-appearance: textfield;
209 | outline-offset: -2px;
210 | }
211 |
212 | [type='search']::-webkit-search-cancel-button,
213 | [type='search']::-webkit-search-decoration {
214 | -webkit-appearance: none;
215 | }
216 |
217 | ::-webkit-input-placeholder {
218 | color: inherit;
219 | opacity: 0.54;
220 | }
221 |
222 | ::-webkit-file-upload-button {
223 | -webkit-appearance: button;
224 | font: inherit;
225 | }
226 |
227 | //----------------------------------------------------------------------------//
228 |
--------------------------------------------------------------------------------
/src/helpers/definitions.ts:
--------------------------------------------------------------------------------
1 | // ---------------------------------------------------------------------------- //
2 | // @begin: constants block
3 |
4 | export const DEFAULT_PAGE_SIZE = 25;
5 |
6 | export const TO_STRING = {}.toString;
7 |
8 | export const TRUTHY = /^(?:t(?:rue)?|y(?:es)?|on|1)$/i;
9 |
10 | export const FALSY = /^(?:f(?:alse)?|no?|off|0)$/i;
11 |
12 | // @end: constants block
13 | // ---------------------------------------------------------------------------- //
14 | // @begin: enums block
15 |
16 | export enum JSTypeof {
17 | UNDEFINED = 'undefined',
18 | FUNCTION = 'function',
19 | OBJECT = 'object',
20 | STRING = 'string',
21 | NUMBER = 'number',
22 | }
23 |
24 | export enum RequestProtocol {
25 | HTTP = 'http',
26 | HTTPS = 'https',
27 | }
28 |
29 | /**
30 | * Defines HTTP Request Methods
31 | */
32 | export enum RequestMethod {
33 | GET = 'GET',
34 | POST = 'POST',
35 | PUT = 'PUT',
36 | PATCH = 'PATCH',
37 | DELETE = 'DELETE',
38 | }
39 |
40 | /**
41 | * Defines request content types
42 | */
43 | export enum RequestContentTypes {
44 | IMAGE = 'image/jpg, image/png',
45 | JSON = 'application/json',
46 | FORM = '',
47 | }
48 |
49 | export enum APIResponseStatus {
50 | SUCCESS = 'api:response::success',
51 | ERROR = 'api:response::error',
52 | }
53 |
54 | // @end: enums block
55 | // ---------------------------------------------------------------------------- //
56 | // @begin: types block
57 |
58 | export type TJSObject = Record;
59 |
60 | export type TJSValue = TJSObject | any;
61 |
62 | export type TArrayFilter- = (item: Item) => boolean;
63 |
64 | export type TFunction = (...args: Tuple) => Return;
65 |
66 | export type TEmptyCallback = () => void;
67 |
68 | export type TCallback =
69 | | TFunction
70 | | TEmptyCallback;
71 |
72 | export type TTypeCallback = (options?: Options) => Return;
73 |
74 | export type TRender = TFunction<[RenderOptions], JSX.Element> | React.ReactNode;
75 |
76 | //---
77 |
78 | /**
79 | * Possible request body to be sended to the API
80 | */
81 | export type TRequestBody = string | FormData | undefined;
82 |
83 | export type TAPIResponse = IStringAPIResponse | IAPIResponse;
84 |
85 | // @end: types block
86 | // ---------------------------------------------------------------------------- //
87 | // @begin: interfaces block
88 |
89 | /* eslint-disable-next-line */
90 | export interface IDictionary {
91 | [key: string]: T;
92 | }
93 |
94 | export interface IProjectWindow extends Window {
95 | /* eslint-disable-next-line */
96 | CSS?: any;
97 | }
98 |
99 | export interface ILocale {
100 | locale: string;
101 | messages: IDictionary;
102 | }
103 |
104 | export interface IBaseModel {
105 | readonly __innerprops__: T;
106 | }
107 |
108 | export interface IBaseResponse {
109 | status: APIResponseStatus;
110 | statusCode: number;
111 | data: T;
112 | /** defined when the API has "pagination" support */
113 | totalCount?: number;
114 | }
115 |
116 | export interface ICancelableBaseResponse {
117 | controller?: AbortController;
118 | request: TTypeCallback>;
119 | }
120 |
121 | /**
122 | * Base response object returned from the buildRequest
123 | */
124 | export interface IAPIResponse {
125 | readonly statusCode: number;
126 | readonly headers: Headers;
127 | readonly body: T;
128 |
129 | // defined to be able to use the Pick
130 | // and read the generic type assigned to it
131 | __bodyType__?: T;
132 | }
133 |
134 | export interface IStringAPIResponse extends IAPIResponse {}
135 |
136 | export interface IPaginationRequestParams {
137 | offset?: number;
138 | page?: number;
139 | countPerPage: number;
140 | previousTotalCount?: number;
141 | }
142 |
143 | /**
144 | * Defines api request options that is used to build an api call.
145 | */
146 | export interface IRequestOptions {
147 | /** like passing the access token with the value: `Bearer ${accessToken}` */
148 | authorization?: string;
149 | contentType?: RequestContentTypes;
150 | protocol?: RequestProtocol | string;
151 | method?: RequestMethod;
152 | host?: string;
153 | api?: string;
154 | /** uses the values defined on the .env file for the host and api options */
155 | useDefaultPrefix?: boolean;
156 | urlPath: string;
157 | variables?: IDictionary;
158 | parameters?: IDictionary;
159 | headers?: IDictionary;
160 | noCache?: boolean;
161 | signal?: AbortSignal;
162 | }
163 |
164 | // @end: interfaces block
165 | // ---------------------------------------------------------------------------- //
166 |
--------------------------------------------------------------------------------
/src/helpers/fetch/buildRequest.ts:
--------------------------------------------------------------------------------
1 | import * as url from 'url';
2 | import * as qs from 'qs';
3 | import {
4 | IDictionary,
5 | RequestContentTypes,
6 | RequestMethod,
7 | TRequestBody,
8 | IRequestOptions,
9 | IAPIResponse,
10 | TAPIResponse,
11 | } from 'helpers/definitions';
12 | import readImageBlobAsDataURL from './readImageBlobAsDataURL';
13 |
14 | /**
15 | * Build an output response object that implements the IAPIResponse readonly attributes
16 | *
17 | * @param {Response} response
18 | * @param {any} body
19 | *
20 | * @returns {R} output response object
21 | */
22 | function buildResponseOutput(
23 | response: Response,
24 | bodyObject: Pick = {}
25 | ): R {
26 | const { status, headers } = response;
27 | return {
28 | get statusCode() {
29 | return status;
30 | },
31 | get headers() {
32 | return headers;
33 | },
34 | get body() {
35 | return bodyObject;
36 | },
37 | } as R;
38 | }
39 |
40 | /**
41 | * Build and handle a fetch request
42 | *
43 | * @param {IRequestOptions} options
44 | *
45 | * @returns {Promise} promise
46 | */
47 | export async function buildRequest(options: IRequestOptions): Promise {
48 | let { parameters } = options;
49 | const { useDefaultPrefix = false } = options;
50 | const {
51 | authorization,
52 | method = RequestMethod.GET,
53 | contentType = RequestContentTypes.JSON,
54 | protocol,
55 | host = useDefaultPrefix ? process.env.API_HOST || undefined : undefined,
56 | api = useDefaultPrefix ? process.env.API_PREFIX || undefined : undefined,
57 | urlPath,
58 | variables = {},
59 | headers: extraHeaders = {},
60 | noCache,
61 | signal,
62 | } = options;
63 |
64 | /**
65 | * No cache parameter
66 | */
67 | if (noCache) {
68 | parameters = { ...parameters, _: Math.random() };
69 | }
70 |
71 | const getRequestURL = () => {
72 | const pathname = api ? `${api}/${urlPath}` : urlPath;
73 | if (!host) {
74 | return pathname;
75 | }
76 |
77 | return url.format({
78 | ...{
79 | slashes: true,
80 | host,
81 | pathname,
82 | },
83 | ...(protocol ? { protocol } : {}),
84 | });
85 | };
86 |
87 | /**
88 | * Request URL is the URL that we will call and all the variable is replaced to values
89 | * @type {string}
90 | */
91 | let requestUrl = getRequestURL();
92 |
93 | /**
94 | * Replace url path variables, like {userId}
95 | */
96 | for (const variable in variables) {
97 | if (variables.hasOwnProperty(variable)) {
98 | requestUrl = requestUrl.replace(`{${variable}}`, variables[variable]);
99 | }
100 | }
101 |
102 | /**
103 | * Prepare parameters that will be sent to the api.
104 | */
105 | let body: TRequestBody;
106 | if (parameters) {
107 | switch (method) {
108 | case RequestMethod.GET:
109 | requestUrl += `?${qs.stringify(parameters)}`;
110 | break;
111 | default:
112 | switch (contentType) {
113 | case RequestContentTypes.JSON:
114 | body = JSON.stringify(parameters);
115 | break;
116 | case RequestContentTypes.FORM:
117 | body = new FormData();
118 | for (const parameter in parameters) {
119 | if (parameters.hasOwnProperty(parameter)) {
120 | body.append(parameter, parameters[parameter]);
121 | }
122 | }
123 | break;
124 | }
125 | break;
126 | }
127 | }
128 |
129 | /**
130 | * Prepare headers
131 | */
132 | const headers: IDictionary = {
133 | Accept: contentType,
134 | 'Content-Type': contentType,
135 | };
136 |
137 | /**
138 | * If there is any authorization added to the header
139 | */
140 | if (authorization) {
141 | headers.Authorization = authorization;
142 | }
143 |
144 | for (const extraHeader in extraHeaders) {
145 | if (extraHeaders.hasOwnProperty(extraHeader)) {
146 | headers[extraHeader] = extraHeaders[extraHeader];
147 | }
148 | }
149 |
150 | /**
151 | * Do the fetch call and process its response
152 | */
153 | try {
154 | const response = await fetch(requestUrl, {
155 | method,
156 | headers,
157 | body,
158 | signal,
159 | });
160 |
161 | if (!response.ok) {
162 | throw response;
163 | }
164 |
165 | let outputObject = {};
166 | if (contentType === RequestContentTypes.IMAGE) {
167 | const imageBlob = await response.blob();
168 | const imageAsString = await readImageBlobAsDataURL(imageBlob);
169 | outputObject = imageAsString;
170 | } else {
171 | try {
172 | outputObject = await response.json();
173 | } catch (e) {
174 | /* comment to fix the tslint empty block warning */
175 | }
176 | }
177 | return buildResponseOutput(response, outputObject);
178 | } catch (error) {
179 | let finalError: R;
180 | try {
181 | finalError = await error.json();
182 | } catch (e) {
183 | finalError = error;
184 | }
185 | throw buildResponseOutput(error, finalError);
186 | }
187 | }
188 |
189 | export default buildRequest;
190 |
--------------------------------------------------------------------------------
/src/helpers/arrays.ts:
--------------------------------------------------------------------------------
1 | import { TArrayFilter, TJSValue } from './definitions';
2 | import { isObject, isArray } from './check';
3 |
4 | /**
5 | * array filter function to get the intersection
6 | *
7 | * @param {T[]} values
8 | * @param {string} byObjectProperty
9 | *
10 | * @return {TFilter}
11 | */
12 | function filterIntersection(values: T[], byObjectProperty?: string): TArrayFilter {
13 | if (byObjectProperty) {
14 | return (x: T) => values.some((y) => (x as any)[byObjectProperty] === (y as any)[byObjectProperty]);
15 | }
16 | return (x: T) => values.includes(x);
17 | }
18 |
19 | /**
20 | * array filter function to get the difference
21 | *
22 | * @param {T[]} values
23 | * @param {string} byObjectProperty
24 | *
25 | * @return {TArrayFilter}
26 | */
27 | function filterDifference(values: T[], byObjectProperty?: string): TArrayFilter {
28 | if (byObjectProperty) {
29 | return (x: T) => !values.some((y) => (x as any)[byObjectProperty] === (y as any)[byObjectProperty]);
30 | }
31 | return (x: T) => !values.includes(x);
32 | }
33 |
34 | // -------------------------------------------------------------------------- //
35 |
36 | /**
37 | * array find function
38 | *
39 | * @param {T} value
40 | * @param {string} byObjectProperty
41 | *
42 | * @return {TArrayFilter}
43 | */
44 | function filterFindValue(value: T, byObjectProperty?: string): TArrayFilter {
45 | if (byObjectProperty) {
46 | return (x: T) => (x as any)[byObjectProperty] === (isObject(value) ? (value as any)[byObjectProperty] : value);
47 | }
48 | return (x: T) => x === value;
49 | }
50 |
51 | // -------------------------------------------------------------------------- //
52 |
53 | /**
54 | * from a given array get the unique elements from it
55 | *
56 | * @param {T[]} values
57 | * @param {string} byObjectProperty - optional parameter in case of array of objects
58 | *
59 | * @return {T[]} values without repetition
60 | */
61 | function unique(values: T[], byObjectProperty?: string): T[] {
62 | return values.reduce((unique, item) => {
63 | return filterIntersection(unique, byObjectProperty)(item) ? unique : [...unique, item];
64 | }, [] as T[]);
65 | }
66 |
67 | /**
68 | * get only the values present on both parameters arrays
69 | *
70 | * @param {T[]} arr1
71 | * @param {T[]} arr2
72 | * @param {string} byObjectProperty - optional parameter in case of array of objects
73 | *
74 | * @return {T[]} values present on both arrays
75 | */
76 | function intersection(arr1: T[], arr2: T[], byObjectProperty?: string): T[] {
77 | return arr1.filter(filterIntersection(arr2, byObjectProperty));
78 | }
79 |
80 | /**
81 | * check if the first parameter contains the values from the second parameter
82 | *
83 | * @param {T[]} sourceArray
84 | * @param {T | T[]} matchValue
85 | * @param {string} byObjectProperty - optional parameter in case of array of objects
86 | *
87 | * @return {boolean} flag
88 | */
89 | function contains(sourceArray: T[], matchValue: T | T[], byObjectProperty?: string): boolean {
90 | if (isArray(matchValue)) {
91 | const arr2Lenght = matchValue.length;
92 | const arr3 = intersection(sourceArray, matchValue, byObjectProperty);
93 | return arr2Lenght === arr3.length;
94 | }
95 | return filterIntersection(sourceArray, byObjectProperty)(matchValue as T);
96 | }
97 |
98 | /**
99 | * get the different values present on the first parameter
100 | *
101 | * @param {T[]} sourceArray
102 | * @param {T[]} matchArray
103 | * @param {string} byObjectProperty - optional parameter in case of array of objects
104 | *
105 | * @return {T[]}
106 | */
107 | function difference(sourceArray: T[], matchArray: T[], byObjectProperty?: string): T[] {
108 | return sourceArray.filter(filterDifference(matchArray, byObjectProperty));
109 | }
110 |
111 | /**
112 | * get the different values present on both parameters arrays
113 | *
114 | * @param {T[]} arr1
115 | * @param {T[]} arr2
116 | * @param {string} byObjectProperty - optional parameter in case of array of objects
117 | *
118 | * @return {T[]}
119 | */
120 | function symmetricDifference(arr1: T[], arr2: T[], byObjectProperty?: string): T[] {
121 | return arr1
122 | .filter(filterDifference(arr2, byObjectProperty))
123 | .concat(arr2.filter(filterDifference(arr1, byObjectProperty)));
124 | }
125 |
126 | /**
127 | * find a value inside of the given values array
128 | *
129 | * @param {T[]} values
130 | * @param {T | string} value
131 | * @param {string} byObjectProperty - optional parameter in case of array of objects
132 | *
133 | * @return {T | undefined} value founded or undefined
134 | */
135 | function find(values: T[], value: T | string, byObjectProperty?: string): T | undefined {
136 | return values.find(filterFindValue(value, byObjectProperty));
137 | }
138 |
139 | /**
140 | * Merge two arrays of given type where will return a new array with the values updates
141 | * and the new non repeted elements added to it
142 | *
143 | * @param {T[]} sourceArray
144 | * @param {T[]} withArray
145 | * @param {string} byObjectProperty - optional parameter in case of array of objects
146 | */
147 | function merge(sourceArray: T[], withArray: T[], byObjectProperty?: string): T[] {
148 | return [
149 | ...sourceArray.map((item) => {
150 | const element = find(withArray, item, byObjectProperty);
151 | if (element) {
152 | return element;
153 | }
154 | return item;
155 | }),
156 | ...difference(withArray, sourceArray, byObjectProperty),
157 | ];
158 | }
159 |
160 | // -------------------------------------------------------------------------- //
161 |
162 | export {
163 | unique as arrayUnique,
164 | intersection as arrayIntersection,
165 | contains as arrayContains,
166 | difference as arrayDifference,
167 | symmetricDifference as arraySymmetricDifference,
168 | find as arrayFind,
169 | merge as arrayMerge,
170 | };
171 |
172 | // -------------------------------------------------------------------------- //
173 |
174 | export default {
175 | unique,
176 | intersection,
177 | contains,
178 | difference,
179 | symmetricDifference,
180 | find,
181 | merge,
182 | };
183 |
--------------------------------------------------------------------------------
/src/helpers/__tests__/check.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as Check from '../check';
3 |
4 | describe('helpers/check', () => {
5 | describe('isFunction', () => {
6 | it('should be a function', () => {
7 | expect(Check.isFunction(() => undefined)).toBeTruthy();
8 | });
9 |
10 | it('should not be a function', () => {
11 | expect(Check.isFunction({})).toBeFalsy();
12 | expect(Check.isFunction(null)).toBeFalsy();
13 | expect(Check.isFunction(undefined)).toBeFalsy();
14 | expect(Check.isFunction('')).toBeFalsy();
15 | expect(Check.isFunction(123)).toBeFalsy();
16 | });
17 | });
18 |
19 | describe('isUndefined', () => {
20 | it('should be undefined', () => {
21 | expect(Check.isUndefined(undefined)).toBeTruthy();
22 | });
23 |
24 | it('should not be undefined', () => {
25 | expect(Check.isUndefined(1)).toBeFalsy();
26 | expect(Check.isUndefined('')).toBeFalsy();
27 | expect(Check.isUndefined(null)).toBeFalsy();
28 | expect(Check.isUndefined({})).toBeFalsy();
29 | expect(Check.isUndefined([])).toBeFalsy();
30 | expect(Check.isUndefined(() => undefined)).toBeFalsy();
31 | });
32 | });
33 |
34 | describe('isObject', () => {
35 | it('should be an object', () => {
36 | expect(Check.isObject({})).toBeTruthy();
37 | expect(Check.isObject({ a: 'a', number: 1, boolean: true, array: [1, 2, 3] })).toBeTruthy();
38 | });
39 |
40 | it('should not be an object', () => {
41 | expect(Check.isObject(() => undefined)).toBeFalsy();
42 | expect(Check.isObject(null)).toBeFalsy();
43 | expect(Check.isObject(undefined)).toBeFalsy();
44 | expect(Check.isObject(123)).toBeFalsy();
45 | expect(Check.isObject('')).toBeFalsy();
46 | });
47 | });
48 |
49 | describe('isArray', () => {
50 | it('should be an array', () => {
51 | expect(Check.isArray([])).toBeTruthy();
52 | expect(Check.isArray([1, 2, 3])).toBeTruthy();
53 | expect(Check.isArray(['a', 'b', 'c'])).toBeTruthy();
54 | expect(Check.isArray(['', 1, NaN, undefined, null, {}, []])).toBeTruthy();
55 | });
56 |
57 | it('should not be an array', () => {
58 | expect(Check.isArray('hello')).toBeFalsy();
59 | expect(Check.isArray({})).toBeFalsy();
60 | expect(Check.isArray(null)).toBeFalsy();
61 | expect(Check.isArray(undefined)).toBeFalsy();
62 | expect(Check.isArray(123)).toBeFalsy();
63 | });
64 | });
65 |
66 | describe('isString', () => {
67 | it('should be a string', () => {
68 | expect(Check.isString('')).toBeTruthy();
69 | });
70 |
71 | it('should not be a string', () => {
72 | expect(Check.isString(123)).toBeFalsy();
73 | expect(Check.isString({})).toBeFalsy();
74 | expect(Check.isString([])).toBeFalsy();
75 | expect(Check.isString(null)).toBeFalsy();
76 | expect(Check.isString(undefined)).toBeFalsy();
77 | expect(Check.isString(() => undefined)).toBeFalsy();
78 | });
79 | });
80 |
81 | describe('isNumber', () => {
82 | it('should be a number', () => {
83 | expect(Check.isNumber(123)).toBeTruthy();
84 | });
85 |
86 | it('should not be a number', () => {
87 | expect(Check.isNumber('')).toBeFalsy();
88 | expect(Check.isNumber({})).toBeFalsy();
89 | expect(Check.isNumber([])).toBeFalsy();
90 | expect(Check.isNumber(null)).toBeFalsy();
91 | expect(Check.isNumber(undefined)).toBeFalsy();
92 | expect(Check.isNumber(() => undefined)).toBeFalsy();
93 | });
94 | });
95 |
96 | describe('isTrue', () => {
97 | it('should be true', () => {
98 | expect(Check.isTrue(1)).toBeTruthy();
99 | expect(Check.isTrue('true')).toBeTruthy();
100 | expect(Check.isTrue(true)).toBeTruthy();
101 | });
102 |
103 | it('should be false', () => {
104 | expect(Check.isTrue(0)).toBeFalsy();
105 | expect(Check.isTrue('false')).toBeFalsy();
106 | expect(Check.isTrue(false)).toBeFalsy();
107 | expect(Check.isTrue(null)).toBeFalsy();
108 | expect(Check.isTrue(undefined)).toBeFalsy();
109 | expect(Check.isTrue(NaN)).toBeFalsy();
110 | expect(Check.isTrue([])).toBeFalsy();
111 | expect(Check.isTrue({})).toBeFalsy();
112 | });
113 | });
114 |
115 | describe('isReactElement', () => {
116 | it('should not be', () => {
117 | expect(Check.isReactElement(() => undefined)).toBeFalsy();
118 | expect(Check.isReactElement({})).toBeFalsy();
119 | expect(Check.isReactElement([])).toBeFalsy();
120 | expect(Check.isReactElement('')).toBeFalsy();
121 | expect(Check.isReactElement(null)).toBeFalsy();
122 | expect(Check.isReactElement(undefined)).toBeFalsy();
123 | });
124 |
125 | it('should be', () => {
126 | expect(Check.isReactElement(<>>)).toBeTruthy();
127 | expect(Check.isReactElement(
hello world
)).toBeTruthy();
128 | expect(Check.isReactElement(hello world)).toBeTruthy();
129 | });
130 | });
131 |
132 | describe('isEmptyChildren', () => {
133 | const getReactComponentChildren = (element: JSX.Element) => {
134 | const { props } = element;
135 | return props.children;
136 | };
137 |
138 | it('should not be', () => {
139 | expect(
140 | Check.isEmptyChildren(
141 | getReactComponentChildren(
142 | <>
143 |
144 |
145 | hello
146 |
147 |
148 | >
149 | )
150 | )
151 | ).toBeFalsy();
152 | expect(
153 | Check.isEmptyChildren(
154 | getReactComponentChildren(
155 |
156 |
157 | hello
158 |
159 |
160 | )
161 | )
162 | ).toBeFalsy();
163 | expect(
164 | Check.isEmptyChildren(
165 | getReactComponentChildren(
166 |
167 | hello
168 |
169 | )
170 | )
171 | ).toBeFalsy();
172 | });
173 |
174 | it('should be', () => {
175 | expect(Check.isEmptyChildren(getReactComponentChildren(<>>))).toBeTruthy();
176 | expect(Check.isEmptyChildren(getReactComponentChildren())).toBeTruthy();
177 | expect(Check.isEmptyChildren(getReactComponentChildren())).toBeTruthy();
178 | });
179 | });
180 | });
181 |
--------------------------------------------------------------------------------
/src/helpers/__tests__/arrays.ts:
--------------------------------------------------------------------------------
1 | import { default as Arrays } from '../arrays';
2 |
3 | describe('helpers/arrays', () => {
4 | describe('unique', () => {
5 | it('on plain arrays', () => {
6 | const valuesResultExpected = ['a', 'b', 'c'];
7 | const values = Arrays.unique(['a', 'a', 'b', 'b', 'b', 'c', 'c']);
8 | expect(values).toHaveLength(valuesResultExpected.length);
9 | expect(values).toMatchObject(valuesResultExpected);
10 | });
11 |
12 | it('on object arrays', () => {
13 | const valuesResultExpected = [{ att: 'a' }, { att: 'b' }, { att: 'c' }];
14 | const values = Arrays.unique(
15 | [{ att: 'a' }, { att: 'a' }, { att: 'b' }, { att: 'b' }, { att: 'c' }, { att: 'c' }],
16 | 'att'
17 | );
18 | expect(values).toHaveLength(valuesResultExpected.length);
19 | expect(values).toMatchObject(valuesResultExpected);
20 | });
21 | });
22 |
23 | describe('intersection', () => {
24 | it('on plain arrays', () => {
25 | const valuesA = ['a', 'b', 'c', 'd'];
26 | const valuesB = ['c', 'd', 'e', 'f'];
27 | const valuesResultExpected = ['c', 'd'];
28 |
29 | const values = Arrays.intersection(valuesA, valuesB);
30 | expect(values).toHaveLength(valuesResultExpected.length);
31 | expect(values).toMatchObject(valuesResultExpected);
32 | });
33 |
34 | it('on object arrays', () => {
35 | const valuesA = [{ att: 'a' }, { att: 'b' }, { att: 'c' }, { att: 'd' }];
36 | const valuesB = [{ att: 'c' }, { att: 'd' }, { att: 'e' }, { att: 'f' }];
37 | const valuesResultExpected = [{ att: 'c' }, { att: 'd' }];
38 |
39 | const values = Arrays.intersection(valuesA, valuesB, 'att');
40 | expect(values).toHaveLength(valuesResultExpected.length);
41 | expect(values).toMatchObject(valuesResultExpected);
42 | });
43 | });
44 |
45 | describe('contains', () => {
46 | describe('on plain arrays', () => {
47 | const values = ['a', 'b', 'c', 'd', 'e', 'f'];
48 |
49 | it('should contain a value', () => {
50 | const check = 'c';
51 | expect(Arrays.contains(values, check)).toBeTruthy();
52 | });
53 |
54 | it('should contain a set of values', () => {
55 | const check = ['c', 'd'];
56 | expect(Arrays.contains(values, check)).toBeTruthy();
57 | });
58 | });
59 |
60 | describe('on object arrays', () => {
61 | const values = [{ att: 'a' }, { att: 'b' }, { att: 'c' }, { att: 'd' }, { att: 'e' }, { att: 'f' }];
62 |
63 | it('should contain a value', () => {
64 | const check = { att: 'c' };
65 | expect(Arrays.contains(values, check, 'att')).toBeTruthy();
66 | });
67 |
68 | it('should contain a set of values', () => {
69 | const check = [{ att: 'c' }, { att: 'd' }];
70 | expect(Arrays.contains(values, check, 'att')).toBeTruthy();
71 | });
72 | });
73 | });
74 |
75 | describe('difference', () => {
76 | it('on plain arrays', () => {
77 | const valuesA = ['a', 'b', 'c', 'd'];
78 | const valuesB = ['c', 'd', 'e', 'f'];
79 | const valuesResultExpected = ['a', 'b'];
80 |
81 | const values = Arrays.difference(valuesA, valuesB);
82 | expect(values).toHaveLength(valuesResultExpected.length);
83 | expect(values).toMatchObject(valuesResultExpected);
84 | });
85 |
86 | it('on object arrays', () => {
87 | const valuesA = [{ att: 'a' }, { att: 'b' }, { att: 'c' }, { att: 'd' }];
88 | const valuesB = [{ att: 'c' }, { att: 'd' }, { att: 'e' }, { att: 'f' }];
89 | const valuesResultExpected = [{ att: 'a' }, { att: 'b' }];
90 |
91 | const values = Arrays.difference(valuesA, valuesB, 'att');
92 | expect(values).toHaveLength(valuesResultExpected.length);
93 | expect(values).toMatchObject(valuesResultExpected);
94 | });
95 | });
96 |
97 | describe('symmetricDifference', () => {
98 | it('on plain arrays', () => {
99 | const valuesA = ['a', 'b', 'c', 'd'];
100 | const valuesB = ['c', 'd', 'e', 'f'];
101 | const valuesResultExpected = ['a', 'b', 'e', 'f'];
102 |
103 | const values = Arrays.symmetricDifference(valuesA, valuesB);
104 | expect(values).toHaveLength(valuesResultExpected.length);
105 | expect(values).toMatchObject(valuesResultExpected);
106 | });
107 |
108 | it('on object arrays', () => {
109 | const valuesA = [{ att: 'a' }, { att: 'b' }, { att: 'c' }, { att: 'd' }];
110 | const valuesB = [{ att: 'c' }, { att: 'd' }, { att: 'e' }, { att: 'f' }];
111 | const valuesResultExpected = [{ att: 'a' }, { att: 'b' }, { att: 'e' }, { att: 'f' }];
112 |
113 | const values = Arrays.symmetricDifference(valuesA, valuesB, 'att');
114 | expect(values).toHaveLength(valuesResultExpected.length);
115 | expect(values).toMatchObject(valuesResultExpected);
116 | });
117 | });
118 |
119 | describe('find', () => {
120 | it('on plain arrays', () => {
121 | const values = ['a', 'b', 'c'];
122 |
123 | expect(Arrays.find(values, 'a')).toBeDefined();
124 | expect(Arrays.find(values, 'b')).toBeDefined();
125 | expect(Arrays.find(values, 'c')).toBeDefined();
126 | });
127 |
128 | it('on object arrays', () => {
129 | const values = [{ att: 'a' }, { att: 'b' }, { att: 'c' }];
130 |
131 | expect(Arrays.find(values, 'a', 'att')).toBeDefined();
132 | expect(Arrays.find(values, 'b', 'att')).toBeDefined();
133 | expect(Arrays.find(values, 'c', 'att')).toBeDefined();
134 | });
135 | });
136 |
137 | describe('merge', () => {
138 | it('on plain arrays', () => {
139 | const valuesA = ['a', 'b', 'c', 'd'];
140 | const valuesB = ['c', 'd', 'e', 'f'];
141 | const valuesResultExpected = ['a', 'b', 'c', 'd', 'e', 'f'];
142 |
143 | const values = Arrays.merge(valuesA, valuesB);
144 | expect(values).toHaveLength(valuesResultExpected.length);
145 | expect(values).toMatchObject(valuesResultExpected);
146 | });
147 |
148 | it('on object arrays', () => {
149 | const valuesA = [{ att: 'a' }, { att: 'b' }, { att: 'c' }, { att: 'd' }];
150 | const valuesB = [{ att: 'c' }, { att: 'd' }, { att: 'e' }, { att: 'f' }];
151 | const valuesResultExpected = [{ att: 'a' }, { att: 'b' }, { att: 'c' }, { att: 'd' }, { att: 'e' }, { att: 'f' }];
152 |
153 | const values = Arrays.merge(valuesA, valuesB, 'att');
154 | expect(values).toHaveLength(valuesResultExpected.length);
155 | expect(values).toMatchObject(valuesResultExpected);
156 | });
157 | });
158 | });
159 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gatsby-typescript-app-starter",
3 | "private": true,
4 | "description": "A simple typescript application starter to get up and developing quickly with Gatsby",
5 | "version": "0.5.0",
6 | "author": "Erko Bridee ",
7 | "dependencies": {
8 | "abortcontroller-polyfill": "^1.7.1",
9 | "clsx": "^1.1.1",
10 | "css-vars-ponyfill": "^2.4.3",
11 | "qs": "^6.10.0",
12 | "react": "^17.0.1",
13 | "react-dom": "^17.0.1",
14 | "react-helmet": "^6.1.0",
15 | "react-intl": "^5.13.4",
16 | "react-redux": "^7.2.2",
17 | "redux": "^4.0.5",
18 | "redux-persist": "^6.0.0",
19 | "redux-thunk": "^2.3.0",
20 | "whatwg-fetch": "^3.6.2"
21 | },
22 | "devDependencies": {
23 | "@babel/cli": "^7.13.10",
24 | "@babel/preset-env": "^7.13.10",
25 | "@babel/preset-react": "^7.12.13",
26 | "@formatjs/intl-relativetimeformat": "^8.1.3",
27 | "@testing-library/jest-dom": "^5.11.9",
28 | "@testing-library/react": "^11.2.5",
29 | "@types/jest": "^26.0.21",
30 | "@types/node": "^14.14.35",
31 | "@types/qs": "^6.9.6",
32 | "@types/reach__router": "^1.3.7",
33 | "@types/react": "^17.0.3",
34 | "@types/react-dom": "^17.0.2",
35 | "@types/react-helmet": "^6.1.0",
36 | "@types/react-redux": "^7.1.16",
37 | "@typescript-eslint/eslint-plugin": "^4.18.0",
38 | "@typescript-eslint/parser": "^4.18.0",
39 | "babel-jest": "^26.6.3",
40 | "babel-plugin-react-intl": "^5.1.2",
41 | "babel-preset-gatsby": "^1.1.0",
42 | "browserslist": "^4.16.3",
43 | "eslint": "^7.22.0",
44 | "eslint-config-prettier": "^8.1.0",
45 | "eslint-loader": "^4.0.2",
46 | "eslint-plugin-prettier": "^3.3.1",
47 | "eslint-plugin-react": "^7.22.0",
48 | "express": "^4.17.1",
49 | "fs-extra": "^9.0.0",
50 | "gatsby": "^3.1.1",
51 | "gatsby-plugin-create-client-paths": "^3.1.0",
52 | "gatsby-plugin-image": "^1.1.1",
53 | "gatsby-plugin-manifest": "^3.1.0",
54 | "gatsby-plugin-offline": "^4.1.0",
55 | "gatsby-plugin-react-helmet": "^4.1.0",
56 | "gatsby-plugin-sass": "^4.1.0",
57 | "gatsby-plugin-sharp": "^3.1.1",
58 | "gatsby-source-filesystem": "^3.1.0",
59 | "gatsby-transformer-sharp": "^3.1.0",
60 | "gh-pages": "^3.1.0",
61 | "husky": "^5.1.3",
62 | "identity-obj-proxy": "^3.0.0",
63 | "intl-pluralrules": "^1.2.2",
64 | "is-ci": "^3.0.0",
65 | "jest": "^26.6.3",
66 | "jest-junit": "^12.0.0",
67 | "lint-staged": "^10.5.4",
68 | "npm-check": "^5.9.2",
69 | "npm-run-all": "^4.1.5",
70 | "postcss-preset-env": "^6.7.0",
71 | "prettier": "^2.2.1",
72 | "react-intl-translations-manager": "^5.0.3",
73 | "request": "^2.88.0",
74 | "sass": "^1.32.8",
75 | "svgo": "^1.3.2",
76 | "ts-jest": "^26.5.4",
77 | "typescript": "^4.2.3",
78 | "undefined": "^0.1.0",
79 | "webfonts-generator": "^0.4.0",
80 | "yargs": "^15.3.1"
81 | },
82 | "keywords": [
83 | "gatsby",
84 | "starter",
85 | "sass",
86 | "typescript",
87 | "dotenv",
88 | "proxy support",
89 | "jest",
90 | "unit tests",
91 | "unit tests coverage"
92 | ],
93 | "license": "MIT",
94 | "scripts": {
95 | "postinstall": "is-ci || npx husky install",
96 | "fonticons:clean-scss": "node scripts/fs/rm src/assets/styles/generated/",
97 | "fonticons:clean-generated": "node scripts/fs/rm .build/icons/",
98 | "fonticons:clean-static": "node scripts/fs/rm static/generated/",
99 | "fonticons:clean": "run-p fonticons:clean-generated fonticons:clean-static fonticons:clean-scss",
100 | "fonticons:compress-mkdir": "node scripts/fs/mkdir .build/icons/compressed",
101 | "fonticons:compress-run": "svgo --config=svgo-config.json --enable=removeStyleElement --enable=removeScriptElement --enable=sortAttrs src/assets/icons/*.svg -o .build/icons/compressed > /dev/null",
102 | "fonticons:compress": "run-s fonticons:compress-mkdir fonticons:compress-run",
103 | "fonticons:generate": "node scripts/font-generator static/generated/fonticons/ -f fonticons",
104 | "fonticons:move-scss": "node scripts/fs/mv static/generated/fonticons/fonticons.scss src/assets/styles/generated/fonticons.scss",
105 | "fonticons": "run-s fonticons:clean fonticons:compress fonticons:generate fonticons:move-scss",
106 | "translations:extract": "TRANSLATIONS=true babel src --extensions '.ts,.tsx' --out-file .build/i18nExtractedMessages/compiled.js",
107 | "translations:runner": "node scripts/translation-runner.js",
108 | "translations": "run-s translations:extract translations:runner",
109 | "generate": "run-p fonticons translations",
110 | "format": "prettier --write '**/*.{js,jsx,ts,tsx,json,md}'",
111 | "lint": "eslint 'src/**/*.{ts,tsx}'",
112 | "type-check": "tsc",
113 | "gatsby-clean": "gatsby clean",
114 | "gatsby-develop": "gatsby develop -H 0.0.0.0",
115 | "gatsby-build": "gatsby build --prefix-paths",
116 | "gatsby-serve": "node scripts/proxied-http-server.js",
117 | "clean:build": "node scripts/fs/rm .build/",
118 | "clean:coverage": "node scripts/fs/rm coverage/",
119 | "clean": "run-p clean:coverage clean:build fonticons:clean gatsby-clean",
120 | "unit-tests": "jest --passWithNoTests",
121 | "check": "run-s type-check lint",
122 | "base": "run-s generate check",
123 | "develop": "run-s base gatsby-develop",
124 | "start": "npm run develop",
125 | "build": "run-s clean base unit-tests gatsby-build",
126 | "serve": "ACTIVE_ENV=development run-s build gatsby-serve",
127 | "gh-deploy": "gh-pages -d public",
128 | "deploy": "PREFIX_PATH=true run-s build gh-deploy",
129 | "upgrade-interactive": "npm-check --update"
130 | },
131 | "lint-staged": {
132 | "src/**/*.{js,jsx,ts,tsx,md,html,css,scss}": "prettier --write",
133 | "src/**/*.{js,jsx,ts,tsx}": "eslint --fix"
134 | },
135 | "husky": {
136 | "hooks": {
137 | "pre-commit": "lint-staged"
138 | }
139 | },
140 | "browserslist": [
141 | ">1%",
142 | "ie>=11",
143 | "iOS >= 9",
144 | "Android >= 4.4",
145 | "not op_mini all"
146 | ],
147 | "gatsby": {
148 | "pathPrefix": "gatsby-typescript-app-starter"
149 | },
150 | "jest-junit": {
151 | "suiteName": "jest tests",
152 | "outputDirectory": "coverage",
153 | "outputName": "junit.xml",
154 | "uniqueOutputName": "false",
155 | "classNameTemplate": "{classname}",
156 | "titleTemplate": "{title}",
157 | "ancestorSeparator": " › ",
158 | "usePathForSuiteName": "true"
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/src/assets/styles/base/_html-elements.scss:
--------------------------------------------------------------------------------
1 | html {
2 | font-size: var(--text-base-size);
3 | font-family: var(--font-family);
4 | text-size-adjust: 100%;
5 | overflow-y: scroll;
6 | -webkit-overflow-scrolling: touch;
7 | }
8 |
9 | body {
10 | @include fontSmooth;
11 | background-color: var(--body-bg, #{color(color-white)});
12 | color: var(--body-color, #{color(color-black)});
13 | font-weight: var(--font-weight-normal);
14 | font-size: var(--text-md);
15 | word-wrap: break-word;
16 | font-kerning: normal;
17 | font-feature-settings: 'kern', 'liga', 'clig', 'calt';
18 | }
19 |
20 | article,
21 | aside,
22 | details,
23 | figcaption,
24 | figure,
25 | footer,
26 | header,
27 | main,
28 | menu,
29 | nav,
30 | section,
31 | summary {
32 | display: block;
33 | }
34 |
35 | audio,
36 | canvas,
37 | progress,
38 | video {
39 | display: inline-block;
40 | }
41 |
42 | audio:not([controls]) {
43 | display: none;
44 | height: 0;
45 | }
46 |
47 | progress {
48 | vertical-align: baseline;
49 | }
50 |
51 | [hidden],
52 | template {
53 | display: none;
54 | }
55 |
56 | a {
57 | background-color: transparent;
58 | text-decoration-skip: objects;
59 | }
60 |
61 | abbr[title] {
62 | border-bottom: none;
63 | text-decoration: underline;
64 | text-decoration: underline dotted;
65 | }
66 |
67 | dfn {
68 | font-style: italic;
69 | }
70 |
71 | mark {
72 | background-color: #{color(color-yellow)};
73 | color: #{color(color-black)};
74 | }
75 |
76 | small {
77 | font-size: 80%;
78 | }
79 |
80 | sub,
81 | sup {
82 | font-size: 75%;
83 | line-height: 0;
84 | position: relative;
85 | vertical-align: baseline;
86 | }
87 | sub {
88 | bottom: -0.25em;
89 | }
90 |
91 | sup {
92 | top: -0.5em;
93 | }
94 |
95 | code,
96 | kbd,
97 | pre,
98 | samp {
99 | font-family: monospace, monospace;
100 | font-size: var(--text-sm);
101 | }
102 |
103 | figure {
104 | margin: 1em 40px;
105 | }
106 |
107 | img {
108 | max-width: 100%;
109 | border-style: none;
110 | }
111 |
112 | svg:not(:root) {
113 | overflow: hidden;
114 | }
115 |
116 | hr {
117 | box-sizing: content-box;
118 | height: 0;
119 | overflow: visible;
120 | }
121 |
122 | button,
123 | input,
124 | optgroup,
125 | select,
126 | textarea {
127 | font: inherit;
128 | margin: 0;
129 | }
130 |
131 | button,
132 | input {
133 | overflow: visible;
134 | }
135 |
136 | button,
137 | select {
138 | text-transform: none;
139 | }
140 |
141 | //----------------------------------------------------------------------------//
142 |
143 | fieldset {
144 | border: 1px solid #{color(color-silver)};
145 | margin: 0 2px;
146 | padding: 0.35em 0.625em 0.75em;
147 | }
148 |
149 | legend {
150 | box-sizing: border-box;
151 | color: inherit;
152 | display: table;
153 | max-width: 100%;
154 | padding: 0;
155 | white-space: normal;
156 | }
157 |
158 | textarea {
159 | overflow: auto;
160 | }
161 |
162 | b,
163 | strong,
164 | dt,
165 | th,
166 | optgroup,
167 | h1,
168 | h2,
169 | h3,
170 | h4,
171 | h5,
172 | h6 {
173 | font-weight: var(--font-weight-bold);
174 | }
175 |
176 | img,
177 | h1,
178 | h2,
179 | h3,
180 | h4,
181 | h5,
182 | h6,
183 | hgroup,
184 | ul,
185 | ol,
186 | dl,
187 | dd,
188 | p,
189 | pre,
190 | figure,
191 | table,
192 | fieldset,
193 | blockquote,
194 | form,
195 | noscript,
196 | iframe,
197 | hr {
198 | margin-bottom: var(--space-md);
199 | }
200 |
201 | h1,
202 | h2,
203 | h3,
204 | h4,
205 | h5,
206 | h6 {
207 | color: inherit;
208 | font-family: var(--font-family);
209 | text-rendering: optimizeLegibility;
210 | line-height: var(--heading-line-height);
211 | }
212 |
213 | h1 {
214 | font-size: var(--text-xxl);
215 | }
216 |
217 | h2 {
218 | font-size: var(--text-xl);
219 | }
220 |
221 | h3 {
222 | font-size: var(--text-lg);
223 | }
224 |
225 | h4 {
226 | font-size: var(--text-md);
227 | }
228 |
229 | h5 {
230 | font-size: var(--text-sm);
231 | }
232 |
233 | h6 {
234 | font-size: var(--text-xs);
235 | }
236 |
237 | ul,
238 | ol {
239 | margin-left: var(--space-md);
240 | list-style-position: outside;
241 | list-style-image: none;
242 | }
243 |
244 | pre {
245 | font-size: 0.85rem;
246 | line-height: 1.42;
247 | background: #{color(color-black, 0.04)};
248 | border-radius: 3px;
249 | overflow: auto;
250 | word-wrap: normal;
251 | padding: var(--space-md);
252 | }
253 |
254 | table {
255 | font-size: var(--text-base-size);
256 | line-height: 1.45;
257 | border-collapse: collapse;
258 | width: 100%;
259 | }
260 |
261 | blockquote {
262 | margin-left: var(--space-md);
263 | margin-right: var(--space-md);
264 | margin-bottom: var(--space-md);
265 | }
266 |
267 | hr {
268 | margin-bottom: calc(var(--space-md) - 1px);
269 | background: #{color(color-black, 0.2)};
270 | border: none;
271 | height: 1px;
272 | }
273 |
274 | address {
275 | margin-bottom: var(--space-md);
276 | }
277 |
278 | li {
279 | margin-bottom: calc(var(--space-md) / 2);
280 | }
281 |
282 | ol li,
283 | ul li {
284 | padding-left: 0;
285 | }
286 |
287 | li > ol,
288 | li > ul {
289 | margin-left: var(--space-md);
290 | margin-bottom: calc(var(--space-md) / 2);
291 | margin-top: calc(var(--space-md) / 2);
292 | }
293 |
294 | blockquote *:last-child,
295 | li *:last-child,
296 | p *:last-child {
297 | margin-bottom: 0;
298 | }
299 |
300 | li > p {
301 | margin-bottom: calc(var(--space-md) / 2);
302 | }
303 |
304 | code,
305 | kbd,
306 | samp {
307 | font-size: var(--text-sm);
308 | line-height: 1.45;
309 | }
310 |
311 | abbr,
312 | acronym,
313 | abbr[title] {
314 | border-bottom: 1px dotted #{color(color-black, 0.5)};
315 | cursor: help;
316 | }
317 |
318 | abbr[title] {
319 | text-decoration: none;
320 | }
321 |
322 | thead {
323 | text-align: left;
324 | }
325 |
326 | td,
327 | th {
328 | --padding-x: calc(var(--space-md) * 1.246);
329 | --padding-y: calc(var(--space-md) * 0.93);
330 | text-align: left;
331 | border-bottom: 1px solid #{color(color-black, 0.12)};
332 | font-feature-settings: 'tnum';
333 | padding-left: var(--padding-x);
334 | padding-right: var(--padding-x);
335 | padding-top: var(--padding-y);
336 | padding-bottom: calc(var(--padding-y) - 1px);
337 | }
338 |
339 | th:first-child,
340 | td:first-child {
341 | padding-left: 0;
342 | }
343 | th:last-child,
344 | td:last-child {
345 | padding-right: 0;
346 | }
347 |
348 | tt,
349 | code {
350 | background-color: #{color(color-black, 0.04)};
351 | border-radius: 3px;
352 | font-family: var(--font-family-code);
353 | padding: 0;
354 | padding-top: 0.2em;
355 | padding-bottom: 0.2em;
356 | }
357 |
358 | pre code {
359 | background: none;
360 | line-height: 1.42;
361 | }
362 |
363 | code:before,
364 | code:after,
365 | tt:before,
366 | tt:after {
367 | letter-spacing: -0.2em;
368 | content: ' ';
369 | }
370 |
371 | pre code:before,
372 | pre code:after,
373 | pre tt:before,
374 | pre tt:after {
375 | content: '';
376 | }
377 |
--------------------------------------------------------------------------------
/src/helpers/__tests__/values.ts:
--------------------------------------------------------------------------------
1 | import * as Values from '../values';
2 | import { arrayFind } from '../arrays';
3 |
4 | //----------------------------------------------------------------------------//
5 |
6 | const REGEX_FLOAT_NUMBER = /^[+-]?\d+([.]\d*)?(e[+-]?\d+)?$/g;
7 |
8 | const REGEX_INT_NUMBER = /^[+-]?\d+(e[+-]?\d+)?$/g;
9 |
10 | const buildDecimalRegex = (fixed: number | string = 1) => new RegExp(`^[+-]?\\d+([.]\\d{${fixed}})$`, 'g');
11 |
12 | const buildRandomIntAsStringRegex = (padStart = 2) =>
13 | new RegExp(`^0{${padStart - 1}}\\d+([.]\\d*)?(e[+-]?\\d+)?$`, `g`);
14 |
15 | //----------------------------------------------------------------------------//
16 |
17 | describe('helpers/values', () => {
18 | describe('getPropertyValue', () => {
19 | it('should get an attribute value from a given object', () => {
20 | const value = 'hello';
21 | const obj = { att: value };
22 | expect(Values.getPropertyValue(obj, 'att')).toEqual(value);
23 | });
24 | });
25 |
26 | describe('setPropertyValue', () => {
27 | it('should set a value to a given object attribute', () => {
28 | const newValue = 'hello world';
29 | let obj = { att: 'hello' };
30 | expect(Values.getPropertyValue(obj, 'att')).not.toEqual(newValue);
31 | obj = Values.setPropertyValue(obj, 'att', newValue);
32 | expect(Values.getPropertyValue(obj, 'att')).toEqual(newValue);
33 | });
34 | });
35 |
36 | describe('randomFloat', () => {
37 | it('should get a random float number', () => {
38 | const min = 1;
39 | const max = 3;
40 | const result = Values.randomFloat(min, max);
41 | expect(`${result}`).toMatch(REGEX_FLOAT_NUMBER);
42 | expect(result >= min && result <= max).toBeTruthy();
43 | });
44 | });
45 |
46 | describe('randomDecimalString', () => {
47 | it('should get a decimal number with fixed 2', () => {
48 | const min = 10;
49 | const max = 30;
50 | const fixed = 2;
51 | const result = Values.randomDecimalString(min, max);
52 | expect(`${result}`).toMatch(buildDecimalRegex(fixed));
53 | const value = Number(result);
54 | expect(value >= min && value <= max).toBeTruthy();
55 | });
56 |
57 | it('should get a decimal number with fixed 3', () => {
58 | const min = 10;
59 | const max = 30;
60 | const fixed = 3;
61 | const result = Values.randomDecimalString(min, max, fixed);
62 | expect(`${result}`).toMatch(buildDecimalRegex(fixed));
63 | const value = Number(result);
64 | expect(value >= min && value <= max).toBeTruthy();
65 | });
66 | });
67 |
68 | describe('randomDecimal', () => {
69 | it('should get a decimal number with fixed {1,2}', () => {
70 | const min = 10;
71 | const max = 30;
72 | const fixed = 2;
73 | const result = Values.randomDecimal(min, max);
74 | expect(`${result}`).toMatch(buildDecimalRegex(`1,${fixed}`));
75 | const value = Number(result);
76 | expect(value >= min && value <= max).toBeTruthy();
77 | });
78 |
79 | it('should get a decimal number with fixed {1,3}', () => {
80 | const min = 10;
81 | const max = 30;
82 | const fixed = 3;
83 | const result = Values.randomDecimal(min, max, fixed);
84 | expect(`${result}`).toMatch(buildDecimalRegex(`1,${fixed}`));
85 | const value = Number(result);
86 | expect(value >= min && value <= max).toBeTruthy();
87 | });
88 | });
89 |
90 | describe('randomInt', () => {
91 | it('should get an integer number', () => {
92 | const min = 1;
93 | const max = 5;
94 | const result = Values.randomInt(min, max);
95 | expect(`${result}`).toMatch(REGEX_INT_NUMBER);
96 | expect(result >= min && result <= max).toBeTruthy();
97 | });
98 | });
99 |
100 | describe('randomIntAsString', () => {
101 | it('should get a random integer string with two 0 left', () => {
102 | const min = 1;
103 | const max = 5;
104 | const padStart = 2;
105 | const result = Values.randomIntAsString(min, max);
106 | expect(`${result}`).toMatch(buildRandomIntAsStringRegex(padStart));
107 | expect(result.length).toEqual(padStart);
108 | const value = Number(result);
109 | expect(value >= min && value <= max).toBeTruthy();
110 | });
111 |
112 | it('should get a random integer string with three 0 left', () => {
113 | const min = 1;
114 | const max = 5;
115 | const padStart = 3;
116 | const result = Values.randomIntAsString(min, max, padStart);
117 | expect(`${result}`).toMatch(buildRandomIntAsStringRegex(padStart));
118 | expect(result.length).toEqual(padStart);
119 | const value = Number(result);
120 | expect(value >= min && value <= max).toBeTruthy();
121 | });
122 | });
123 |
124 | describe('randomBoolean', () => {
125 | it('should be a boolean', () => {
126 | expect(typeof Values.randomBoolean()).toEqual('boolean');
127 | });
128 | });
129 |
130 | describe('randomValue', () => {
131 | it('should return undefined to not object or array as parameter', () => {
132 | expect(Values.randomValue(null)).toBeUndefined();
133 | expect(Values.randomValue(undefined)).toBeUndefined();
134 | expect(Values.randomValue(NaN)).toBeUndefined();
135 | expect(Values.randomValue(123)).toBeUndefined();
136 | expect(Values.randomValue('hello')).toBeUndefined();
137 | });
138 |
139 | describe('in an object values', () => {
140 | it('should get a random value from a given object', () => {
141 | const obj = { att1: 'value 1', att2: 'value 2', att3: 'value 3' };
142 | const objValues: any[] = Object.values(obj);
143 | const randomValue = Values.randomValue(obj);
144 | expect(randomValue).toBeDefined();
145 | expect(objValues.includes(randomValue)).toBeTruthy();
146 | });
147 | });
148 |
149 | describe('in an array values', () => {
150 | it('should get a random value from a plain array', () => {
151 | const values: any[] = ['a', 'b', 'c', 'd', 'e', 'f'];
152 | const randomValue = Values.randomValue(values);
153 | expect(randomValue).toBeDefined();
154 | expect(values.includes(randomValue)).toBeTruthy();
155 | });
156 |
157 | it('should get a random value from an object array', () => {
158 | const values: any[] = [{ att: 'a' }, { att: 'b' }, { att: 'c' }, { att: 'd' }, { att: 'e' }, { att: 'f' }];
159 | const randomValue = Values.randomValue(values);
160 | expect(randomValue).toBeDefined();
161 | expect(arrayFind(values, randomValue, 'att')).toBeDefined();
162 | });
163 | });
164 | });
165 |
166 | describe('calculateAvailablePages', () => {
167 | const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
168 |
169 | it('should use the default pageSize and return 1 as result', () => {
170 | expect(Values.calculateAvailablePages(values)).toEqual(1);
171 | });
172 |
173 | it('should use 5 items as page size ', () => {
174 | expect(Values.calculateAvailablePages(values, 5)).toEqual(2);
175 | });
176 | });
177 |
178 | describe('paginate', () => {
179 | const pageSize = 5;
180 | const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
181 |
182 | it('should have an empty response to an empty data array', () => {
183 | const result = Values.paginate([], 0, pageSize);
184 | expect(result).toBeDefined();
185 | expect(result).toHaveLength(0);
186 | });
187 |
188 | it('should use the default values to get the first page', () => {
189 | const result = Values.paginate(values);
190 | expect(result).toBeDefined();
191 | expect(result).toHaveLength(10);
192 | });
193 |
194 | it('should get the first page', () => {
195 | const result = Values.paginate(values, 0, pageSize);
196 | expect(result).toBeDefined();
197 | expect(result).toHaveLength(pageSize);
198 | });
199 |
200 | it('should get the second page', () => {
201 | const result = Values.paginate(values, 5, pageSize);
202 | expect(result).toBeDefined();
203 | expect(result).toHaveLength(pageSize);
204 | });
205 |
206 | it('should get last 3 items from the array', () => {
207 | const result = Values.paginate(values, 7, pageSize);
208 | expect(result).toBeDefined();
209 | expect(result).toHaveLength(3);
210 | });
211 | });
212 |
213 | describe('randomSetOfData', () => {
214 | const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
215 |
216 | it('should get a random set of data between 1 and 10', () => {
217 | const result = Values.randomSetOfData(values);
218 | expect(result).toBeDefined();
219 | const { length } = result;
220 | expect(length >= 1 && length <= 10).toBeTruthy();
221 | });
222 |
223 | it('should get a random set of data with 5 items', () => {
224 | const pageSize = 5;
225 | const result = Values.randomSetOfData(values, pageSize);
226 | expect(result).toBeDefined();
227 | expect(result).toHaveLength(pageSize);
228 | });
229 | });
230 |
231 | describe('randomAmountOfData', () => {
232 | const values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
233 |
234 | it('should get an empty array to a given empty data array', () => {
235 | const result = Values.randomAmountOfData([], 10);
236 | expect(result).toBeDefined();
237 | expect(result).toHaveLength(0);
238 | });
239 |
240 | it('should get a random amount of data between 1 and 10', () => {
241 | const result = Values.randomAmountOfData(values);
242 | expect(result).toBeDefined();
243 | const { length } = result;
244 | expect(length >= 1 && length <= 10).toBeTruthy();
245 | });
246 |
247 | it('should get a random amount of data with 5 items', () => {
248 | const pageSize = 5;
249 | const result = Values.randomAmountOfData(values, pageSize);
250 | expect(result).toBeDefined();
251 | expect(result).toHaveLength(pageSize);
252 | });
253 | });
254 | });
255 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Gatsby's typescript application starter
8 |
9 |
10 | Kick off your project with this default boilerplate. This starter ships with the main Gatsby configuration files you might need to get up and running blazing fast with the blazing fast app generator for React.
11 |
12 | _Have another more specific idea? You may want to check out our vibrant collection of [official and community-created starters](https://www.gatsbyjs.org/docs/gatsby-starters/)._
13 |
14 | ## 🚀 Quick start
15 |
16 | ### 1. **Create a Gatsby site.**
17 |
18 | Use the Gatsby CLI to create a new site, specifying the default starter.
19 |
20 | ```sh
21 | # create a new Gatsby site using the typescript application starter
22 | gatsby new my-typescript-app-starter https://github.com/erkobridee/gatsby-typescript-app-starter
23 | ```
24 |
25 | Another way to create the a new project without having the Gatsby CLI
26 |
27 | ```sh
28 | npx gatsby new my-typescript-app-starter https://github.com/erkobridee/gatsby-typescript-app-starter
29 | ```
30 |
31 | **TIP:** double check if you need to have the _**.git**_ directory or you should delete it
32 |
33 | ### 2. **Start developing.**
34 |
35 | Navigate into your new site’s directory and start it up.
36 |
37 | ```sh
38 | cd my-typescript-app-starter/
39 | npm i
40 | npm start
41 | ```
42 |
43 | or you can also use: `npm run develop`
44 |
45 | ### 3. **Open the source code and start editing!**
46 |
47 | Your site is now running at `http://localhost:8000`!
48 |
49 | _Note: You'll also see a second link: _`http://localhost:8000/___graphql`_. This is a tool you can use to experiment with querying your data. Learn more about using this tool in the [Gatsby tutorial](https://www.gatsbyjs.org/tutorial/part-five/#introducing-graphiql)._
50 |
51 | Open the `my-typescript-app-starter` directory in your code editor of choice and edit `src/pages/index.js`. Save your changes and the browser will update in real time!
52 |
53 | ## 🧐 What's inside?
54 |
55 | A quick look at the top-level files and directories you'll see in this project. [Gatsby Docs - Gatsby Project Structure](https://www.gatsbyjs.org/docs/gatsby-project-structure/).
56 |
57 | .
58 | ├── node_modules
59 | ├── __mock__
60 | ├── coverage
61 | ├── jest-helpers
62 | ├── scripts
63 | ├── src
64 | ├── static
65 | ├── .babelrc.js
66 | ├── .eslintrc
67 | ├── .gitignore
68 | ├── .prettierrc
69 | ├── gatsby-browser.js
70 | ├── gatsby-config.js
71 | ├── gatsby-node.js
72 | ├── gatsby-ssr.js
73 | ├── gatsby-wrap-root-element.js
74 | ├── jest.config.js
75 | ├── package-lock.json
76 | ├── package.json
77 | ├── tsconfig.json
78 | └── README.md
79 |
80 | - **`/__mock__`** - jest global mocks directory.
81 |
82 | - **`/.build/jest-temp`** - jest unit tests cache directory.
83 |
84 | - **`/.build/.i18nExtractedMessages`** - i18n messages extracted from the code and those messages will be used to generate the JSON translation files.
85 |
86 | - **`/.cache`** - files manages by the Gatsby used to build the output on the **`/public`** folder.
87 |
88 | - **`/node_modules`** - This directory contains all of the modules of code that your project depends on (npm packages) are automatically installed.
89 |
90 | - **`/coverage`** - output of jest unit tests coverage reports.
91 |
92 | - **`/jest-helpers`** - jest setup helpers.
93 |
94 | - **`/public`** - Gatsby output directory, the content of this folder after the build flow must be copied to the server.
95 |
96 | - **`/scripts`** - contains font icons generation, the translations manager and the path prefix helper.
97 |
98 | - **`/src`** - This directory will contain all of the code related to what you will see on the front-end of your site (what you see in the browser) such as your site header or a page template. `src` is a convention for “source code”.
99 |
100 | - **`/static`** - static files that should be copied by the Gastby to the **`/public`** folder during the build flow. [Gatsby Docs - Adding assets outside of the module system](https://www.gatsbyjs.org/docs/static-folder/)
101 |
102 | - **`.gitignore`** - This file tells git which files it should not track / not maintain a version history for.
103 |
104 | - **`.prettierrc`** - This is a configuration file for [Prettier](https://prettier.io/). Prettier is a tool to help keep the formatting of your code consistent.
105 |
106 | - **`gatsby-browser.js`** - This file is where Gatsby expects to find any usage of the [Gatsby browser APIs](https://www.gatsbyjs.org/docs/browser-apis/) (if any). These allow customization/extension of default Gatsby settings affecting the browser.
107 |
108 | - **`gatsby-config.js`** - This is the main configuration file for a Gatsby site. This is where you can specify information about your site (metadata) like the site title and description, which Gatsby plugins you’d like to include, etc. (Check out the [config docs](https://www.gatsbyjs.org/docs/gatsby-config/) for more detail).
109 |
110 | - **`gatsby-node.js`** - This file is where Gatsby expects to find any usage of the [Gatsby Node APIs](https://www.gatsbyjs.org/docs/node-apis/) (if any). These allow customization/extension of default Gatsby settings affecting pieces of the site build process.
111 |
112 | - **`gatsby-ssr.js`** - This file is where Gatsby expects to find any usage of the [Gatsby server-side rendering APIs](https://www.gatsbyjs.org/docs/ssr-apis/) (if any). These allow customization of default Gatsby settings affecting server-side rendering.
113 |
114 | - **`gatsby-wrap-root-element.js`** - used by **`gatsby-browser.js`** as the `export.wrapRootElement`, more details about it please check the [Gatsby browser APIs - wrapRootElement](https://www.gatsbyjs.org/docs/browser-apis/#wrapRootElement). The wrappers used by the application they are defined at the `src/components/Wrapper` folder.
115 |
116 | - **`jest.config.js`** - project jest configs
117 |
118 | - **`package-lock.json`** - (See `package.json` below, first). This is an automatically generated file based on the exact versions of your npm dependencies that were installed for your project. **(You won’t change this file directly).**
119 |
120 | - **`package.json`** - A manifest file for Node.js projects, which includes things like metadata (the project’s name, author, etc). This manifest is how npm knows which packages to install for your project.
121 |
122 | - **`README.md`** - A text file containing useful reference information about your project.
123 |
124 | ### 📂 The `src/` folder - important items to know
125 |
126 | .
127 | └── src
128 | ├── assets
129 | ├── icons
130 | ├── languages
131 | └── styles
132 | ├── components
133 | ├── Layout
134 | ├── SEO
135 | ├── ui
136 | └── Wrapper
137 | ├── data
138 | ├── api
139 | ├── models
140 | └── schemas
141 | ├── domains
142 | ├── helpers
143 | ├── pages
144 | ├── store
145 | ├── helpers
146 | ├── state
147 | ├── definitions.ts
148 | └── index.ts
149 | └── html.js
150 |
151 | - **`/assets`**
152 |
153 | - **`/icons`** - SVG files, each one represents a font icon, the file should be named following the pattern `{icon-name}_icon.svg` and to be easialy used on the project we have the component `FontIcon` located at the `src/components/ui/FontIcon` folder
154 |
155 | - **`/languages`** - translations JSON files generated from the extrated messages from the code, here you should maintain and update the keys entries to each supported language
156 |
157 | - **`/styles`** - global styles definitions and mixins which uses the SASS syntax
158 |
159 | - **`/components`** - stand alone components
160 |
161 | - **`/Layout`** - components that defines the pages layouts used on the application
162 |
163 | - **`/SEO`** - component to easialy manage the HTML header elements, like the page title
164 |
165 | - **`/Wrapper`** - components related to the providers, for example, the redux, internationalization and so one
166 |
167 | - **`/ui`** - small ui components, like the `FontIcon` one
168 |
169 | - **`/data`**
170 |
171 | - **`/schemas`** - defines the data structure used to communicate with the backends/APIs
172 |
173 | - **`/models`** - defines the data structure used inside of the application that receives as input one or more `schemas`.
174 |
175 | - **`/api`** - defines the communication with the backends/APIs using the data `schemas` definitions and transform them to data `models` to be used inside of the application.
176 |
177 | - **`/domains`** - and/or use cases managed by the application, for example: authentication.
178 |
179 | - **`/helpers`** - common code used across the aplication, like small processment code, fetch, react hooks and types/values definitions.
180 |
181 | - **`/pages`** - the components on this directory will define the page content and the file name will define the URL path to the page.
182 |
183 | - **`/store`** - code related to the [Redux](https://redux.js.org/) that follows the [re-ducks pattern](https://www.freecodecamp.org/news/scaling-your-redux-app-with-ducks-6115955638be/), check on the notes section to read more about this pattern.
184 |
185 | - **`/helpers`** - has a create reducer as an object definition instead of need to define as a switch case pattern
186 |
187 | - **`/state`** - the redux states are placed inside of this directory
188 |
189 | - **`/{statename}`** - defines one redux state
190 |
191 | - **`definitions.ts`** - defines the action types strings, the state object interface and the triggered actions interface
192 |
193 | - **`actions.ts`** - defines the actions triggered that uses the types and actions interfaces defined on the `definitions.ts`
194 |
195 | - **`operations.ts`** - defines the operations that manages a async flow or manage a set of actions to be triggered
196 |
197 | - **`reducers.ts`** - defines the redux reducers to the given redux state
198 |
199 | - **`selectors.ts`** - access data from the given redux state
200 |
201 | - **`index.ts`** - expose the redux reducers from the given directory
202 |
203 | - **`definitions.ts`** - defines the Redux root state type definition
204 |
205 | - **`index.ts`** - defines the Redux store
206 |
207 | - **`html.js`** - overwrites the default Gatsby index.html generation [Gatsby Doc - Customizing html.js](https://www.gatsbyjs.org/docs/custom-html/), it was needed to redefine the `viewport` configuration to avoid the zoom in on the iOS devices ([Prevent iOS from zooming in on input fields](https://blog.osmosys.asia/2017/01/05/prevent-ios-from-zooming-in-on-input-fields/))
208 |
209 | ## 💻 Available Commands
210 |
211 | - generate translations using react-intl: `npm run translations`
212 |
213 | - format the code using the prettier: `npm run format`
214 |
215 | - lint the code using the eslint: `npm run lint`
216 |
217 | - check the code (typings and lint): `npm run check`
218 |
219 | - start the development server: `npm run develop` or `npm start` (if you need to read a specific `.env` file, define the `NODE_ENV` before the command)
220 |
221 | - cleanup the temporary directories: `npm run clean`
222 |
223 | - build the production output version: `npm run build` (if you need to read a specific `.env` file, define the `ACTIVE_ENV` before the command)
224 |
225 | - test the production outputed version: `npm run serve`
226 |
227 | - run the jest unit tests and generate the coverage report: `npm run unit-tests`
228 |
229 | ## 🎓 Learning Gatsby
230 |
231 | Looking for more guidance? Full documentation for Gatsby lives [on the website](https://www.gatsbyjs.org/). Here are some places to start:
232 |
233 | - **For most developers, we recommend starting with our [in-depth tutorial for creating a site with Gatsby](https://www.gatsbyjs.org/tutorial/).** It starts with zero assumptions about your level of ability and walks through every step of the process.
234 |
235 | - **To dive straight into code samples, head [to our documentation](https://www.gatsbyjs.org/docs/).** In particular, check out the _Guides_, _API Reference_, and _Advanced Tutorials_ sections in the sidebar.
236 |
237 | - [Upgrade for Minor or Patch Releases](https://www.gatsbyjs.org/docs/upgrade-gatsby-and-dependencies/)
238 |
239 | ## 📝 Notes
240 |
241 | - the project uses sass and the postcss with autoprefixer support and related to this last one there is an open issue related to the warning message showed on the terminal `BrowserslistError: Unknown browser query 'android all'` ( [browserslist issue #382](https://github.com/browserslist/browserslist/issues/382) )
242 |
243 | - added a temporary solution described on the issue comments
244 |
245 | - the project has the redux on it and its follow the re-ducks pattern, read more about on this links [Scaling your Redux App with ducks](https://www.freecodecamp.org/news/scaling-your-redux-app-with-ducks-6115955638be/) and check this [example](https://github.com/FortechRomania/react-redux-complete-example)
246 |
247 | - but, instead of `src/state/` it uses `src/store/` and instead of `src/state/ducks/` it uses `src/store/state/`
248 |
249 | - Gatsby
250 |
251 | - production build issue when has the `window` on the code, check the [gatsbyjs / gatsby - issue 12427](https://github.com/gatsbyjs/gatsby/issues/12427)
252 |
253 | - the project has the configuration to be able to use absolute imports on the code ( [Gatsby Docs - Absolute imports](https://www.gatsbyjs.org/docs/add-custom-webpack-config/#absolute-imports) )
254 |
255 | - [Gatsby Docs - How to use a custom `.babelrc` file](https://www.gatsbyjs.org/docs/babel/#how-to-use-a-custom-babelrc-file)
256 |
257 | - [Gatsby Docs - Environment Variables](www.gatsbyjs.org/docs/environment-variables/)
258 |
259 | - [Building Apps with Gatsby](https://www.gatsbyjs.org/docs/building-apps-with-gatsby/)
260 |
261 | - [[Slides] Benefits of using TypeScript + React + Unit Testing](https://slides.com/erkobridee/benefits-typescript-react-unit-testing/)
262 |
263 | ## 🧪Jest unit tests
264 |
265 | This project has unit tests support that runs on top of the [Jest](https://jestjs.io/) - ( [Docs](https://jestjs.io/docs/en/getting-started) | [API](https://jestjs.io/docs/en/api) ).
266 |
267 | The jest unit tests will be executed right before the gatsby-build on the build flow `npm run build` or you can execute it anytime with the following commands: `npm run unit-tests`, `jest` or if you want to keep it running use `jest --watch`;
268 |
269 | The jest mock files should be placed inside a directory with the given name: `__mock__`, read more about it on the [Jest Docs](https://jestjs.io/docs/en/manual-mocks).
270 |
271 | About the the jest unit tests detection, it will look for the content of directories with the name `__tests__` or files with the sufix `{test|spec}.[jt]sx?`, read more about it on the [Jest Docs](https://jestjs.io/docs/en/configuration#testregex-string--arraystring).
272 |
273 | ### Useful links related to TypeScript + React + Jest
274 |
275 | - [[GitHub] facebook/jest - TypeScript](https://github.com/facebook/jest/tree/master/examples/typescript)
276 |
277 | - [Add Testing | Gatsby.js Doc](https://www.gatsbyjs.org/docs/testing/)
278 |
279 | - [Unit Testing | Gatsby.js Doc](https://www.gatsbyjs.org/docs/unit-testing/)
280 |
281 | ---
282 |
283 | - [Understanding Jest Mocks | Rick Hanlon II - Medium](https://medium.com/@rickhanlonii/understanding-jest-mocks-f0046c68e53c)
284 |
285 | - [Configuring Jest to show code coverage for all of your files](https://joshtronic.com/2017/10/24/configuring-jest-to-show-code-coverage-for-all-of-your-files/)
286 |
287 | - [Best kept Jest secret: Testing only changed files with coverage reports | @stipsan - Medium](https://medium.com/@stipsan/best-kept-jest-secret-testing-only-changed-files-with-coverage-reports-3affc8b4d30f)
288 |
289 | ---
290 |
291 | - [Using Jest with TypeScript](https://basarat.gitbooks.io/typescript/docs/testing/jest.html)
292 |
293 | - [Testing TypeScript with Jest](https://rjzaworski.com/2016/12/testing-typescript-with-jest)
294 |
295 | - [[GitHub] rjz/typescript-react-redux](https://github.com/rjz/typescript-react-redux)
296 |
297 | - [Mocking TypeScript classes with Jest | David Guijarro - Medium](https://medium.com/@davguij/mocking-typescript-classes-with-jest-8ef992170d1d)
298 |
299 | - [[GitHub] davguij/typescript-jest-mocked-classes](https://github.com/davguij/typescript-jest-mocked-classes)
300 |
301 | - [Debugging with TypeScript, Jest, ts-jest and Visual Studio Code](https://medium.com/@mtiller/debugging-with-typescript-jest-ts-jest-and-visual-studio-code-ef9ca8644132)
302 |
303 | - [TypeScript - jest (ts-jest) | typescript Tutorial](https://riptutorial.com/typescript/example/29207/jest--ts-jest-)
304 |
305 | - [[GitHub] kulshekhar/ts-jest](https://github.com/kulshekhar/ts-jest) - TypeScript preprocessor with sourcemap support for Jest
306 |
307 | - [[GitHub] zeit / next.js - Testing with typescript + jest + ts-jest #8663](https://github.com/zeit/next.js/issues/8663) - tip to how to solve the jsx parsing problem
308 |
309 | - [[GitHub] jest-community / jest-junit](https://github.com/jest-community/jest-junit) - A Jest reporter that creates compatible junit xml files
310 |
311 | ---
312 |
313 | - [Testing in React with Jest and Enzyme: An Introduction | @rossbulat - Medium](https://medium.com/@rossbulat/testing-in-react-with-jest-and-enzyme-an-introduction-99ce047dfcf8)
314 |
315 | - [Test Driven Development in React with Jest and Enzyme | @rossbulat - Medium](https://medium.com/@rossbulat/testing-in-react-with-jest-and-enzyme-an-introduction-99ce047dfcf8)
316 |
--------------------------------------------------------------------------------