├── .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 |
8 |
iconiconiconiconiconiconicon
iconiconiconiconiconiconicon
iconiconiconiconiconiconicon
iconiconiconiconiconiconicon
9 |
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 | 6 | 7 | 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 | 6 | 7 | 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 | 6 | 10 | 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 `icon`; 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 `icon`; 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 | 3 | 4 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Amazon AWS 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /static/cpp-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /static/csharp-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /static/django-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 61 | 62 | Django 63 | 64 | 65 | -------------------------------------------------------------------------------- /static/docker-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 72 | 73 | -------------------------------------------------------------------------------- /static/eslint-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /static/gatsby-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /static/github-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /static/graphql-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /static/java-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /static/jest-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 26 | 27 | Jest 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /static/js-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 98 | JS 99 | -------------------------------------------------------------------------------- /static/kubernetes-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 42 | 43 | 66 | 68 | 69 | 71 | image/svg+xml 72 | 74 | 75 | 76 | 77 | 78 | 83 | 85 | 93 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /static/map-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 65 | 66 | 67 | 79 | 80 | B 81 | 82 | 83 | -------------------------------------------------------------------------------- /static/mysql-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /static/nginx-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /static/prettier-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /static/python-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 50 | 53 | 56 | 57 | -------------------------------------------------------------------------------- /static/raspberrypi-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | raspberry pi 85 | -------------------------------------------------------------------------------- /static/react-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /static/redux-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /static/rescript-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /static/restapi-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 61 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /static/sass-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sass 4 | 5 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /static/storybook-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /static/swift-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 63 | 64 | 65 | swift 66 | 67 | 68 | 70 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /static/testinglibrary-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 27 | 28 | 32 | 33 | 34 | 35 | 36 | 37 | Testing Library 38 | 39 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /static/ts-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 73 | 74 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 98 | TS 99 | -------------------------------------------------------------------------------- /static/webpack-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 35 | 36 | 37 | 38 | --------------------------------------------------------------------------------