├── .github
├── ISSUE_TEMPLATE
│ ├── bug.md
│ ├── etc.md
│ └── svg-request.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── .prettierrc
├── .storybook
├── main.js
└── preview.js
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── babel.config.js
├── gatsby-config.js
├── jest.config.js
├── package-lock.json
├── package.json
├── src
├── asset
│ └── favicon.png
├── components
│ ├── templates
│ │ ├── result
│ │ │ ├── ResultTemplate.stories.tsx
│ │ │ ├── index.tsx
│ │ │ └── style.ts
│ │ ├── select
│ │ │ ├── SelectTemplate.stories.tsx
│ │ │ ├── SelectTemplate.test.tsx
│ │ │ ├── index.tsx
│ │ │ └── style.ts
│ │ └── setting
│ │ │ ├── SettingTemplate.stories.tsx
│ │ │ ├── SettingTemplate.test.tsx
│ │ │ ├── index.tsx
│ │ │ └── style.ts
│ └── ui
│ │ ├── CopyButton
│ │ ├── CopyButton.stories.tsx
│ │ ├── CopyButton.test.tsx
│ │ ├── index.tsx
│ │ └── styled.ts
│ │ ├── Icon
│ │ ├── Check.tsx
│ │ ├── Copy.tsx
│ │ ├── Icon.stories.tsx
│ │ └── Question.tsx
│ │ ├── SettingCount
│ │ ├── SettingCount.stories.tsx
│ │ ├── SettingCount.test.tsx
│ │ ├── index.tsx
│ │ └── style.ts
│ │ ├── SettingInterval
│ │ ├── SettingInterval.stories.tsx
│ │ ├── SettingInvertal.test.tsx
│ │ ├── index.tsx
│ │ └── style.ts
│ │ ├── SettingResult
│ │ ├── SettingResult.stories.tsx
│ │ ├── SettingResult.test.tsx
│ │ ├── index.tsx
│ │ └── style.ts
│ │ ├── SettingSize
│ │ ├── SettingSize.stories.tsx
│ │ ├── SettingSize.test.tsx
│ │ ├── index.tsx
│ │ └── style.ts
│ │ └── TechBox
│ │ ├── TechBox.stories.tsx
│ │ ├── TechBox.test.tsx
│ │ ├── index.tsx
│ │ └── style.ts
├── pages
│ ├── 404.tsx
│ └── index.tsx
├── style
│ ├── GlobalStyle.tsx
│ ├── color.ts
│ └── styleWrapper.ts
└── utils
│ ├── SEO.tsx
│ ├── fp.test.ts
│ ├── fp.ts
│ ├── makeHTML.test.ts
│ ├── makeHTML.ts
│ ├── makeMarkdown.test.ts
│ └── makeMarkdown.ts
└── static
├── aws-icon.svg
├── cpp-icon.svg
├── csharp-icon.svg
├── django-icon.svg
├── docker-icon.svg
├── eslint-icon.svg
├── gatsby-icon.svg
├── github-icon.svg
├── graphql-icon.svg
├── java-icon.svg
├── jest-icon.svg
├── js-icon.svg
├── kubernetes-icon.svg
├── map-icon.svg
├── mysql-icon.svg
├── nginx-icon.svg
├── prettier-icon.svg
├── python-icon.svg
├── raspberrypi-icon.svg
├── react-icon.svg
├── redux-icon.svg
├── rescript-icon.svg
├── restapi-icon.svg
├── sass-icon.svg
├── storybook-icon.svg
├── swift-icon.svg
├── testinglibrary-icon.svg
├── ts-icon.svg
└── webpack-icon.svg
/.github/ISSUE_TEMPLATE/bug.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug
3 | about: If you find a bug
4 | title: "[bug] title"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Description
11 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/etc.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: ETC
3 | about: What do you want?
4 | title: "[tag] title"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
20 |
21 | ## Description
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/svg-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: SVG Request
3 | about: Suggest SVG ICON
4 | title: "[feat] icon-name"
5 | labels: svg
6 | assignees: ''
7 |
8 | ---
9 |
10 | **One icon per issue**
11 |
12 | ### Icon
13 | - [ ] icon-name
14 |
15 | ### Icon link
16 | [icon-name](link)
17 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## 제목
2 |
3 | ## 작업 내용
4 |
5 | ## 연관된 Issue번호
6 |
7 | - #이슈번호
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | index.html
2 | .DS_Store
3 |
4 | java-icon
5 | node_modules/
6 | .cache/
7 | public
8 | coverage
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "printWidth": 120,
7 | "arrowParens": "always",
8 | "useTabs": false
9 | }
10 |
--------------------------------------------------------------------------------
/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
3 | addons: [
4 | '@storybook/addon-links',
5 | '@storybook/addon-essentials',
6 | '@storybook/addon-actions',
7 | '@storybook/addon-knobs',
8 | ],
9 | core: {
10 | builder: 'webpack5',
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | import GlobalStyle from '../src/style/GlobalStyle';
2 |
3 | export const parameters = {
4 | actions: { argTypesRegex: '^on[A-Z].*' },
5 | controls: {
6 | matchers: {
7 | color: /(background|color)$/i,
8 | date: /Date$/,
9 | },
10 | },
11 | };
12 |
13 | export const decorators = [
14 | (Story) => (
15 | <>
16 |
17 |
18 | >
19 | ),
20 | ];
21 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # 기여방법!
2 |
3 | ### 🚧 주의
4 |
5 | > 인터넷의 무료 아이콘 사용 시 `라이센스`를 반드시 확인해주세요!
6 |
7 | ## RULE
8 |
9 | 1. Issue 작성
10 | 2. PR
11 |
12 | ### commit rule
13 |
14 | ```
15 | [feat] 영어로 작성해주세요!
16 | ```
17 |
18 | - feat: 새로운 기능/아이콘 추가
19 | - refactor: 코드 수정
20 | - fix: 버그 수정
21 | - test: 테스트 코드
22 | - docs: 문서
23 | - chore: 환경설정
24 |
25 | ## 새로운 아이콘을 추가/수정하고자 할 경우!
26 |
27 | 1. static폴더에 `icon이름-icon.svg` 형식으로 추가
28 | 2. src/pages/index.tsx의 techSrc배열에 `icon이름-icon.svg` 삽입
29 | 3. `npm run develop` 실행 후 8000번 포트에서 테스트!
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 shellboy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TechStack Generator
2 |
3 | > Create a different kind of README with TSG!
4 |
5 | [techstack-generator](https://techstack-generator.vercel.app/) provides animated techstack icons in html and markdown.
6 |
7 |
10 |
11 | ## Contribute
12 |
13 | If you want to add/evolve new icons or find errors in the code, please refer to the [How to Contribute](https://github.com/qkrdmstlr3/techstack-generator/blob/master/CONTRIBUTING.md) documentation!
14 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/react', '@babel/preset-typescript'],
3 | };
4 |
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | siteMetadata: {
3 | siteUrl: 'https://techstack-generator.vercel.app/',
4 | title: 'Techstack Generator',
5 | description: `animated techstack markdown/html generator`,
6 | author: `shellboy`,
7 | },
8 | plugins: [
9 | 'gatsby-plugin-emotion',
10 | 'gatsby-plugin-image',
11 | 'gatsby-plugin-sitemap',
12 | 'gatsby-plugin-sharp',
13 | 'gatsby-transformer-sharp',
14 | {
15 | resolve: `gatsby-plugin-typescript`,
16 | options: {
17 | isTSX: true,
18 | jsxPragma: `jsx`,
19 | allExtensions: true,
20 | },
21 | },
22 | {
23 | resolve: `gatsby-plugin-manifest`,
24 | options: {
25 | name: 'Techstack-Generator',
26 | short_name: 'Techstack-Generator',
27 | start_url: '/',
28 | icon: 'src/asset/favicon.png',
29 | },
30 | },
31 | {
32 | resolve: 'gatsby-plugin-robots-txt',
33 | options: {
34 | host: 'https://techstack-generator.vercel.app/',
35 | sitemap: 'https://techstack-generator.vercel.app/sitemap.xml',
36 | policy: [{ userAgent: '*', allow: '/' }],
37 | },
38 | },
39 | {
40 | resolve: `gatsby-plugin-google-fonts`,
41 | options: {
42 | fonts: [`Roboto`, `source sans pro\:300,400,400i,700`],
43 | display: 'swap',
44 | },
45 | },
46 | ],
47 | };
48 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | moduleFileExtensions: ['ts', 'tsx', 'js'],
3 | setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'],
4 | };
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "techstack-generator",
3 | "version": "1.0.0",
4 | "description": "techstack-generator",
5 | "main": "index.js",
6 | "scripts": {
7 | "develop": "gatsby develop",
8 | "start": "gatsby develop",
9 | "build": "gatsby build",
10 | "serve": "gatsby serve",
11 | "clean": "gatsby clean",
12 | "storybook": "start-storybook -p 6006",
13 | "build-storybook": "build-storybook",
14 | "test": "jest",
15 | "test-coverage": "jest --coverage"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/qkrdmstlr3/techstack-generator.git"
20 | },
21 | "keywords": [],
22 | "author": "",
23 | "license": "ISC",
24 | "bugs": {
25 | "url": "https://github.com/qkrdmstlr3/techstack-generator/issues"
26 | },
27 | "homepage": "https://github.com/qkrdmstlr3/techstack-generator#readme",
28 | "dependencies": {
29 | "@emotion/react": "^11.4.1",
30 | "@emotion/styled": "^11.3.0",
31 | "@fxts/core": "^0.3.2",
32 | "@testing-library/jest-dom": "^5.14.1",
33 | "@testing-library/react": "^12.1.2",
34 | "babel-jest": "^27.2.5",
35 | "gatsby": "^3.13.1",
36 | "gatsby-plugin-emotion": "^6.14.0",
37 | "gatsby-plugin-google-fonts": "^1.0.1",
38 | "gatsby-plugin-image": "^1.14.1",
39 | "gatsby-plugin-manifest": "^3.14.0",
40 | "gatsby-plugin-robots-txt": "^1.6.13",
41 | "gatsby-plugin-sharp": "^3.14.1",
42 | "gatsby-plugin-sitemap": "^4.10.0",
43 | "gatsby-plugin-typescript": "^3.14.0",
44 | "gatsby-source-filesystem": "^3.14.0",
45 | "gatsby-transformer-sharp": "^3.14.0",
46 | "react": "^17.0.1",
47 | "react-dom": "^17.0.1",
48 | "react-helmet": "^6.1.0",
49 | "react-markdown": "^7.0.1",
50 | "rehype-raw": "^6.1.0",
51 | "ts-jest": "^27.0.5"
52 | },
53 | "devDependencies": {
54 | "@babel/core": "^7.15.8",
55 | "@babel/preset-env": "^7.15.8",
56 | "@babel/preset-react": "^7.14.5",
57 | "@babel/preset-typescript": "^7.15.0",
58 | "@storybook/addon-actions": "^6.3.10",
59 | "@storybook/addon-essentials": "^6.3.10",
60 | "@storybook/addon-knobs": "^6.4.0",
61 | "@storybook/addon-links": "^6.3.10",
62 | "@storybook/builder-webpack5": "^6.3.10",
63 | "@storybook/manager-webpack5": "^6.3.10",
64 | "@storybook/react": "^6.3.10",
65 | "@types/jest": "^27.0.2",
66 | "@types/node": "^16.10.3",
67 | "@types/react": "^17.0.27",
68 | "@types/react-dom": "^17.0.9",
69 | "@types/react-helmet": "^6.1.4",
70 | "babel-loader": "^8.2.2",
71 | "jest": "^27.2.5",
72 | "typescript": "^4.4.3"
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/asset/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qkrdmstlr3/techstack-generator/b0ea9dfeca9d801d638c6137a0808901a0b93bd6/src/asset/favicon.png
--------------------------------------------------------------------------------
/src/components/templates/result/ResultTemplate.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withKnobs, object } from '@storybook/addon-knobs';
3 | import { action } from '@storybook/addon-actions';
4 | import ResultTemplate from '.';
5 | import { SettingType } from '../setting/index';
6 | import { ResultType } from '../../ui/SettingResult/index';
7 | import { TechType } from '../select/index';
8 |
9 | export default {
10 | title: 'Templates/resultTemplate',
11 | component: ResultTemplate,
12 | decorators: [withKnobs],
13 | };
14 |
15 | const techs: TechType[] = new Array(20).fill(0).map((_, index) => ({
16 | src: 'https://techstack-generator.vercel.app/js-icon.svg',
17 | selected: true,
18 | number: index + 1,
19 | }));
20 |
21 | const setting: SettingType = {
22 | count: 5,
23 | interval: '50',
24 | size: '100',
25 | results: [ResultType.html],
26 | };
27 |
28 | const techsKnob = object('techs', techs);
29 | const settingKnob = object('setting', setting);
30 | const changeAction = action('onchange');
31 |
32 | export const resultTemplate = () => (
33 |
34 | );
35 |
--------------------------------------------------------------------------------
/src/components/templates/result/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactMarkdown from 'react-markdown';
3 | import rehypeRaw from 'rehype-raw';
4 | import { SettingType } from '../setting/index';
5 | import { TechType } from '../select/index';
6 | import { ResultType } from '../../ui/SettingResult';
7 | import * as Style from './style';
8 |
9 | import makeHTML from '../../../utils/makeHTML';
10 | import makeMarkdown from '../../../utils/makeMarkdown';
11 | import CopyButton from '../../ui/CopyButton';
12 | import { pipe, filter, sort } from '@fxts/core';
13 |
14 | interface ResultTemplateProps {
15 | setting: SettingType;
16 | techs: TechType[];
17 | changeTemplate: () => void;
18 | }
19 |
20 | function ResultTemplate({ setting, techs, changeTemplate }: ResultTemplateProps) {
21 | const selectedTechs = pipe(
22 | techs,
23 | filter((tech) => tech.selected),
24 | sort((a, b) => a.number - b.number)
25 | );
26 | const resultMarkdown = makeMarkdown({ setting, selectedTechs, forView: false });
27 | const resultMarkdownForView = makeMarkdown({ setting, selectedTechs, forView: true });
28 | const resultHTML = makeHTML({ setting, selectedTechs, forView: false });
29 | const resultHTMLForView = makeHTML({ setting, selectedTechs, forView: true });
30 |
31 | // TODO: 중복 제거
32 | return (
33 |
34 | TSG
35 | animated TechStack Generator
36 | {setting.results.includes(ResultType.html) && (
37 | <>
38 |
39 |
40 | HTML RESULT
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {resultHTML}
49 |
50 | >
51 | )}
52 | {setting.results.includes(ResultType.markdown) && (
53 | <>
54 |
55 |
56 | MARKDOWN RESULT
57 |
58 |
59 |
60 |
61 | {resultMarkdownForView}
62 |
63 |
64 |
65 |
66 | {resultMarkdown}
67 |
68 | >
69 | )}
70 | BACK
71 |
72 | made by{' '}
73 |
74 | shellboy
75 |
76 |
77 |
78 | );
79 | }
80 |
81 | export default ResultTemplate;
82 |
--------------------------------------------------------------------------------
/src/components/templates/result/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../../style/color';
3 |
4 | export const Container = styled.main`
5 | margin: 0 auto;
6 | margin-top: 70px;
7 | padding-bottom: 150px;
8 | width: 510px;
9 | color: ${color.white};
10 | user-select: none;
11 | `;
12 |
13 | export const Title = styled.h1`
14 | margin: 0;
15 | margin-bottom: 5px;
16 | font-size: 72px;
17 | font-weight: 700;
18 | text-align: center;
19 | `;
20 |
21 | export const Description = styled.p`
22 | margin: 0;
23 | font-size: 36px;
24 | text-align: center;
25 | `;
26 |
27 | export const CategoryWrapper = styled.div`
28 | margin: 20px 0 40px 0;
29 | `;
30 |
31 | export const CategoryTitleWrapper = styled.div`
32 | display: flex;
33 | align-items: center;
34 | `;
35 |
36 | export const CategoryTitle = styled.h2`
37 | margin: 13px 15px 10px 0;
38 | color: ${color.green};
39 | font-size: 24px;
40 | font-weight: 700;
41 | `;
42 |
43 | export const CategoryResultContentWrapper = styled.div`
44 | position: relative;
45 | border-radius: 4px;
46 | display: flex;
47 | justify-content: center;
48 | `;
49 |
50 | export const CategoryResultContent = styled.div`
51 | width: fit-content;
52 | border-radius: 4px;
53 | background-color: ${color.white};
54 | `;
55 |
56 | export const CategoryContent = styled.div`
57 | padding: 15px 10px;
58 | height: 100px;
59 | overflow: hidden;
60 |
61 | font-size: 13px;
62 | overflow-y: scroll;
63 | border-radius: 4px;
64 | color: ${color.black};
65 | background-color: ${color.white};
66 |
67 | > div {
68 | width: fit-content;
69 | }
70 | `;
71 |
72 | export const BackButton = styled.button`
73 | width: 100%;
74 | height: 68px;
75 |
76 | border-radius: 4px;
77 | font-size: 32px;
78 | background-color: ${color.white};
79 | transition: all 0.5s;
80 | outline: none;
81 | border: none;
82 |
83 | :active {
84 | transform: scale(0.95);
85 | }
86 | `;
87 |
88 | export const Copyright = styled.p`
89 | margin-top: 10px;
90 | text-align: center;
91 | font-size: 18px;
92 | word-spacing: 10px;
93 | letter-spacing: 3px;
94 | `;
95 |
96 | export const ATag = styled.a`
97 | border-bottom: 2px solid ${color.green};
98 | `;
99 |
--------------------------------------------------------------------------------
/src/components/templates/select/SelectTemplate.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withKnobs, object } from '@storybook/addon-knobs';
3 | import { action } from '@storybook/addon-actions';
4 | import SelectTemplate, { TechType } from '.';
5 |
6 | export default {
7 | title: 'Templates/selectTemplate',
8 | component: SelectTemplate,
9 | decorators: [withKnobs],
10 | };
11 |
12 | const techs: TechType[] = new Array(20).fill(0).map((_, index) => ({
13 | src: 'https://techstack-generator.vercel.app/js-icon.svg',
14 | selected: false,
15 | number: index + 1,
16 | }));
17 |
18 | const techsKnob = object('techs', techs);
19 | const clickAction = action('onclick');
20 | const changeAction = action('onchange');
21 |
22 | export const selectTemplate = () => (
23 |
24 | );
25 |
--------------------------------------------------------------------------------
/src/components/templates/select/SelectTemplate.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from 'react';
5 | import { render, waitFor, fireEvent } from '@testing-library/react';
6 | import SelectTemplate, { TechType } from '.';
7 |
8 | const selectTechMock = jest.fn();
9 | const changeTemplate = jest.fn();
10 | const techs: TechType[] = new Array(20).fill(0).map((_, index) => ({
11 | src: String(index),
12 | selected: false,
13 | number: 0,
14 | }));
15 |
16 | describe('Component/Template/SelectTemplate', () => {
17 | it('rendering test', async () => {
18 | const selectComponent = render(
19 |
20 | );
21 | await waitFor(() => {
22 | selectComponent.getByText('TSG');
23 | selectComponent.getByText('animated TechStack Generator');
24 | selectComponent.getByText('SETTING');
25 | selectComponent.getByText('shellboy');
26 | const techboxs = selectComponent.getAllByLabelText('techbox');
27 | expect(techboxs).toHaveLength(techs.length);
28 | });
29 | });
30 |
31 | it('click setting button', async () => {
32 | const selectComponent = render(
33 |
34 | );
35 | await waitFor(() => {
36 | const settingButton = selectComponent.getByText('SETTING');
37 | fireEvent.click(settingButton);
38 |
39 | expect(changeTemplate).toHaveBeenCalledTimes(1);
40 | });
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/src/components/templates/select/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { map, pipe, toArray } from '@fxts/core';
3 | import TechBox from '../../ui/TechBox';
4 | import * as Style from './style';
5 |
6 | export interface TechType {
7 | selected: boolean;
8 | src: string;
9 | number: number;
10 | }
11 |
12 | interface SelectTemplateProps {
13 | techs: TechType[];
14 | selectTech: (selectedTech: TechType) => void;
15 | changeTemplate: () => void;
16 | }
17 |
18 | function SelectTemplate({ techs, selectTech, changeTemplate }: SelectTemplateProps) {
19 | return (
20 |
21 | TSG
22 | animated TechStack Generator
23 |
24 | {pipe(
25 | techs,
26 | map((tech) => (
27 |
28 |
29 |
30 | )),
31 | toArray
32 | )}
33 |
34 | SETTING
35 |
36 | made by{' '}
37 |
38 | shellboy
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | export default SelectTemplate;
46 |
--------------------------------------------------------------------------------
/src/components/templates/select/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../../style/color';
3 |
4 | export const Container = styled.main`
5 | margin: 0 auto;
6 | margin-top: 70px;
7 | padding-bottom: 150px;
8 | width: 510px;
9 | color: ${color.white};
10 | user-select: none;
11 | `;
12 |
13 | export const Title = styled.h1`
14 | margin: 0;
15 | margin-bottom: 5px;
16 | font-size: 72px;
17 | font-weight: 700;
18 | text-align: center;
19 | `;
20 |
21 | export const Description = styled.p`
22 | margin: 0;
23 | font-size: 36px;
24 | text-align: center;
25 | `;
26 |
27 | export const TechStackList = styled.ul`
28 | padding: 0;
29 | display: flex;
30 | flex-wrap: wrap;
31 | list-style: none;
32 | gap: 36px;
33 | `;
34 |
35 | export const TechStackItem = styled.li`
36 | display: flex;
37 | justify-content: center;
38 | align-items: center;
39 | width: 100px;
40 | height: 100px;
41 | `;
42 |
43 | export const SettingButton = styled.button`
44 | width: 100%;
45 | height: 68px;
46 | margin: 30px 0 0 0;
47 |
48 | border: none;
49 | border-radius: 4px;
50 | font-size: 32px;
51 | background-color: ${color.white};
52 | transition: all 0.5s;
53 | outline: none;
54 |
55 | :active {
56 | transform: scale(0.95);
57 | }
58 | `;
59 |
60 | export const Copyright = styled.p`
61 | margin-top: 10px;
62 | text-align: center;
63 | font-size: 18px;
64 | word-spacing: 10px;
65 | letter-spacing: 3px;
66 | `;
67 |
68 | export const ATag = styled.a`
69 | border-bottom: 2px solid ${color.green};
70 | `;
71 |
--------------------------------------------------------------------------------
/src/components/templates/setting/SettingTemplate.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withKnobs, object, number } from '@storybook/addon-knobs';
3 | import { action } from '@storybook/addon-actions';
4 | import SettingTemplate, { SettingType } from '.';
5 | import { ResultType } from '../../ui/SettingResult';
6 |
7 | export default {
8 | title: 'Templates/settingTemplate',
9 | component: SettingTemplate,
10 | decorators: [withKnobs],
11 | };
12 |
13 | const setting: SettingType = {
14 | count: 5,
15 | interval: '50',
16 | size: '100',
17 | results: [ResultType.html],
18 | };
19 |
20 | const settingKnob = object('setting', setting);
21 | const selectedCountKnob = number('selectedCount', 10);
22 | const changeSettingAction = action('changeAction');
23 | const changeTemplateAction = action('changeTemplate');
24 |
25 | export const settingTemplate = () => (
26 |
32 | );
33 |
--------------------------------------------------------------------------------
/src/components/templates/setting/SettingTemplate.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from 'react';
5 | import { render, waitFor, fireEvent } from '@testing-library/react';
6 | import SelectTemplate from '.';
7 | import { TechType } from '../select';
8 | import { SettingType } from '../setting';
9 | import { ResultType } from '../../ui/SettingResult';
10 |
11 | const changeTemplate = jest.fn();
12 | const changeSetting = jest.fn();
13 | const setting: SettingType = {
14 | count: 1,
15 | interval: '50',
16 | size: '100',
17 | results: [ResultType.html, ResultType.markdown],
18 | };
19 |
20 | describe('Component/Template/SelectTemplate', () => {
21 | beforeEach(() => {
22 | jest.clearAllMocks();
23 | });
24 |
25 | it('rendering test', async () => {
26 | const settingComponent = render(
27 |
33 | );
34 |
35 | await waitFor(() => {
36 | settingComponent.getByText('TSG');
37 | settingComponent.getByText('animated TechStack Generator');
38 | settingComponent.getByText('BACK');
39 | settingComponent.getByText('shellboy');
40 |
41 | settingComponent.getByText('SIZE');
42 | settingComponent.getByText('COUNT IN ROW');
43 | settingComponent.getByText('ICON INTERVAL');
44 | settingComponent.getAllByText('RESULT');
45 | });
46 | });
47 |
48 | describe('change test', () => {
49 | it('change size', async () => {
50 | const settingComponent = render(
51 |
57 | );
58 |
59 | await waitFor(() => {
60 | const sizeInput = settingComponent.getByLabelText('size-range');
61 | fireEvent.change(sizeInput, { target: { value: 70 } });
62 | expect(changeSetting).toHaveBeenCalledTimes(1);
63 | });
64 | });
65 |
66 | it('change count', async () => {
67 | const settingComponent = render(
68 |
74 | );
75 |
76 | await waitFor(() => {
77 | const plusButton = settingComponent.getByText('+');
78 | fireEvent.click(plusButton);
79 | expect(changeSetting).toHaveBeenCalledTimes(1);
80 | });
81 | });
82 |
83 | it('change count', async () => {
84 | const settingComponent = render(
85 |
91 | );
92 |
93 | await waitFor(() => {
94 | const minusButton = settingComponent.getByText('-');
95 | fireEvent.click(minusButton);
96 | expect(changeSetting).toHaveBeenCalledTimes(1);
97 | });
98 | });
99 |
100 | it('change interval', async () => {
101 | const settingComponent = render(
102 |
108 | );
109 |
110 | await waitFor(() => {
111 | const intervalInput = settingComponent.getByLabelText('interval-range');
112 | fireEvent.change(intervalInput, { target: { value: 70 } });
113 | expect(changeSetting).toHaveBeenCalledTimes(1);
114 | });
115 | });
116 |
117 | it('change result', async () => {
118 | const settingComponent = render(
119 |
125 | );
126 |
127 | await waitFor(() => {
128 | const htmlCheckbox = settingComponent.getByLabelText('html-checkbox');
129 | fireEvent.click(htmlCheckbox);
130 | expect(changeSetting).toHaveBeenCalledTimes(1);
131 | });
132 | });
133 | });
134 |
135 | describe('change template test', () => {
136 | it('click backButton', async () => {
137 | const settingComponent = render(
138 |
144 | );
145 |
146 | await waitFor(() => {
147 | const backButton = settingComponent.getByText('BACK');
148 | fireEvent.click(backButton);
149 | expect(changeTemplate).toHaveBeenCalledTimes(1);
150 | });
151 | });
152 |
153 | it('click resultButton', async () => {
154 | const settingComponent = render(
155 |
161 | );
162 |
163 | await waitFor(() => {
164 | const resultButton = settingComponent.getAllByText('RESULT');
165 | fireEvent.click(resultButton[1]);
166 | expect(changeTemplate).toHaveBeenCalledTimes(1);
167 | });
168 | });
169 | });
170 | });
171 |
--------------------------------------------------------------------------------
/src/components/templates/setting/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as Style from './style';
3 | import { ResultType } from '../../ui/SettingResult/index';
4 | import SettingSize from '../../ui/SettingSize';
5 | import SettingCount from '../../ui/SettingCount';
6 | import SettingInterval from '../../ui/SettingInterval';
7 | import SettingResult from '../../ui/SettingResult';
8 |
9 | export interface SettingType {
10 | size: string;
11 | count: number;
12 | interval: string;
13 | results: ResultType[];
14 | }
15 |
16 | interface SettingTemplateProps {
17 | setting: SettingType;
18 | selectedCount: number;
19 | changeSetting: (key: string, value: string | number | ResultType[]) => void;
20 | changeTemplate: (isBackButton?: boolean) => void;
21 | }
22 |
23 | function SettingTemplate({ setting, selectedCount, changeSetting, changeTemplate }: SettingTemplateProps) {
24 | const changeSize = (size: string) => {
25 | changeSetting('size', size);
26 | };
27 |
28 | const changeCount = (count: number) => {
29 | changeSetting('count', count < 1 ? 1 : Math.min(count, selectedCount || 1));
30 | };
31 |
32 | const changeInterval = (interval: string) => {
33 | changeSetting('interval', interval);
34 | };
35 |
36 | const changeResults = (results: ResultType[]) => {
37 | changeSetting('results', results);
38 | };
39 |
40 | return (
41 |
42 | TSG
43 | animated TechStack Generator
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | changeTemplate(true)}>BACK
52 | changeTemplate()}>RESULT
53 |
54 |
55 | made by{' '}
56 |
57 | shellboy
58 |
59 |
60 |
61 | );
62 | }
63 |
64 | export default SettingTemplate;
65 |
--------------------------------------------------------------------------------
/src/components/templates/setting/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../../style/color';
3 |
4 | export const Container = styled.main`
5 | margin: 0 auto;
6 | margin-top: 70px;
7 | padding-bottom: 150px;
8 | width: 510px;
9 | color: ${color.white};
10 | user-select: none;
11 | `;
12 |
13 | export const Title = styled.h1`
14 | margin: 0;
15 | margin-bottom: 5px;
16 | font-size: 72px;
17 | font-weight: 700;
18 | text-align: center;
19 | `;
20 |
21 | export const Description = styled.p`
22 | margin: 0;
23 | font-size: 36px;
24 | text-align: center;
25 | `;
26 |
27 | export const ContentWrapper = styled.div`
28 | margin: 40px 0;
29 | height: 450px;
30 | display: flex;
31 | flex-direction: column;
32 | justify-content: space-between;
33 | `;
34 |
35 | export const ButtonWrapper = styled.div`
36 | margin: 0 5px;
37 | display: flex;
38 | justify-content: space-between;
39 | align-items: center;
40 | `;
41 |
42 | export const BackButton = styled.button`
43 | width: 48%;
44 | height: 68px;
45 |
46 | border-radius: 4px;
47 | font-size: 32px;
48 | background-color: ${color.white};
49 | transition: all 0.5s;
50 | outline: none;
51 | border: none;
52 |
53 | :active {
54 | transform: scale(0.95);
55 | }
56 | `;
57 |
58 | export const ResultButton = styled.button`
59 | width: 48%;
60 | height: 68px;
61 |
62 | border-radius: 4px;
63 | font-size: 32px;
64 | color: ${color.white};
65 | background-color: ${color.green};
66 | transition: all 0.5s;
67 | outline: none;
68 | border: none;
69 |
70 | :active {
71 | transform: scale(0.95);
72 | }
73 | `;
74 |
75 | export const Copyright = styled.p`
76 | margin-top: 10px;
77 | text-align: center;
78 | font-size: 18px;
79 | word-spacing: 10px;
80 | letter-spacing: 3px;
81 | `;
82 |
83 | export const ATag = styled.a`
84 | border-bottom: 2px solid ${color.green};
85 | `;
86 |
--------------------------------------------------------------------------------
/src/components/ui/CopyButton/CopyButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withKnobs, text } from '@storybook/addon-knobs';
3 | import CopyButton from '.';
4 |
5 | export default {
6 | title: 'Component/CopyButton',
7 | component: CopyButton,
8 | decorators: [withKnobs],
9 | };
10 |
11 | const textToCopy = text('text', 'text');
12 | export const copyButton = (): React.ReactElement => ;
13 |
--------------------------------------------------------------------------------
/src/components/ui/CopyButton/CopyButton.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from 'react';
5 | import { render, waitFor, fireEvent, act } from '@testing-library/react';
6 | import CopyButton from '.';
7 |
8 | Object.assign(navigator, {
9 | clipboard: {
10 | writeText: () => {},
11 | },
12 | });
13 |
14 | describe('Component/UI/CopyButton', () => {
15 | const text = 'copy text';
16 |
17 | it('clipboard test', async () => {
18 | jest.spyOn(navigator.clipboard, 'writeText');
19 | const copyButtonComponent = render();
20 |
21 | await waitFor(() => {
22 | const copyButton = copyButtonComponent.getByLabelText('copy-button');
23 | fireEvent.click(copyButton);
24 | expect(navigator.clipboard.writeText).toHaveBeenCalledWith(text);
25 | });
26 | });
27 |
28 | it('timeout test', async () => {
29 | jest.useFakeTimers();
30 | jest.spyOn(global, 'setTimeout');
31 | const copyButtonComponent = render();
32 |
33 | await waitFor(() => {
34 | const copyButton = copyButtonComponent.getByLabelText('copy-button');
35 | fireEvent.click(copyButton);
36 | expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 700);
37 | });
38 | });
39 | });
40 |
--------------------------------------------------------------------------------
/src/components/ui/CopyButton/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Copy from '../Icon/Copy';
3 | import Check from '../Icon/Check';
4 | import * as Style from './styled';
5 |
6 | type IconType = 'copy' | 'check';
7 |
8 | interface CopyButtonProps {
9 | text: string;
10 | }
11 |
12 | function CopyButton({ text }: CopyButtonProps) {
13 | const [iconType, setIconType] = useState('copy');
14 |
15 | const copyToClipboard = (value: string) => {
16 | if (iconType !== 'copy') return;
17 | navigator.clipboard.writeText(value);
18 |
19 | setIconType('check');
20 | setTimeout(() => {
21 | setIconType('copy');
22 | }, 700);
23 | };
24 |
25 | return (
26 | copyToClipboard(text)}>
27 | {iconType === 'copy' ? : }
28 |
29 | );
30 | }
31 |
32 | export default CopyButton;
33 |
--------------------------------------------------------------------------------
/src/components/ui/CopyButton/styled.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const IconWrapper = styled.div`
4 | cursor: pointer;
5 | display: flex;
6 | align-items: flex-end;
7 | `;
8 |
--------------------------------------------------------------------------------
/src/components/ui/Icon/Check.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Check() {
4 | return (
5 |
8 | );
9 | }
10 |
11 | export default Check;
12 |
--------------------------------------------------------------------------------
/src/components/ui/Icon/Copy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Copy() {
4 | return (
5 |
8 | );
9 | }
10 |
11 | export default Copy;
12 |
--------------------------------------------------------------------------------
/src/components/ui/Icon/Icon.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Check from './Check';
3 | import Copy from './Copy';
4 | import Question from './Question';
5 |
6 | export default {
7 | title: 'Component/Icon',
8 | component: [Check, Copy, Question],
9 | };
10 |
11 | export const check = () => ;
12 | export const copy = () => ;
13 | export const question = () => ;
14 |
--------------------------------------------------------------------------------
/src/components/ui/Icon/Question.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Question() {
4 | return (
5 |
11 | );
12 | }
13 |
14 | export default Question;
15 |
--------------------------------------------------------------------------------
/src/components/ui/SettingCount/SettingCount.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import SettingCount from '.';
3 |
4 | export default {
5 | title: 'Component/SettingCount',
6 | component: SettingCount,
7 | };
8 |
9 | export const settingCount = (): React.ReactElement => {
10 | const [count, setCount] = useState(0);
11 |
12 | const changeCount = (count: number) => {
13 | setCount(count);
14 | };
15 |
16 | return ;
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/ui/SettingCount/SettingCount.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from 'react';
5 | import { render, waitFor, fireEvent } from '@testing-library/react';
6 | import SettingCount from '.';
7 |
8 | const changeCountMock = jest.fn();
9 |
10 | describe('Component/UI/SettingCount', () => {
11 | beforeEach(() => {
12 | jest.clearAllMocks();
13 | });
14 |
15 | it('rendering test', async () => {
16 | const settingCountComponent = render();
17 |
18 | await waitFor(() => {
19 | settingCountComponent.getByText(5);
20 | settingCountComponent.getByText('-');
21 | settingCountComponent.getByText('+');
22 | });
23 | });
24 |
25 | it('plus count', async () => {
26 | const settingCountComponent = render();
27 |
28 | await waitFor(() => {
29 | const plusButton = settingCountComponent.getByText('+');
30 | fireEvent.click(plusButton);
31 |
32 | expect(changeCountMock).toHaveBeenCalledTimes(1);
33 | });
34 | });
35 |
36 | it('minus count', async () => {
37 | const settingCountComponent = render();
38 |
39 | await waitFor(() => {
40 | const minusButton = settingCountComponent.getByText('-');
41 | fireEvent.click(minusButton);
42 |
43 | expect(changeCountMock).toHaveBeenCalledTimes(1);
44 | });
45 | });
46 | });
47 |
--------------------------------------------------------------------------------
/src/components/ui/SettingCount/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as Style from './style';
3 |
4 | interface SettingCountProps {
5 | count: number;
6 | changeCount: (count: number) => void;
7 | }
8 |
9 | function SettingCount({ count, changeCount }: SettingCountProps) {
10 | return (
11 |
12 | COUNT IN ROW
13 |
14 | changeCount(count - 1)}>-
15 | {count}
16 | changeCount(count + 1)}>+
17 |
18 |
19 | );
20 | }
21 |
22 | export default SettingCount;
23 |
--------------------------------------------------------------------------------
/src/components/ui/SettingCount/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../../style/color';
3 |
4 | export const Container = styled.div`
5 | position: relative;
6 | user-select: none;
7 | `;
8 |
9 | export const Title = styled.h2`
10 | position: absolute;
11 | margin: 0;
12 | top: 0;
13 | left: 0;
14 |
15 | color: ${color.white};
16 | font-size: 24px;
17 | font-weight: 700;
18 | `;
19 |
20 | export const Wrapper = styled.div`
21 | display: flex;
22 | justify-content: space-between;
23 | margin: 0 auto;
24 | padding-top: 20px;
25 | width: 170px;
26 | color: ${color.white};
27 | `;
28 |
29 | export const Opperation = styled.button`
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 |
34 | background-color: ${color.none};
35 | border: none;
36 | color: ${color.white};
37 | font-size: 48px;
38 | cursor: pointer;
39 | `;
40 |
41 | export const Number = styled.span`
42 | display: flex;
43 | justify-content: center;
44 | width: 30px;
45 | font-size: 50px;
46 | `;
47 |
--------------------------------------------------------------------------------
/src/components/ui/SettingInterval/SettingInterval.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import SettingInterval from '.';
3 |
4 | export default {
5 | title: 'Component/SettingInterval',
6 | component: SettingInterval,
7 | };
8 |
9 | export const settingInterval = (): React.ReactElement => {
10 | const [interval, setInterval] = useState('50');
11 | const changeInterval = (interval: string) => {
12 | setInterval(interval);
13 | };
14 |
15 | return ;
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/ui/SettingInterval/SettingInvertal.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from 'react';
5 | import { render, waitFor, fireEvent } from '@testing-library/react';
6 | import SettingInterval from '.';
7 |
8 | const changeIntervalMock = jest.fn();
9 |
10 | describe('Component/UI/SettingInterval', () => {
11 | it('rendering test', async () => {
12 | const settingIntervalComponent = render();
13 |
14 | await waitFor(() => {
15 | const exampleLine = settingIntervalComponent.getByLabelText('interval-line');
16 | const rangeInput = settingIntervalComponent.getByLabelText('interval-range');
17 |
18 | expect(rangeInput).toHaveAttribute('value', '50');
19 | expect(exampleLine).toHaveStyle({
20 | width: '50px',
21 | });
22 | });
23 | });
24 |
25 | it('change range', async () => {
26 | const settingIntervalComponent = render();
27 |
28 | await waitFor(() => {
29 | const rangeInput = settingIntervalComponent.getByLabelText('interval-range');
30 | fireEvent.change(rangeInput, { target: { value: 68, valueAsNumber: 68 } });
31 |
32 | expect(changeIntervalMock).toHaveBeenCalledTimes(1);
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/components/ui/SettingInterval/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as Style from './style';
3 | import Question from '../Icon/Question';
4 |
5 | interface SettingIntervalProps {
6 | interval: string;
7 | changeInterval: (interval: string) => void;
8 | }
9 |
10 | function SettingInterval({ interval, changeInterval }: SettingIntervalProps) {
11 | return (
12 |
13 | ICON INTERVAL
14 |
15 |
16 | It doesn't apply to MARKDOWN
17 |
18 | changeInterval(event.target.value)}
26 | />
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | export default SettingInterval;
37 |
--------------------------------------------------------------------------------
/src/components/ui/SettingInterval/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../../style/color';
3 |
4 | interface LineProps {
5 | width: string;
6 | }
7 |
8 | export const Container = styled.div`
9 | position: relative;
10 | display: flex;
11 | align-items: center;
12 | height: 100px;
13 | user-select: none;
14 | `;
15 |
16 | export const IconWrapper = styled.header`
17 | position: absolute;
18 | top: 5px;
19 | left: 185px;
20 |
21 | &:hover div {
22 | visibility: visible;
23 | }
24 | `;
25 |
26 | export const ToolTip = styled.div`
27 | position: absolute;
28 | bottom: 5px;
29 | left: 20px;
30 | width: fit-content;
31 | visibility: hidden;
32 | padding: 5px;
33 | width: 160px;
34 | background-color: ${color.black};
35 | color: ${color.green};
36 | text-align: center;
37 | border-radius: 6px;
38 | font-size: 18px;
39 | `;
40 |
41 | export const Title = styled.h2`
42 | position: absolute;
43 | margin: 0;
44 | top: 0;
45 | left: 0;
46 |
47 | color: ${color.white};
48 | font-size: 24px;
49 | font-weight: 700;
50 | `;
51 |
52 | export const ExampleBox = styled.div`
53 | width: 120px;
54 | height: 120px;
55 | display: flex;
56 | justify-content: center;
57 | align-items: center;
58 | `;
59 |
60 | export const Bar = styled.div`
61 | width: 8px;
62 | height: 60px;
63 | border-radius: 4px;
64 | background-color: ${color.white};
65 | `;
66 |
67 | export const Line = styled.div`
68 | width: ${(props) => props.width}px;
69 | border: 1px dashed ${color.white};
70 | `;
71 |
72 | export const Range = styled.input`
73 | width: 350px;
74 | height: 4px;
75 | margin-right: 60px;
76 | -webkit-appearance: none;
77 |
78 | background-color: ${color.white};
79 | outline: none;
80 | border: none;
81 |
82 | &::-webkit-slider-thumb {
83 | -webkit-appearance: none;
84 | width: 20px;
85 | height: 20px;
86 | border-radius: 50%;
87 | background-color: ${color.green};
88 | cursor: pointer;
89 | }
90 | &::-moz-range-thumb {
91 | width: 20px;
92 | height: 20px;
93 | border-radius: 50%;
94 | background-color: ${color.green};
95 | cursor: pointer;
96 | border: none;
97 | }
98 | `;
99 |
--------------------------------------------------------------------------------
/src/components/ui/SettingResult/SettingResult.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import SettingResult, { ResultType } from '.';
3 |
4 | export default {
5 | title: 'Component/SettingResult',
6 | component: SettingResult,
7 | };
8 |
9 | export const settingResult = (): React.ReactElement => {
10 | const [results, setResults] = useState([]);
11 |
12 | const changeResults = (results: ResultType[]) => {
13 | setResults(results);
14 | };
15 |
16 | return ;
17 | };
18 |
--------------------------------------------------------------------------------
/src/components/ui/SettingResult/SettingResult.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from 'react';
5 | import { render, waitFor, fireEvent } from '@testing-library/react';
6 | import SettingResult, { ResultType } from '.';
7 |
8 | const changeResultslMock = jest.fn();
9 |
10 | describe('Component/UI/SettingResult', () => {
11 | describe('rendering test', () => {
12 | it('check none', async () => {
13 | const settingResultComponent = render();
14 |
15 | await waitFor(() => {
16 | settingResultComponent.getByText('HTML');
17 | settingResultComponent.getByText('MARKDOWN');
18 | });
19 | });
20 |
21 | it('check html', async () => {
22 | const settingResultComponent = render(
23 |
24 | );
25 |
26 | await waitFor(() => {
27 | settingResultComponent.getByText('HTML');
28 | settingResultComponent.getByText('MARKDOWN');
29 | settingResultComponent.getByLabelText('html-check');
30 | const markdownCheck = settingResultComponent.queryByLabelText('markdown-check');
31 | expect(markdownCheck).toBeNull();
32 | });
33 | });
34 |
35 | it('check markdown', async () => {
36 | const settingResultComponent = render(
37 |
38 | );
39 |
40 | await waitFor(() => {
41 | settingResultComponent.getByText('HTML');
42 | settingResultComponent.getByText('MARKDOWN');
43 | settingResultComponent.getByLabelText('markdown-check');
44 | const htmlCheck = settingResultComponent.queryByLabelText('html-check');
45 | expect(htmlCheck).toBeNull();
46 | });
47 | });
48 | });
49 |
50 | it('click checkbox able', async () => {
51 | const settingResultComponent = render();
52 |
53 | await waitFor(() => {
54 | const htmlCheckbox = settingResultComponent.getByLabelText('html-checkbox');
55 | const markdownCheckbox = settingResultComponent.getByLabelText('markdown-checkbox');
56 | fireEvent.click(htmlCheckbox);
57 | fireEvent.click(markdownCheckbox);
58 |
59 | expect(changeResultslMock).toHaveBeenCalledTimes(2);
60 | });
61 | });
62 |
63 | it('click checkbox disable', async () => {
64 | const settingResultComponent = render(
65 |
66 | );
67 |
68 | await waitFor(() => {
69 | const htmlCheckbox = settingResultComponent.getByLabelText('html-checkbox');
70 | fireEvent.click(htmlCheckbox);
71 |
72 | expect(changeResultslMock).toHaveBeenCalledWith([]);
73 | });
74 | });
75 | });
76 |
--------------------------------------------------------------------------------
/src/components/ui/SettingResult/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as Style from './style';
3 | import { reverse } from '../../../utils/fp';
4 |
5 | export enum ResultType {
6 | html = 'HTML',
7 | markdown = 'MARKDOWN',
8 | }
9 |
10 | interface SettingResultProps {
11 | results: ResultType[];
12 | changeResults: (results: ResultType[]) => void;
13 | }
14 |
15 | function SettingResult({ results, changeResults }: SettingResultProps) {
16 | const clickCheckBox = (selectedResult: ResultType) => {
17 | changeResults(reverse(selectedResult, results));
18 | };
19 |
20 | return (
21 |
22 | RESULT
23 | clickCheckBox(ResultType.html)}>
24 |
25 | {results.find((result) => result === ResultType.html) && }
26 |
27 | {ResultType.html}
28 |
29 | clickCheckBox(ResultType.markdown)}>
30 |
31 | {results.find((result) => result === ResultType.markdown) && }
32 |
33 | {ResultType.markdown}
34 |
35 |
36 | );
37 | }
38 |
39 | export default SettingResult;
40 |
--------------------------------------------------------------------------------
/src/components/ui/SettingResult/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../../style/color';
3 |
4 | export const Container = styled.div`
5 | padding-top: 20px;
6 | padding-left: 15px;
7 | position: relative;
8 | display: flex;
9 | align-items: center;
10 | height: 100px;
11 | user-select: none;
12 | `;
13 |
14 | export const Title = styled.h2`
15 | position: absolute;
16 | margin: 0;
17 | top: 0;
18 | left: 0;
19 |
20 | color: ${color.white};
21 | font-size: 24px;
22 | font-weight: 700;
23 | `;
24 |
25 | export const CheckBoxWrapper = styled.div`
26 | display: flex;
27 | align-items: center;
28 | margin-right: 70px;
29 | cursor: pointer;
30 | `;
31 |
32 | export const CheckBox = styled.div`
33 | width: 30px;
34 | height: 30px;
35 | display: flex;
36 | justify-content: center;
37 | align-items: center;
38 | margin-right: 20px;
39 |
40 | background-color: ${color.white};
41 | border-radius: 4px;
42 | `;
43 |
44 | export const Check = styled.div`
45 | width: 20px;
46 | height: 20px;
47 |
48 | background-color: ${color.green};
49 | border-radius: 4px;
50 | `;
51 |
52 | export const CheckTitle = styled.span`
53 | color: ${color.white};
54 | font-size: 24px;
55 | `;
56 |
--------------------------------------------------------------------------------
/src/components/ui/SettingSize/SettingSize.stories.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import SettingSize from '.';
3 |
4 | export default {
5 | title: 'Component/SettingSize',
6 | component: SettingSize,
7 | };
8 |
9 | export const settingSize = (): React.ReactElement => {
10 | const [size, setSize] = useState('65');
11 | const changeSize = (size: string) => {
12 | setSize(size);
13 | };
14 |
15 | return ;
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/ui/SettingSize/SettingSize.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from 'react';
5 | import { render, waitFor, fireEvent } from '@testing-library/react';
6 | import SettingSize from '.';
7 |
8 | const changeSizeMock = jest.fn();
9 |
10 | describe('Component/UI/SettingSize', () => {
11 | it('rendering test', async () => {
12 | const settingSizeComponent = render();
13 |
14 | await waitFor(() => {
15 | const exampleBox = settingSizeComponent.getByLabelText('example-box');
16 | const rangeInput = settingSizeComponent.getByLabelText('size-range');
17 |
18 | expect(rangeInput).toHaveAttribute('value', '65');
19 | expect(exampleBox).toHaveStyle({
20 | width: '65px',
21 | height: '65px',
22 | });
23 | });
24 | });
25 |
26 | it('change range', async () => {
27 | const settingSizeComponent = render();
28 |
29 | await waitFor(() => {
30 | const rangeInput = settingSizeComponent.getByLabelText('size-range');
31 | fireEvent.change(rangeInput, { target: { value: 68, valueAsNumber: 68 } });
32 |
33 | expect(changeSizeMock).toHaveBeenCalled();
34 | });
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/components/ui/SettingSize/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import * as Style from './style';
3 |
4 | interface SettingSizeProps {
5 | size: string;
6 | changeSize: (size: string) => void;
7 | }
8 |
9 | function SettingSize({ size, changeSize }: SettingSizeProps) {
10 | return (
11 |
12 | SIZE
13 | changeSize(event.target.value)}
21 | />
22 |
23 |
24 | );
25 | }
26 |
27 | export default SettingSize;
28 |
--------------------------------------------------------------------------------
/src/components/ui/SettingSize/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../../style/color';
3 |
4 | interface ExampleBoxProps {
5 | size: string;
6 | }
7 |
8 | export const Container = styled.div`
9 | position: relative;
10 | display: flex;
11 | align-items: center;
12 | height: 100px;
13 | user-select: none;
14 | `;
15 |
16 | export const Title = styled.h2`
17 | position: absolute;
18 | margin: 0;
19 | top: 0;
20 | left: 0;
21 |
22 | color: ${color.white};
23 | font-size: 24px;
24 | font-weight: 700;
25 | `;
26 |
27 | export const ExampleBox = styled.div`
28 | width: ${(props) => `${props.size}px`};
29 | height: ${(props) => `${props.size}px`};
30 | background-color: ${color.white};
31 | border-radius: 4px;
32 | `;
33 |
34 | export const Range = styled.input`
35 | width: 350px;
36 | height: 4px;
37 | margin-right: 60px;
38 | -webkit-appearance: none;
39 |
40 | background-color: ${color.white};
41 | outline: none;
42 | border: none;
43 |
44 | &::-webkit-slider-thumb {
45 | -webkit-appearance: none;
46 | width: 20px;
47 | height: 20px;
48 | border-radius: 50%;
49 | background-color: ${color.green};
50 | cursor: pointer;
51 | }
52 | &::-moz-range-thumb {
53 | width: 20px;
54 | height: 20px;
55 | border-radius: 50%;
56 | background-color: ${color.green};
57 | cursor: pointer;
58 | border: none;
59 | }
60 | `;
61 |
--------------------------------------------------------------------------------
/src/components/ui/TechBox/TechBox.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { action } from '@storybook/addon-actions';
3 | import TechBox from '.';
4 | import StyleWrapper from '../../../style/styleWrapper';
5 | import color from '../../../style/color';
6 |
7 | export default {
8 | title: 'Component/TechBox',
9 | component: TechBox,
10 | };
11 |
12 | const clickAction = action('onclick');
13 |
14 | const unselectedTech = { src: 'https://techstack-generator.vercel.app/js-icon.svg', selected: false, number: 0 };
15 | const selectedTech = { src: 'https://techstack-generator.vercel.app/js-icon.svg', selected: true, number: 1 };
16 | export const techBox = (): React.ReactElement => (
17 |
18 |
19 | not selected
20 |
21 |
22 |
23 | selected
24 |
25 |
26 |
27 | );
28 |
--------------------------------------------------------------------------------
/src/components/ui/TechBox/TechBox.test.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment jsdom
3 | */
4 | import React from 'react';
5 | import { render, waitFor, fireEvent } from '@testing-library/react';
6 | import TechBox from '.';
7 |
8 | const selectTechMock = jest.fn();
9 | const selectedTech = { src: 'src', selected: true, number: 1 };
10 | const unselectedTech = { src: 'src', selected: false, number: 0 };
11 |
12 | describe('Component/UI/TechBox', () => {
13 | it('rendering test(selected)', async () => {
14 | const techBoxComponent = render();
15 |
16 | await waitFor(() => {
17 | techBoxComponent.getByText('1');
18 | });
19 | });
20 |
21 | it('rendering test(unselected)', async () => {
22 | const techBoxComponent = render();
23 |
24 | await waitFor(() => {
25 | const submitButton = techBoxComponent.queryByText('1');
26 | expect(submitButton).toBeNull();
27 | });
28 | });
29 |
30 | it('click techbox ', async () => {
31 | const techBoxComponent = render();
32 |
33 | await waitFor(() => {
34 | const techBox = techBoxComponent.getByLabelText('techbox');
35 | fireEvent.click(techBox);
36 |
37 | expect(selectTechMock).toHaveBeenCalledTimes(1);
38 | });
39 | });
40 | });
41 |
--------------------------------------------------------------------------------
/src/components/ui/TechBox/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { TechType } from '../../templates/select';
3 | import * as Style from './style';
4 |
5 | interface TechBoxProps {
6 | tech: TechType;
7 | clickTech: (tech: TechType) => void;
8 | }
9 |
10 | function TechBox({ tech, clickTech }: TechBoxProps) {
11 | return (
12 | clickTech(tech)}>
13 |
14 |
15 |
16 | {tech.selected && (
17 |
18 | {tech.number}
19 |
20 | )}
21 |
22 | );
23 | }
24 |
25 | export default TechBox;
26 |
--------------------------------------------------------------------------------
/src/components/ui/TechBox/style.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 | import color from '../../../style/color';
3 |
4 | interface SelectedProps {
5 | selected: boolean;
6 | }
7 |
8 | export const Wrapper = styled.div`
9 | position: relative;
10 | width: ${(props) => (props.selected ? '85px' : '100px')};
11 | height: ${(props) => (props.selected ? '85px' : '100px')};
12 | transition: 0.5s all;
13 | cursor: pointer;
14 |
15 | :active {
16 | transform: ${(props) => (props.selected ? 'scale(0.9)' : 'scale(1.1)')};
17 | }
18 | `;
19 |
20 | export const ImageWrapper = styled.div`
21 | width: 100%;
22 | height: 100%;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | background-color: ${color.white};
27 | border-radius: 4px;
28 | opacity: ${(props) => (props.selected ? '0.7' : '1')};
29 | `;
30 |
31 | export const Image = styled.img`
32 | width: 95%;
33 | height: 95%;
34 | `;
35 |
36 | export const NumberWrapper = styled.div`
37 | position: absolute;
38 | top: 50%;
39 | left: 50%;
40 | transform: translate(-50%, -50%);
41 | width: 50px;
42 | height: 50px;
43 | display: flex;
44 | justify-content: center;
45 | align-items: center;
46 | opacity: 0.7;
47 |
48 | font-size: 24px;
49 | border-radius: 25px;
50 | background-color: ${color.green};
51 | `;
52 |
53 | export const Number = styled.span`
54 | color: ${color.white};
55 | font-weight: bold;
56 | user-select: none;
57 | `;
58 |
--------------------------------------------------------------------------------
/src/pages/404.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const NotFoundPage = () => {
4 | return (
5 |
6 | Not found
7 |
8 | );
9 | };
10 |
11 | export default NotFoundPage;
12 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import SelectTemplate, { TechType } from '../components/templates/select';
3 | import SettingTemplate, { SettingType } from '../components/templates/setting/index';
4 | import GlobalStyle from '../style/GlobalStyle';
5 | import { ResultType } from '../components/ui/SettingResult/index';
6 | import ResultTemplate from '../components/templates/result/index';
7 | import SEO from '../utils/SEO';
8 |
9 | enum TemplateType {
10 | select = 'select',
11 | setting = 'setting',
12 | result = 'result',
13 | }
14 |
15 | const techsSrc: string[] = [
16 | 'js-icon.svg',
17 | 'ts-icon.svg',
18 | 'rescript-icon.svg',
19 | 'cpp-icon.svg',
20 | 'csharp-icon.svg',
21 | 'swift-icon.svg',
22 | 'react-icon.svg',
23 | 'redux-icon.svg',
24 | 'gatsby-icon.svg',
25 | 'sass-icon.svg',
26 | 'storybook-icon.svg',
27 | 'webpack-icon.svg',
28 | 'eslint-icon.svg',
29 | 'prettier-icon.svg',
30 | 'jest-icon.svg',
31 | 'testinglibrary-icon.svg',
32 | 'python-icon.svg',
33 | 'django-icon.svg',
34 | 'graphql-icon.svg',
35 | 'restapi-icon.svg',
36 | 'github-icon.svg',
37 | 'docker-icon.svg',
38 | 'kubernetes-icon.svg',
39 | 'aws-icon.svg',
40 | 'nginx-icon.svg',
41 | 'mysql-icon.svg',
42 | 'raspberrypi-icon.svg',
43 | 'java-icon.svg',
44 | ];
45 |
46 | const initTechs = (): TechType[] => {
47 | return techsSrc.map((src) => ({ src, selected: false, number: 0 }));
48 | };
49 |
50 | const IndexPage = () => {
51 | const [techs, setTechs] = useState(initTechs());
52 | const [selectedCount, setSelectedCount] = useState(0);
53 | const [currentTemplate, setCurrentTemplate] = useState(TemplateType.select);
54 | const [setting, setSetting] = useState({
55 | size: '65',
56 | count: 1,
57 | interval: '50',
58 | results: [ResultType.markdown],
59 | });
60 |
61 | useEffect(() => {
62 | if (selectedCount && setting.count - 1 == selectedCount) {
63 | setSetting({ ...setting, count: selectedCount });
64 | }
65 | }, [selectedCount]);
66 |
67 | const selectTech = (selectedTech: TechType) => {
68 | const updatedTechs = techs.map((tech) => {
69 | if (tech.number > selectedTech.number && selectedTech.number !== 0) {
70 | tech.number -= 1;
71 | }
72 | return tech;
73 | });
74 |
75 | const selectedTechInList = updatedTechs.find((tech) => tech.src === selectedTech.src);
76 | selectedTechInList.selected = !selectedTechInList.selected;
77 | selectedTechInList.number = selectedTechInList.selected ? selectedCount + 1 : 0;
78 |
79 | setSelectedCount(selectedTechInList.selected ? selectedCount + 1 : selectedCount - 1);
80 | setTechs(updatedTechs);
81 | };
82 |
83 | const changeTemplate = (isBackButton?: boolean) => {
84 | switch (currentTemplate) {
85 | case TemplateType.select:
86 | setCurrentTemplate(TemplateType.setting);
87 | break;
88 | case TemplateType.setting:
89 | if (isBackButton) setCurrentTemplate(TemplateType.select);
90 | else setCurrentTemplate(TemplateType.result);
91 | break;
92 | case TemplateType.result:
93 | setCurrentTemplate(TemplateType.setting);
94 | break;
95 | }
96 | };
97 |
98 | const changeSetting = (key: string, value: string | number | ResultType[]) => {
99 | setSetting({ ...setting, [key]: value });
100 | };
101 |
102 | return (
103 | <>
104 |
105 |
106 | {(() => {
107 | switch (currentTemplate) {
108 | case TemplateType.select:
109 | return ;
110 | case TemplateType.result:
111 | return ;
112 | case TemplateType.setting:
113 | return (
114 |
120 | );
121 | }
122 | })()}
123 | >
124 | );
125 | };
126 |
127 | export default IndexPage;
128 |
--------------------------------------------------------------------------------
/src/style/GlobalStyle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Global, css } from '@emotion/react';
3 | import color from './color';
4 |
5 | function GlobalStyle(): React.ReactElement {
6 | return (
7 |
31 | );
32 | }
33 |
34 | export default GlobalStyle;
35 |
--------------------------------------------------------------------------------
/src/style/color.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | black: '#000',
3 | darkgray: '#253237',
4 | white: '#fff',
5 | green: '#00f703',
6 | none: 'transparent',
7 | };
8 |
--------------------------------------------------------------------------------
/src/style/styleWrapper.ts:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled';
2 |
3 | export const StyleWrapper = styled.div`
4 | .description {
5 | margin-bottom: 0.5rem;
6 | font-size: 2rem;
7 | }
8 | & > div + div {
9 | margin-top: 2rem;
10 | }
11 | `;
12 |
13 | export default StyleWrapper;
14 |
--------------------------------------------------------------------------------
/src/utils/SEO.tsx:
--------------------------------------------------------------------------------
1 | import { Helmet } from 'react-helmet';
2 |
3 | import React from 'react';
4 |
5 | function SEO() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | export default SEO;
20 |
--------------------------------------------------------------------------------
/src/utils/fp.test.ts:
--------------------------------------------------------------------------------
1 | import { reverse } from './fp';
2 |
3 | describe('Util/fp/reverse', () => {
4 | it('if exist', () => {
5 | const array = ['1', '2', '3', '4'];
6 | const result = reverse('4', array);
7 |
8 | expect(result).toHaveLength(3);
9 | expect(result).not.toContain('4');
10 | });
11 |
12 | it('if not exist', () => {
13 | const array = ['1', '2', '3'];
14 | const result = reverse('4', array);
15 |
16 | expect(result).toHaveLength(4);
17 | expect(result).toContain('4');
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/src/utils/fp.ts:
--------------------------------------------------------------------------------
1 | // if not exist, add
2 | // if exist, remove
3 | export const reverse = (a: A, b: A[]): A[] => {
4 | if (b.includes(a)) return b.filter((c) => c != a);
5 | return [...b, a];
6 | };
7 |
--------------------------------------------------------------------------------
/src/utils/makeHTML.test.ts:
--------------------------------------------------------------------------------
1 | import { TechType } from '../components/templates/select';
2 | import makeHTML from './makeHTML';
3 | import { SettingType } from '../components/templates/setting/index';
4 | import { ResultType } from '../components/ui/SettingResult';
5 |
6 | const selectedTechs: TechType[] = new Array(21).fill(0).map((_, index) => ({
7 | src: String(index),
8 | selected: true,
9 | number: 0,
10 | }));
11 | const setting: SettingType = {
12 | count: 10,
13 | interval: '50',
14 | size: '50',
15 | results: [ResultType.html],
16 | };
17 |
18 | describe('Utils/makeHTML', () => {
19 | it('forview is true', () => {
20 | const html = makeHTML({ setting, selectedTechs, forView: true });
21 |
22 | expect(html).not.toBeNull();
23 | expect(html).not.toContain('https://techstack-generator.vercel.app/');
24 | });
25 |
26 | it('forview is false', () => {
27 | const html = makeHTML({ setting, selectedTechs, forView: false });
28 |
29 | expect(html).not.toBeNull();
30 | expect(html).toContain('https://techstack-generator.vercel.app/');
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/utils/makeHTML.ts:
--------------------------------------------------------------------------------
1 | import { pipe, map, join, chunk, toArray } from '@fxts/core';
2 | import { TechType } from '../components/templates/select';
3 | import { SettingType } from '../components/templates/setting';
4 |
5 | interface MakeTemplateProps {
6 | setting: SettingType;
7 | selectedTechs: TechType[];
8 | forView: boolean;
9 | }
10 |
11 | const makeImgTag = (forView: boolean, techSrc: string, setting: SettingType, order: number, colNumber: number) => {
12 | const isLastInRow = (order + 1) % setting.count === 0;
13 | const isLastRow = order + 1 > (colNumber - 1) * setting.count;
14 | const marginRight = isLastInRow ? 0 : setting.interval;
15 | const marginBottom = isLastRow ? 0 : setting.interval;
16 | const imgStyle = `style="width: ${setting.size}px; height: ${setting.size}px; margin-right: ${marginRight}px; margin-bottom: ${marginBottom}px;"`;
17 |
18 | const url = forView ? '' : 'https://techstack-generator.vercel.app/';
19 | return `
`;
20 | };
21 |
22 | const makeDivTag = (imgTagList: string[]) => {
23 | return `${join('', imgTagList)}
`;
24 | };
25 |
26 | export default ({ setting, selectedTechs, forView }: MakeTemplateProps) => {
27 | const colNumber = Math.ceil(selectedTechs.length / setting.count);
28 | const markdown = pipe(
29 | Object.entries(selectedTechs),
30 | map(([order, tech]) => makeImgTag(forView, tech.src, setting, Number(order), colNumber)),
31 | chunk(setting.count),
32 | map((imgTagList) => makeDivTag(imgTagList)),
33 | join('')
34 | );
35 |
36 | return markdown;
37 | };
38 |
--------------------------------------------------------------------------------
/src/utils/makeMarkdown.test.ts:
--------------------------------------------------------------------------------
1 | import { TechType } from '../components/templates/select';
2 | import makeMarkdown from './makeMarkdown';
3 | import { SettingType } from '../components/templates/setting/index';
4 | import { ResultType } from '../components/ui/SettingResult';
5 |
6 | const selectedTechs: TechType[] = new Array(21).fill(0).map((_, index) => ({
7 | src: String(index),
8 | selected: true,
9 | number: 0,
10 | }));
11 | const setting: SettingType = {
12 | count: 10,
13 | interval: '50',
14 | size: '50',
15 | results: [ResultType.html],
16 | };
17 |
18 | describe('Utils/makeMarkdown', () => {
19 | it('forview is true', () => {
20 | const markdown = makeMarkdown({ setting, selectedTechs, forView: true });
21 |
22 | expect(markdown).not.toBeNull();
23 | expect(markdown).not.toContain('https://techstack-generator.vercel.app/');
24 | });
25 |
26 | it('forview is false', () => {
27 | const markdown = makeMarkdown({ setting, selectedTechs, forView: false });
28 |
29 | expect(markdown).not.toBeNull();
30 | expect(markdown).toContain('https://techstack-generator.vercel.app/');
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/utils/makeMarkdown.ts:
--------------------------------------------------------------------------------
1 | import { pipe, map, join, chunk } from '@fxts/core';
2 | import { TechType } from '../components/templates/select';
3 | import { SettingType } from '../components/templates/setting';
4 |
5 | interface MakeTemplateProps {
6 | setting: SettingType;
7 | selectedTechs: TechType[];
8 | forView: boolean;
9 | }
10 |
11 | const makeImgTag = (forView: boolean, techSrc: string, size: string) => {
12 | const url = forView ? '' : 'https://techstack-generator.vercel.app/';
13 | return `
`;
14 | };
15 |
16 | const makeDivTag = (imgTagList: string[]) => {
17 | return `${join('', imgTagList)}
`;
18 | };
19 |
20 | export default ({ setting, selectedTechs, forView }: MakeTemplateProps) => {
21 | const markdown = pipe(
22 | selectedTechs,
23 | map((tech) => makeImgTag(forView, tech.src, setting.size)),
24 | chunk(setting.count),
25 | map((imgTagList) => makeDivTag(imgTagList)),
26 | join('')
27 | );
28 | return markdown;
29 | };
30 |
--------------------------------------------------------------------------------
/static/aws-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/cpp-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/csharp-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/django-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/docker-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/eslint-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
32 |
--------------------------------------------------------------------------------
/static/gatsby-icon.svg:
--------------------------------------------------------------------------------
1 |
45 |
--------------------------------------------------------------------------------
/static/github-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/graphql-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/java-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/jest-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
31 |
--------------------------------------------------------------------------------
/static/js-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/kubernetes-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
105 |
--------------------------------------------------------------------------------
/static/map-icon.svg:
--------------------------------------------------------------------------------
1 |
83 |
--------------------------------------------------------------------------------
/static/mysql-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/nginx-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/prettier-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/python-icon.svg:
--------------------------------------------------------------------------------
1 |
57 |
--------------------------------------------------------------------------------
/static/raspberrypi-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/react-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/redux-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
40 |
--------------------------------------------------------------------------------
/static/rescript-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/restapi-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
68 |
--------------------------------------------------------------------------------
/static/sass-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/storybook-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/swift-icon.svg:
--------------------------------------------------------------------------------
1 |
89 |
--------------------------------------------------------------------------------
/static/testinglibrary-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/ts-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/webpack-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------