├── .eslintignore ├── public ├── terminal.jpg ├── screenshot_laptop.png ├── screenshot_phone.png ├── site.webmanifest └── browserconfig.xml ├── SECURITY.md ├── .prettierignore ├── .prettierrc ├── tsconfig.node.json ├── src ├── main.tsx ├── components │ ├── commands │ │ ├── GeneralOutput.tsx │ │ ├── Clear.tsx │ │ ├── History.tsx │ │ ├── Email.tsx │ │ ├── About.tsx │ │ ├── Help.tsx │ │ ├── Education.tsx │ │ ├── Socials.tsx │ │ ├── Projects.tsx │ │ └── Welcome.tsx │ ├── styles │ │ ├── Themes.styled.tsx │ │ ├── TerminalInfo.styled.tsx │ │ ├── Education.styled.tsx │ │ ├── Output.styled.tsx │ │ ├── Work.styled.tsx │ │ ├── About.styled.tsx │ │ ├── Projects.styled.tsx │ │ ├── Help.styled.tsx │ │ ├── Terminal.styled.tsx │ │ ├── Welcome.styled.tsx │ │ ├── GlobalStyle.tsx │ │ └── themes.ts │ ├── TermInfo.tsx │ ├── Usage.tsx │ ├── Output.tsx │ └── Terminal.tsx ├── utils │ ├── storage.ts │ ├── test-utils.tsx │ └── funcs.ts ├── vite-env.d.ts ├── hooks │ └── useTheme.ts └── App.tsx ├── vite.config.ts ├── .gitignore ├── tsconfig.json ├── .github ├── pull_request_template.md ├── workflows │ └── greetings.yml └── ISSUE_TEMPLATE │ └── bug.yml ├── .eslintrc.json ├── LICENSE ├── index.html ├── package.json ├── CONTRIBUTING.md ├── README.md └── CODE_OF_CONDUCT.md /.eslintignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml -------------------------------------------------------------------------------- /public/terminal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zemerik/Terminal/HEAD/public/terminal.jpg -------------------------------------------------------------------------------- /public/screenshot_laptop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zemerik/Terminal/HEAD/public/screenshot_laptop.png -------------------------------------------------------------------------------- /public/screenshot_phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zemerik/Terminal/HEAD/public/screenshot_phone.png -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | # Security Policy 3 | 4 | ## Reporting a Vulnerability 5 | 6 | > To Report a Vulnerability, kindly email at [zemerikY@gmail.com](mailto:zemeriky@gmail.com) 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | /* 3 | 4 | # Except this folder 5 | !/src 6 | !/.github 7 | !tsconfig.json 8 | !tsconfig.node.json 9 | !package.json 10 | !.prettierrc 11 | !.eslintrc.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "printWidth": 80, 6 | "singleQuote": false, 7 | "jsxSingleQuote": false, 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "forceConsistentCasingInFileNames": true, 5 | "composite": true, 6 | "module": "esnext", 7 | "moduleResolution": "node" 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | 5 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 6 | 7 | 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /src/components/commands/GeneralOutput.tsx: -------------------------------------------------------------------------------- 1 | import {Wrapper} from "../styles/Output.styled"; 2 | 3 | type Props = { 4 | children: string; 5 | }; 6 | 7 | const GeneralOutput: React.FC = ({children}) => ( 8 | {children} 9 | ); 10 | export default GeneralOutput; 11 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #1d2a35 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/styles/Themes.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ThemesWrapper = styled.div` 4 | display: flex; 5 | flex-wrap: wrap; 6 | `; 7 | 8 | export const ThemeSpan = styled.span` 9 | margin-right: 0.875rem; 10 | margin-bottom: 0.25rem; 11 | white-space: nowrap; 12 | `; 13 | -------------------------------------------------------------------------------- /src/components/TermInfo.tsx: -------------------------------------------------------------------------------- 1 | import {User, WebsiteName, Wrapper} from "./styles/TerminalInfo.styled"; 2 | 3 | const TermInfo = () => { 4 | return ( 5 | 6 | visitor@zemerik:~$ 7 | 8 | ); 9 | }; 10 | 11 | export default TermInfo; 12 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | export const setToLS = (key: string, value: string) => { 2 | // window.localStorage.setItem(key, JSON.stringify(value)); 3 | window.localStorage.setItem(key, value); 4 | }; 5 | 6 | export const getFromLS = (key: string) => { 7 | const value = window.localStorage.getItem(key); 8 | 9 | if (value) { 10 | return value; 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/styles/TerminalInfo.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.span` 4 | display: inline-block; 5 | margin-right: 0.75rem; 6 | `; 7 | 8 | export const WebsiteName = styled.span` 9 | color: ${({theme}) => theme.colors?.primary}; 10 | `; 11 | 12 | export const User = styled.span` 13 | color: ${({theme}) => theme.colors?.secondary}; 14 | `; 15 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import react from "@vitejs/plugin-react"; 5 | import { defineConfig } from "vite"; 6 | import { VitePWA } from "vite-plugin-pwa"; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | plugins: [ 11 | react(), 12 | VitePWA({ 13 | registerType: "autoUpdate", 14 | }), 15 | ] 16 | }); 17 | -------------------------------------------------------------------------------- /src/components/styles/Education.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const EduIntro = styled.div` 4 | margin-bottom: 1rem; 5 | margin-top: 1rem; 6 | `; 7 | 8 | export const EduList = styled.div` 9 | margin-bottom: 1rem; 10 | 11 | .title { 12 | font-weight: 700; 13 | margin-bottom: 0.5rem; 14 | color: #f3c26b; 15 | } 16 | 17 | .desc { 18 | color: ${({theme}) => theme.colors?.text[200]}; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /src/components/styles/Output.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const OutputContainer = styled.div` 4 | padding-bottom: 0.25rem; 5 | `; 6 | 7 | export const Wrapper = styled.div` 8 | margin-top: 1rem; 9 | margin-bottom: 0.75rem; 10 | `; 11 | 12 | export const UsageDiv = styled.div<{ marginY?: boolean }>` 13 | margin-top: ${props => (props.marginY ? "0.75rem" : "0.25rem")}; 14 | margin-bottom: 0.75rem; 15 | line-height: 1.5rem; 16 | `; 17 | -------------------------------------------------------------------------------- /src/components/commands/Clear.tsx: -------------------------------------------------------------------------------- 1 | import {useContext, useEffect} from "react"; 2 | import {UsageDiv} from "../styles/Output.styled"; 3 | import {termContext} from "../Terminal"; 4 | 5 | const Clear: React.FC = () => { 6 | const {arg, clearHistory} = useContext(termContext); 7 | useEffect(() => { 8 | if (arg.length < 1) clearHistory?.(); 9 | }, []); 10 | return arg.length > 0 ? Usage: clear : <>; 11 | }; 12 | 13 | export default Clear; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | package-lock.json 10 | 11 | # Yarn 12 | yarn-error.log 13 | .yarn/* 14 | !.yarn/releases 15 | !.yarn/plugins 16 | !.yarn/sdks 17 | yarn.lock 18 | 19 | 20 | node_modules 21 | dist 22 | dist-ssr 23 | *.local 24 | 25 | # Editor directories and files 26 | .vscode/* 27 | !.vscode/extensions.json 28 | .idea 29 | .DS_Store 30 | *.suo 31 | *.ntvs* 32 | *.njsproj 33 | *.sln 34 | *.sw? 35 | -------------------------------------------------------------------------------- /src/components/styles/Work.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const EduIntro = styled.div` 4 | margin-bottom: 1rem; 5 | margin-top: 1rem; 6 | `; 7 | 8 | export const EduList = styled.div` 9 | margin-bottom: 1rem; 10 | 11 | .title { 12 | font-weight: 700; 13 | margin-bottom: 0.5rem; 14 | color: #f3c26b; 15 | } 16 | 17 | .desc { 18 | color: ${({theme}) => theme.colors?.text[200]}; 19 | } 20 | 21 | .activeYear { 22 | color: lawngreen; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import "styled-components"; 4 | 5 | declare module "styled-components" { 6 | export interface DefaultTheme { 7 | id: string; 8 | name: string; 9 | colors: { 10 | body: string; 11 | scrollHandle: string; 12 | scrollHandleHover: string; 13 | primary: string; 14 | secondary: string; 15 | text: { 16 | 100: string; 17 | 200: string; 18 | 300: string; 19 | }; 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/styles/About.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const AboutWrapper = styled.div` 4 | margin-top: 0.25rem; 5 | margin-bottom: 0.75rem; 6 | p { 7 | margin-top: 0.5rem; 8 | line-height: 1.5rem; 9 | } 10 | `; 11 | 12 | export const HighlightSpan = styled.span` 13 | font-weight: 700; 14 | color: #f3c26b; 15 | `; 16 | 17 | export const HighlightAlt = styled.span` 18 | font-weight: 700; 19 | `; 20 | 21 | export const HighlightAKA = styled.span` 22 | color: dodgerblue; 23 | `; 24 | -------------------------------------------------------------------------------- /src/utils/test-utils.tsx: -------------------------------------------------------------------------------- 1 | import { cleanup, render } from "@testing-library/react"; 2 | import { afterEach } from "vitest"; 3 | 4 | afterEach(() => { 5 | cleanup(); 6 | }); 7 | 8 | const customRender = (ui: React.ReactElement, options = {}) => 9 | render(ui, { 10 | // wrap provider(s) here if needed 11 | wrapper: ({ children }) => children, 12 | ...options, 13 | }); 14 | 15 | export * from "@testing-library/react"; 16 | export { default as userEvent } from "@testing-library/user-event"; 17 | // override render export 18 | export { customRender as render }; 19 | -------------------------------------------------------------------------------- /src/components/commands/History.tsx: -------------------------------------------------------------------------------- 1 | import {useContext} from "react"; 2 | import _ from "lodash"; 3 | import {Wrapper} from "../styles/Output.styled"; 4 | import {termContext} from "../Terminal"; 5 | 6 | const History: React.FC = () => { 7 | const {history, index} = useContext(termContext); 8 | const currentHistory = _.reverse(_.slice(history, index)); 9 | 10 | return ( 11 | 12 | {currentHistory.map(cmd => ( 13 |
{cmd}
14 | ))} 15 |
16 | ); 17 | }; 18 | 19 | export default History; 20 | -------------------------------------------------------------------------------- /src/components/styles/Projects.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const ProjectContainer = styled.div` 4 | margin-top: 0.5rem; 5 | margin-bottom: 0.875rem; 6 | `; 7 | 8 | export const ProjectsIntro = styled.div` 9 | margin-top: 1rem; 10 | color: dodgerblue; 11 | margin-bottom: 1rem; 12 | line-height: 1.5rem; 13 | `; 14 | 15 | export const ProjectTitle = styled.div` 16 | font-weight: 700; 17 | color: #f3c26b; 18 | margin-bottom: 0.25rem; 19 | `; 20 | 21 | export const ProjectDesc = styled.div` 22 | color: ${({theme}) => theme.colors?.text[200]}; 23 | text-align: justify; 24 | line-height: 1.5rem; 25 | max-width: 500px; 26 | `; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "types": ["vite-plugin-pwa/client"] 19 | }, 20 | "include": ["src"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /src/components/commands/Email.tsx: -------------------------------------------------------------------------------- 1 | import {useContext} from "react"; 2 | import _ from "lodash"; 3 | import {Wrapper} from "../styles/Output.styled"; 4 | import {termContext} from "../Terminal"; 5 | 6 | const Email: React.FC = () => { 7 | const {history, rerender} = useContext(termContext); 8 | 9 | /* ===== get current command ===== */ 10 | const currentCommand = _.split(history[0], " "); 11 | 12 | if (rerender && currentCommand[0] === "email" && currentCommand.length <= 1) { 13 | window.open("mailto:" + "ZemerikY@gmail.com", "_self"); 14 | } 15 | 16 | return ( 17 | 18 | ZemerikY@gmail.com 19 | 20 | ); 21 | }; 22 | 23 | export default Email; 24 | -------------------------------------------------------------------------------- /src/components/Usage.tsx: -------------------------------------------------------------------------------- 1 | import { UsageDiv } from "./styles/Output.styled"; 2 | 3 | type Props = { 4 | cmd: "projects" | "socials" | "games"; 5 | marginY?: boolean; 6 | }; 7 | 8 | const arg = { 9 | projects: { placeholder: "project-no", example: "4" }, 10 | socials: { placeholder: "social-no", example: "2" }, 11 | games: { placeholder: "games-no", example: "1" }, 12 | }; 13 | 14 | const Usage: React.FC = ({ cmd, marginY = false }) => { 15 | const action = "go"; 16 | return ( 17 | 18 | Usage: {cmd} {action} <{arg[cmd].placeholder}>
19 | eg: {cmd} {action} {arg[cmd].example} 20 |
21 | ); 22 | }; 23 | 24 | export default Usage; 25 | -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import themes from "../components/styles/themes"; 3 | import { setToLS, getFromLS } from "../utils/storage"; 4 | import { DefaultTheme } from "styled-components"; 5 | 6 | export const useTheme = () => { 7 | const [theme, setTheme] = useState(themes.dark); 8 | const [themeLoaded, setThemeLoaded] = useState(false); 9 | 10 | const setMode = (mode: DefaultTheme) => { 11 | setToLS("tsn-theme", mode.name); 12 | setTheme(mode); 13 | }; 14 | 15 | useEffect(() => { 16 | const localThemeName = getFromLS("tsn-theme"); 17 | localThemeName ? setTheme(themes[localThemeName]) : setTheme(themes.dark); 18 | setThemeLoaded(true); 19 | }, []); 20 | 21 | return { theme, themeLoaded, setMode }; 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/styles/Help.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HelpWrapper = styled.div` 4 | margin-top: 1rem; 5 | margin-bottom: 0.75rem; 6 | `; 7 | 8 | export const CmdList = styled.div` 9 | margin-bottom: 0.5rem; 10 | `; 11 | 12 | export const Cmd = styled.span` 13 | color: ${({ theme }) => theme.colors?.primary}; 14 | `; 15 | 16 | export const CmdDesc = styled.span` 17 | color: ${({ theme }) => theme.colors?.text[200]}; 18 | margin-bottom: 0.75rem; 19 | 20 | @media (max-width: 550px) { 21 | display: block; 22 | } 23 | `; 24 | 25 | export const KeyContainer = styled.div` 26 | font-size: 0.875rem; 27 | margin-top: 1rem; 28 | 29 | @media (max-width: 550px) { 30 | display: none; 31 | } 32 | 33 | div { 34 | margin-top: 0.45rem; 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /src/components/commands/About.tsx: -------------------------------------------------------------------------------- 1 | import {AboutWrapper, HighlightAKA, HighlightSpan,} from "../styles/About.styled"; 2 | 3 | const About: React.FC = () => { 4 | return ( 5 | 6 |

7 |

8 | 👋 Hi I Hemang, 9 |

10 |

11 |

12 | 💘 As a 15-year-old high school student, I am driven by an unwavering passion for pursuing a career as a front-end software developer. With a knack for transforming concepts into tangible realities, I possess a strong command of Python, JavaScript, and HTML, enabling me to bring ideas to life through coding expertise. 13 |

14 |

15 |
16 | ); 17 | }; 18 | 19 | export default About; 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Fixes Issue 4 | 5 | 6 | 7 | 8 | 9 | ## Changes proposed 10 | 11 | 12 | 13 | 14 | 20 | 21 | ## Check List (Check all the applicable boxes) 22 | 23 | - [ ] My Changes follow the Code of Conduct of this Project. 24 | - [ ] My Post or Change does not contain any **Plagarized** Content. 25 | - [ ] The title of the PR is a short description of the Changes made. 26 | 27 | ## Note to reviewers 28 | 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:markdown/recommended", 11 | "prettier" 12 | ], 13 | "overrides": [], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": "latest", 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["react", "@typescript-eslint", "react-hooks", "prettier"], 20 | "ignorePatterns": ["node_modules", "dist"], 21 | "rules": { 22 | "react/react-in-jsx-scope": "off", 23 | "react/no-unescaped-entities": 0, 24 | "camelcase": "error", 25 | "no-duplicate-imports": "error", 26 | "@typescript-eslint/no-unused-vars": "error", 27 | "react/prop-types": 0, 28 | "linebreak-style": ["warn", "unix"] 29 | }, 30 | "settings": { 31 | "react": { 32 | "version": "detect" 33 | }, 34 | "import/resolver": { 35 | "typescript": {} 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Hemang Yadav 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/styles/Terminal.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Wrapper = styled.div` 4 | padding: 1.25rem; 5 | padding-top: 0.75rem; 6 | 7 | display: flex; 8 | flex-direction: column-reverse; 9 | max-height: calc(100vh - 2rem); 10 | overflow-y: auto; 11 | `; 12 | 13 | export const CmdNotFound = styled.div` 14 | margin-top: 0.25rem; 15 | margin-bottom: 1rem; 16 | `; 17 | 18 | export const Empty = styled.div` 19 | margin-bottom: 0.25rem; 20 | `; 21 | 22 | export const MobileSpan = styled.span` 23 | line-height: 1.5rem; 24 | margin-right: 0.75rem; 25 | 26 | @media (min-width: 550px) { 27 | display: none; 28 | } 29 | `; 30 | 31 | export const MobileBr = styled.br` 32 | @media (min-width: 550px) { 33 | display: none; 34 | } 35 | `; 36 | 37 | export const Form = styled.form` 38 | @media (min-width: 550px) { 39 | display: flex; 40 | } 41 | `; 42 | 43 | export const Input = styled.input` 44 | flex-grow: 1; 45 | 46 | @media (max-width: 550px) { 47 | min-width: 85%; 48 | } 49 | `; 50 | 51 | export const Hints = styled.span` 52 | margin-right: 0.875rem; 53 | `; 54 | -------------------------------------------------------------------------------- /src/components/commands/Help.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Cmd, 3 | CmdDesc, 4 | CmdList, 5 | HelpWrapper, 6 | KeyContainer, 7 | } from "../styles/Help.styled"; 8 | import { commands } from "../Terminal"; 9 | import { generateTabs } from "../../utils/funcs"; 10 | 11 | const Help: React.FC = () => { 12 | return ( 13 | 14 | {commands.map(({ cmd, desc, tab }) => ( 15 | 16 | {cmd} 17 | {generateTabs(tab)} 18 | - {desc} 19 | 20 | ))} 21 | 22 |
23 | Tab or Ctrl + i  => 24 | autocompletes the command 25 |
26 |
27 | Up Arrow {generateTabs(5)} => go 28 | back to previous command 29 |
30 |
31 | Ctrl + l {generateTabs(5)} => 32 | clear the terminal 33 |
34 |
35 |
36 | ); 37 | }; 38 | 39 | export default Help; 40 | -------------------------------------------------------------------------------- /src/components/commands/Education.tsx: -------------------------------------------------------------------------------- 1 | import {generateTabs} from "../../utils/funcs"; 2 | import {EduIntro, EduList} from "../styles/Education.styled"; 3 | import {Wrapper} from "../styles/Output.styled"; 4 | 5 | const Education: React.FC = () => { 6 | return ( 7 | 8 | My Education Background ! 9 | {eduBg.map(({title, desc, tab, year}) => ( 10 | 11 |
{title}
12 |
13 | {desc} {generateTabs(tab)} 14 | {year} 15 |
16 |
17 | ))} 18 |
19 | ); 20 | }; 21 | 22 | const eduBg = [ 23 | { 24 | title: "Game / Web Development", 25 | desc: "Hillcrest Christian College", 26 | year: "2024", 27 | tab: 4, 28 | }, 29 | { 30 | title: "Cybersecurity", 31 | desc: "Monash University", 32 | year: "2024", 33 | tab: 14, 34 | }, 35 | { 36 | title: "Autodesk Tinkercad", 37 | desc: "Hillcrest Christian College", 38 | year: "2023", 39 | tab: 4, 40 | }, 41 | ]; 42 | 43 | export default Education; 44 | -------------------------------------------------------------------------------- /.github/workflows/greetings.yml: -------------------------------------------------------------------------------- 1 | name: Greetings 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | issues: 7 | types: [opened] 8 | 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | 13 | jobs: 14 | greet: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Greet on PRs and Issues 18 | uses: actions/github-script@v7 19 | with: 20 | script: | 21 | try { 22 | const isPR = context.payload.pull_request !== undefined; 23 | const number = isPR ? context.payload.pull_request.number : context.payload.issue.number; 24 | const commentBody = isPR 25 | ? `Welcome, @${{ github.actor }}! Thanks for raising the issue!` 26 | : `Great job, @${{ github.actor }}! Thanks for creating the pull request`; 27 | 28 | await github.rest.issues.createComment({ 29 | owner: context.repo.owner, 30 | repo: context.repo.repo, 31 | issue_number: number, 32 | body: commentBody 33 | }); 34 | 35 | console.log('Comment successfully created.'); 36 | } catch (error) { 37 | console.error('Error creating comment:', error); 38 | // Do not mark the step as failed; continue with the workflow. 39 | } -------------------------------------------------------------------------------- /src/components/styles/Welcome.styled.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const HeroContainer = styled.div` 4 | display: flex; 5 | flex-wrap: wrap-reverse; 6 | 7 | @media (max-width: 932px) { 8 | margin-bottom: 1.5rem; 9 | } 10 | 11 | div { 12 | @media (min-width: 1024px) { 13 | flex-basis: 50%; 14 | } 15 | } 16 | `; 17 | 18 | export const PreName = styled.pre` 19 | margin-top: 0.5rem; 20 | margin-bottom: 1.5rem; 21 | 22 | @media (max-width: 550px) { 23 | display: none; 24 | } 25 | `; 26 | 27 | export const PreWrapper = styled.div` 28 | text-align: center; 29 | `; 30 | 31 | export const PreNameMobile = styled.pre` 32 | margin-top: 0.5rem; 33 | margin-bottom: 1.5rem; 34 | 35 | @media (min-width: 550px) { 36 | display: none; 37 | } 38 | `; 39 | 40 | export const PreImg = styled.pre` 41 | @media (max-width: 550px) { 42 | display: none; 43 | } 44 | `; 45 | 46 | export const Seperator = styled.div` 47 | margin-top: 0.75rem; 48 | margin-bottom: 0.75rem; 49 | `; 50 | 51 | export const Cmd = styled.span` 52 | color: ${({theme}) => theme.colors?.primary}; 53 | `; 54 | 55 | export const Link = styled.a` 56 | color: ${({theme}) => theme.colors?.secondary}; 57 | text-decoration: none; 58 | line-height: 1.5rem; 59 | white-space: nowrap; 60 | border-bottom: 2px dashed ${({theme}) => theme.colors?.secondary}; 61 | 62 | &:hover { 63 | border-bottom-style: solid; 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Zemerik's Terminal 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 37 | 38 | 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | 2 | name: 🐞 Bug Report 3 | description: File a bug report 4 | title: '[Bug]: ' 5 | labels: ['bug'] 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: What happened? 15 | description: Also tell us, what did you expect to happen? 16 | placeholder: Add descriptions 17 | value: 'Briefly Describe the bug you found' 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: screenshots 22 | attributes: 23 | label: Add screenshots 24 | description: Add screenshots to see the problems 25 | placeholder: Add screenshots 26 | value: 'Add screenshots' 27 | - type: dropdown 28 | id: browsers 29 | attributes: 30 | label: What browsers are you seeing the problem on? 31 | multiple: true 32 | options: 33 | - Firefox 34 | - Chrome 35 | - Safari 36 | - Microsoft Edge 37 | - Brave 38 | - Other 39 | - type: checkboxes 40 | id: self-grab 41 | attributes: 42 | label: Self - Grab 43 | description: By checking this box, you can fix this bug 44 | options: 45 | - label: I would like to work on this issue 46 | id: terms 47 | attributes: 48 | label: Code of Conduct 49 | description: By submitting this issue, you agree to follow our Code of Conduct 50 | options: 51 | - label: I agree to follow this project's Code of Conduct 52 | -------------------------------------------------------------------------------- /src/components/Output.tsx: -------------------------------------------------------------------------------- 1 | import About from "./commands/About"; 2 | import Clear from "./commands/Clear"; 3 | import Education from "./commands/Education"; 4 | import Email from "./commands/Email"; 5 | import GeneralOutput from "./commands/GeneralOutput"; 6 | import Help from "./commands/Help"; 7 | import Welcome from "./commands/Welcome"; 8 | import History from "./commands/History"; 9 | import Projects from "./commands/Projects"; 10 | import Socials from "./commands/Socials"; 11 | import { OutputContainer, UsageDiv } from "./styles/Output.styled"; 12 | import { termContext } from "./Terminal"; 13 | import { useContext } from "react"; 14 | 15 | type Props = { 16 | index: number; 17 | cmd: string; 18 | }; 19 | 20 | const Output: React.FC = ({ index, cmd }) => { 21 | const { arg } = useContext(termContext); 22 | 23 | const specialCmds = ["projects", "socials", "games"]; 24 | 25 | // return 'Usage: ' if command arg is not valid 26 | // eg: about tt 27 | if (!specialCmds.includes(cmd) && arg.length > 0) 28 | return Usage: {cmd}; 29 | 30 | return ( 31 | 32 | { 33 | { 34 | about: , 35 | clear: , 36 | education: , 37 | email: , 38 | help: , 39 | history: , 40 | projects: , 41 | pwd: /home/Zemerik, 42 | socials: , 43 | welcome: , 44 | whoami: ( 45 | 46 | Well, it looks like you're having an identity crisis even the 47 | terminal can't solve. Maybe try asking a mirror instead? 🪞 48 | 49 | ), 50 | }[cmd] 51 | } 52 | 53 | ); 54 | }; 55 | 56 | export default Output; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "terminal-portfolio", 3 | "private": true, 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "tsc && vite build", 8 | "preview": "vite preview", 9 | "test:once": "vitest run", 10 | "test": "vitest", 11 | "coverage": "vitest run --coverage", 12 | "prepare": "husky install", 13 | "lint": "eslint .", 14 | "format:check": "prettier --check .", 15 | "format": "prettier --write ." 16 | }, 17 | "dependencies": { 18 | "lodash": "^4.17.21", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "styled-components": "^5.3.10", 22 | "styled-normalize": "^8.0.7", 23 | "terminal-portfolio": "file:" 24 | }, 25 | "devDependencies": { 26 | "@testing-library/jest-dom": "^5.16.5", 27 | "@testing-library/react": "^14.0.0", 28 | "@testing-library/user-event": "^14.4.3", 29 | "@types/lodash": "^4.14.194", 30 | "@types/react": "^18.2.6", 31 | "@types/react-dom": "^18.2.4", 32 | "@types/styled-components": "^5.1.26", 33 | "@types/testing-library__jest-dom": "^5.14.5", 34 | "@typescript-eslint/eslint-plugin": "^5.59.6", 35 | "@typescript-eslint/parser": "^5.59.6", 36 | "@vitejs/plugin-react": "^4.0.0", 37 | "@vitest/coverage-c8": "^0.31.1", 38 | "eslint": "^8.40.0", 39 | "eslint-config-prettier": "^8.8.0", 40 | "eslint-import-resolver-typescript": "^3.5.5", 41 | "eslint-plugin-import": "^2.27.5", 42 | "eslint-plugin-markdown": "^3.0.0", 43 | "eslint-plugin-prettier": "^4.2.1", 44 | "eslint-plugin-react": "^7.32.2", 45 | "eslint-plugin-react-hooks": "^4.6.0", 46 | "husky": "^8.0.3", 47 | "jsdom": "^22.0.0", 48 | "lint-staged": "^13.2.2", 49 | "prettier": "^2.8.8", 50 | "typescript": "^5.0.4", 51 | "vite": "^4.3.8", 52 | "vite-plugin-pwa": "^0.14.7", 53 | "vitest": "^0.31.1" 54 | }, 55 | "lint-staged": { 56 | "*": [ 57 | "eslint", 58 | "prettier --write" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/styles/GlobalStyle.tsx: -------------------------------------------------------------------------------- 1 | import {createGlobalStyle, DefaultTheme} from "styled-components"; 2 | import {normalize} from "styled-normalize"; 3 | 4 | const GlobalStyle = createGlobalStyle<{ theme: DefaultTheme }>` 5 | ${normalize} 6 | *, ::before, ::after { 7 | border-width: 0; 8 | border-style: solid; 9 | border-color: theme('borderColor.DEFAULT', currentColor); 10 | } 11 | 12 | blockquote, dl, dd, h1, h2, h3, 13 | h4, h5, h6, hr, figure, p, pre { 14 | margin: 0; 15 | } 16 | 17 | h1, h2, h3, h4, h5, h6 { 18 | font-size: inherit; 19 | font-weight: inherit; 20 | } 21 | 22 | img, svg, video, canvas, audio, 23 | iframe, embed, object { 24 | display: block; 25 | } 26 | 27 | body { 28 | font-family: 'Cascadia Code', 'IBM Plex Mono', monospace; 29 | font-weight: 500; 30 | font-size: 14px; 31 | background-color: ${({theme}) => theme.colors?.body}; 32 | color: ${({theme}) => theme.colors?.text[100]}; 33 | } 34 | 35 | /* ===== Custom Scroll Bar ===== */ 36 | /* width */ 37 | ::-webkit-scrollbar { 38 | width: 15px; 39 | } 40 | 41 | /* Track */ 42 | ::-webkit-scrollbar-track { 43 | background: ${({theme}) => theme.colors?.body}; 44 | } 45 | 46 | /* Handle */ 47 | ::-webkit-scrollbar-thumb { 48 | background: ${({theme}) => theme.colors?.scrollHandle}; 49 | } 50 | 51 | /* Handle on hover */ 52 | ::-webkit-scrollbar-thumb:hover { 53 | background: ${({theme}) => theme.colors?.scrollHandleHover}; 54 | } 55 | 56 | input[type=text] { 57 | background-color: ${({theme}) => theme.colors?.body}; 58 | color: ${({theme}) => theme.colors?.text[100]}; 59 | caret-color: ${({theme}) => theme.colors?.primary}; 60 | } 61 | 62 | input[type=text]:focus-visible { 63 | outline: none; 64 | } 65 | 66 | .sr-only { 67 | position: absolute; 68 | left: -10000px; 69 | top: auto; 70 | width: 1px; 71 | height: 1px; 72 | overflow: hidden; 73 | } 74 | `; 75 | 76 | export default GlobalStyle; 77 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useEffect, useState } from "react"; 2 | import { DefaultTheme, ThemeProvider } from "styled-components"; 3 | import { useTheme } from "./hooks/useTheme"; 4 | import GlobalStyle from "./components/styles/GlobalStyle"; 5 | import Terminal from "./components/Terminal"; 6 | 7 | export const themeContext = createContext< 8 | ((switchTheme: DefaultTheme) => void) | null 9 | >(null); 10 | 11 | function App() { 12 | // themes 13 | const { theme, themeLoaded, setMode } = useTheme(); 14 | const [selectedTheme, setSelectedTheme] = useState(theme); 15 | 16 | // Disable browser's default behavior 17 | // to prevent the page go up when Up Arrow is pressed 18 | useEffect(() => { 19 | window.addEventListener( 20 | "keydown", 21 | e => { 22 | ["ArrowUp", "ArrowDown"].indexOf(e.code) > -1 && e.preventDefault(); 23 | }, 24 | false 25 | ); 26 | }, []); 27 | 28 | useEffect(() => { 29 | setSelectedTheme(theme); 30 | }, [themeLoaded]); 31 | 32 | // Update meta tag colors when switching themes 33 | useEffect(() => { 34 | const themeColor = theme.colors?.body; 35 | 36 | const metaThemeColor = document.querySelector("meta[name='theme-color']"); 37 | const maskIcon = document.querySelector("link[rel='mask-icon']"); 38 | const metaMsTileColor = document.querySelector( 39 | "meta[name='msapplication-TileColor']" 40 | ); 41 | 42 | metaThemeColor && metaThemeColor.setAttribute("content", themeColor); 43 | metaMsTileColor && metaMsTileColor.setAttribute("content", themeColor); 44 | maskIcon && maskIcon.setAttribute("color", themeColor); 45 | }, [selectedTheme]); 46 | 47 | const themeSwitcher = (switchTheme: DefaultTheme) => { 48 | setSelectedTheme(switchTheme); 49 | setMode(switchTheme); 50 | }; 51 | 52 | return ( 53 | <> 54 |

55 | Terminal Portfolio 56 |

57 | {themeLoaded && ( 58 | 59 | 60 | 61 | 62 | 63 | 64 | )} 65 | 66 | ); 67 | } 68 | 69 | export default App; 70 | -------------------------------------------------------------------------------- /src/components/commands/Socials.tsx: -------------------------------------------------------------------------------- 1 | import {useContext, useEffect} from "react"; 2 | import {ProjectsIntro} from "../styles/Projects.styled"; 3 | import {Cmd, CmdDesc, CmdList, HelpWrapper} from "../styles/Help.styled"; 4 | import {checkRedirect, generateTabs, getCurrentCmdArry, isArgInvalid,} from "../../utils/funcs"; 5 | import {termContext} from "../Terminal"; 6 | import Usage from "../Usage"; 7 | 8 | const Socials: React.FC = () => { 9 | const {arg, history, rerender} = useContext(termContext); 10 | 11 | /* ===== get current command ===== */ 12 | const currentCommand = getCurrentCmdArry(history); 13 | 14 | /* ===== check current command makes redirect ===== */ 15 | useEffect(() => { 16 | if (checkRedirect(rerender, currentCommand, "socials")) { 17 | socials.forEach(({id, url}) => { 18 | id === parseInt(arg[1]) && window.open(url, "_blank"); 19 | }); 20 | } 21 | }, [arg, rerender, currentCommand]); 22 | 23 | /* ===== check arg is valid ===== */ 24 | const checkArg = () => 25 | isArgInvalid(arg, "go", ["1", "2", "3", "4"]) ? ( 26 | 27 | ) : null; 28 | 29 | return arg.length > 0 || arg.length > 2 ? ( 30 | checkArg() 31 | ) : ( 32 | 33 | Here are my social links 34 | {socials.map(({id, title, url, tab}) => ( 35 | 36 | {`${id}. ${title}`} 37 | {generateTabs(tab)} 38 | - {url} 39 | 40 | ))} 41 | 42 | 43 | ); 44 | }; 45 | 46 | const socials = [ 47 | { 48 | id: 1, 49 | title: "GitHub", 50 | url: "https://github.com/Zemerik/", 51 | tab: 3, 52 | }, 53 | { 54 | id: 2, 55 | title: "LinkedIn", 56 | url: "https://linkedin.com/in/Zemerik/", 57 | tab: 1, 58 | }, 59 | { 60 | id: 3, 61 | title: "Twitter", 62 | url: "https://twitter.com/Zemerik_X/", 63 | tab: 2, 64 | }, 65 | { 66 | id: 4, 67 | title: "Instagram", 68 | url: "https://instagram.com/Zemerik_INSTA/", 69 | tab: 0, 70 | }, 71 | ]; 72 | 73 | export default Socials; 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 | 11 |

12 | 13 |

14 | Zemerik's Terminal 15 |

16 | 17 |

18 | An Interactive Terminal 19 |

20 | 21 |

22 | Laptop Screenshot 23 |

24 | 25 | ## ❓ How to Contribute? 26 | 27 | - All Contributions are highly appreciated, if you would like to contribute to this project, follow the steps below. 28 | 29 | 1. Fork a copy of this Repository on your Github account by clicking below, 30 | 31 | - [Fork](https://github.com/Zemerik/Terminal/fork) 32 | 33 | 2. Clone your Forked Repository by using the following `GIT` command: 34 | 35 | ```bash 36 | git clone https://github.com/[YOUR GITHUB USERNAME]/Terminal.git 37 | ``` 38 | 39 | 3. Navigate into the Project's `Directory` by using the command below: 40 | 41 | ```bash 42 | cd Terminal 43 | ``` 44 | 45 | 4. Initialize a Remote to the original Repository by the following `GIT` command: 46 | 47 | ```bash 48 | git remote add upstream https://github.com/Zemerik/Terminal 49 | ``` 50 | 51 | 5. Create a new `branch` in which you can make your desired changes: 52 | 53 | ```bash 54 | git checkout -b newcontribution 55 | ``` 56 | 57 | 6. After making your changes, add all your files to the Staging Area: 58 | 59 | ```bash 60 | git add --all 61 | ``` 62 | 63 | 7. Commit your Changes: 64 | 65 | ```bash 66 | git commit -m "[COMMIT MSG]" 67 | ``` 68 | 69 | > [!Note] 70 | > Remember to live a good commit message 71 | 72 | 8. Push all your Changes: 73 | 74 | ```bash 75 | git push origin newcontribution 76 | ``` 77 | 78 | 9. Create a new Pull - Request (PR) on the Original Repository 79 | 80 | > Your Pull Request will be merged / reviewed as soon as possible. 81 | 82 | ## 🐞Bug/Issue/Feedback/Feature Request: 83 | 84 | - If you would like to report a bug, a issue, implement any feedack, or request any feature, you are free to do so by opening a issue on this repository. Remember to give a detailed explanation of what you are trying to say, and how it will help the website. 85 | 86 | ## 💁 Support: 87 | 88 | For any kind of support or inforrmation, you are free to join our **Discord Server**, 89 | 90 | 91 | 92 | 93 | 94 |

95 | Thanks for Visiting🙏 96 |

97 | 98 |

99 | Don't forget to leave a ⭐ 100 |
101 | Made with 💖 by Hemang Yadav (Zemerik) 102 |

-------------------------------------------------------------------------------- /src/components/commands/Projects.tsx: -------------------------------------------------------------------------------- 1 | import {useContext, useEffect} from "react"; 2 | import {checkRedirect, getCurrentCmdArry, isArgInvalid,} from "../../utils/funcs"; 3 | import {ProjectContainer, ProjectDesc, ProjectsIntro, ProjectTitle,} from "../styles/Projects.styled"; 4 | import {termContext} from "../Terminal"; 5 | import Usage from "../Usage"; 6 | 7 | const Projects: React.FC = () => { 8 | const {arg, history, rerender} = useContext(termContext); 9 | 10 | /* ===== get current command ===== */ 11 | const currentCommand = getCurrentCmdArry(history); 12 | 13 | /* ===== check current command is redirect ===== */ 14 | useEffect(() => { 15 | if (checkRedirect(rerender, currentCommand, "projects")) { 16 | projects.forEach(({id, url, repo}) => { 17 | id === parseInt(arg[1]) && 18 | window.open(url !== "" ? url : repo, "_blank"); 19 | }); 20 | } 21 | }, [arg, rerender, currentCommand]); 22 | 23 | /* ===== check arg is valid ===== */ 24 | const checkArg = () => 25 | isArgInvalid(arg, "go", ["1", "2", "3"]) ? ( 26 | 27 | ) : null; 28 | 29 | return arg.length > 0 || arg.length > 2 ? ( 30 | checkArg() 31 | ) : ( 32 |
33 | 34 | The web is like a canvas, and code is the paint. Create your masterpiece.
35 |
36 | {projects.map(({id, title, desc}) => ( 37 | 38 | {`${id}. ${title}`} 39 | {desc} 40 | 41 | ))} 42 | 43 |
44 | ); 45 | }; 46 | 47 | const projects = [ 48 | { 49 | id: 1, 50 | title: "ZemPosts", 51 | desc: "Post & Connect with Developers", 52 | url: "https://zemposts.vercel.app", 53 | repo: "https://github.com/Zemerik/ZemPosts", 54 | }, 55 | { 56 | id: 2, 57 | title: "ZemShowcase", 58 | desc: "Showcase & Connect with Developers", 59 | url: "https://zemshowcase.vercel.app", 60 | repo: "https://github.com/Zemerik/ZemShowcase", 61 | }, 62 | { 63 | id: 3, 64 | title: "ZemProfiles", 65 | desc: "Discover & Connect with Developers", 66 | url: "https://zemprofiles.vercel.app", 67 | repo: "https://github.com/Zemerik/ZemProfiles", 68 | }, 69 | { 70 | id: 4, 71 | title: "Resume", 72 | desc: "A Dynamic Resume", 73 | url: "https://zemeriksresume.vercel.app", 74 | repo: "https://github.com/Zemerik/Resume", 75 | }, 76 | { 77 | id: 5, 78 | title: "Dashboard", 79 | desc: "A Unifed Digital Identity Hub", 80 | url: "https://zemeriksdashboard.vercel.app", 81 | repo: "https://github.com/Zemerik/Dashboard", 82 | }, 83 | ]; 84 | 85 | export default Projects; 86 | -------------------------------------------------------------------------------- /src/components/styles/themes.ts: -------------------------------------------------------------------------------- 1 | import {DefaultTheme} from "styled-components"; 2 | 3 | export type Themes = { 4 | [key: string]: DefaultTheme; 5 | }; 6 | 7 | const theme: Themes = { 8 | dark: { 9 | id: "T_001", 10 | name: "dark", 11 | colors: { 12 | body: "#1D2A35", 13 | scrollHandle: "#19252E", 14 | scrollHandleHover: "#162028", 15 | primary: "#05CE91", 16 | secondary: "#FF9D00", 17 | text: { 18 | 100: "#cbd5e1", 19 | 200: "#B2BDCC", 20 | 300: "#64748b", 21 | }, 22 | }, 23 | }, 24 | light: { 25 | id: "T_002", 26 | name: "light", 27 | colors: { 28 | body: "#EFF3F3", 29 | scrollHandle: "#C1C1C1", 30 | scrollHandleHover: "#AAAAAA", 31 | primary: "#027474", 32 | secondary: "#FF9D00", 33 | text: { 34 | 100: "#334155", 35 | 200: "#475569", 36 | 300: "#64748b", 37 | }, 38 | }, 39 | }, 40 | "blue-matrix": { 41 | id: "T_003", 42 | name: "blue-matrix", 43 | colors: { 44 | body: "#101116", 45 | scrollHandle: "#424242", 46 | scrollHandleHover: "#616161", 47 | primary: "#00ff9c", 48 | secondary: "#60fdff", 49 | text: { 50 | 100: "#ffffff", 51 | 200: "#c7c7c7", 52 | 300: "#76ff9f", 53 | }, 54 | }, 55 | }, 56 | espresso: { 57 | id: "T_004", 58 | name: "espresso", 59 | colors: { 60 | body: "#323232", 61 | scrollHandle: "#5b5b5b", 62 | scrollHandleHover: "#393939", 63 | primary: "#E1E48B", 64 | secondary: "#A5C260", 65 | text: { 66 | 100: "#F7F7F7", 67 | 200: "#EEEEEE", 68 | 300: "#5b5b5b", 69 | }, 70 | }, 71 | }, 72 | "green-goblin": { 73 | id: "T_005", 74 | name: "green-goblin", 75 | colors: { 76 | body: "#000000", 77 | scrollHandle: "#2E2E2E", 78 | scrollHandleHover: "#414141", 79 | primary: "#E5E500", 80 | secondary: "#04A500", 81 | text: { 82 | 100: "#01FF00", 83 | 200: "#04A5B2", 84 | 300: "#E50101", 85 | }, 86 | }, 87 | }, 88 | ubuntu: { 89 | id: "T_006", 90 | name: "ubuntu", 91 | colors: { 92 | body: "#2D0922", 93 | scrollHandle: "#F47845", 94 | scrollHandleHover: "#E65F31", 95 | primary: "#80D932", 96 | secondary: "#80D932", 97 | text: { 98 | 100: "#FFFFFF", 99 | 200: "#E1E9CC", 100 | 300: "#CDCDCD", 101 | }, 102 | }, 103 | }, 104 | }; 105 | 106 | export default theme; 107 | -------------------------------------------------------------------------------- /src/components/commands/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Cmd, HeroContainer, PreName, PreNameMobile, PreWrapper, Seperator,} from "../styles/Welcome.styled"; 3 | 4 | const Welcome: React.FC = () => { 5 | return ( 6 | 7 |
8 | 9 | {` 10 | ******** ******** **** **** ******** ******* ** ** ** ** ******** ********** ******** ******* **** **** ** **** ** ** ** 11 | //////** /**///// /**/** **/**/**///// /**////** /**/** ** //* **////// /////**/// /**///// /**////** /**/** **/**/**/**/** /** **** /** 12 | ** /** /**//** ** /**/** /** /** /**/** ** / /** /** /** /** /** /**//** ** /**/**/**//** /** **//** /** 13 | ** /******* /** //*** /**/******* /******* /**/**** /********* /** /******* /******* /** //*** /**/**/** //** /** ** //** /** 14 | ** /**//// /** //* /**/**//// /**///** /**/**/** ////////** /** /**//// /**///** /** //* /**/**/** //**/** **********/** 15 | ** /** /** / /**/** /** //** /**/**//** /** /** /** /** //** /** / /**/**/** //****/**//////**/** 16 | ********/********/** /**/********/** //**/**/** //** ******** /** /********/** //**/** /**/**/** //***/** /**/******** 17 | //////// //////// // // //////// // // // // // //////// // //////// // // // // // // /// // // //////// 18 | `} 19 | 20 | 21 | 22 | 23 | {` 24 | 25 | :::===== :::===== :::======= :::===== :::==== ::: ::: === == :::=== :::==== :::===== :::==== :::======= ::: :::= === :::==== ::: 26 | === ::: ::: === === ::: ::: === ::: ::: === == ::: :::==== ::: ::: === ::: === === ::: :::===== ::: === ::: 27 | === ====== === === === ====== ======= === ====== ===== === ====== ======= === === === === ======== ======== === 28 | === === === === === === === === === === === === === === === === === === === ==== === === === 29 | ======== ======== === === ======== === === === === === ====== === ======== === === === === === === === === === ======== 30 | 31 | 32 | `} 33 | 34 | 35 |
Welcome to Hemang Yadav (Zemerik)'s Terminal
36 | ---- 37 |
38 | For a list of available commands, type help. 39 |
40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Welcome; 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 | 11 |

12 | 13 |

14 | Zemerik's Terminal 15 |

16 | 17 |

18 | An Interactive Terminal 19 |

20 | 21 |

22 | Laptop Screenshot 23 |

24 | 25 | ![Wakatime Stats](https://wakatime.com/badge/user/9860690e-8928-4746-844c-c2924f121f2d/project/b984b121-3ec9-4083-af1d-d2df660cd16f.svg) 26 | 27 | ## ❗About: 28 | 29 | Welcome to my personal terminal-themed website! Dive into an interactive experience where you can explore my world through a classic command-line interface. Just type in the provided commands to learn more about my background, skills, projects, and interests. Whether you're here to discover my professional journey, check out my portfolio, or just curious about who I am, this website offers a unique, nostalgic way to connect. Get ready to type, discover, and enjoy the seamless blend of modern content with a retro touch! 30 | 31 | ## ⭐ Key Features: 32 | 33 | - Interactive Command-Line Interface: 34 | 35 | - Skills 36 | 37 | - Projects 38 | 39 | - Responsive Design 40 | 41 | - User-Friendly Commands 42 | 43 | ## 📱 Guide: 44 | 45 | ### Commands: 46 | 47 | - `about` - About Zemerik 48 | - `clear` - Clear the Terminal 49 | - `education` - Education Background 50 | - `email` - Contact Me through my Email 51 | - `history` - View Command History 52 | - `projects` - View all my Projects 53 | - `pwd` - Your Current Working Directory 54 | - `socials` - Follow Zemerik around the Web 55 | - `welcome` - Display Welcome Section 56 | - `whoami` - Get to Know who you are 57 | 58 | ### Shortcuts: 59 | 60 | - `Tab` / `Ctrl + i` - autocompletes the command 61 | - `Up Arrow` - go back to previous command 62 | - `Ctrl + l` - clear the terminal 63 | 64 | > Feel free to open [Pull-Requests](https://github.com/Zemerik/Terminal/pulls) for more Commands / Shorcuts. 65 | 66 | ## 💻 Screenshots: 67 | 68 | ![Phone Screenshot](public/screenshot_phone.png) 69 | 70 | ## 🚀 Quick Start: 71 | 72 | ### Prerequisites: 73 | 74 | - [NodeJS](https://nodejs.org) installed on your machine 75 | - [GIT](https://git-scm.com) installed on your machine 76 | - A Code Editor 77 | 78 | ### Cloning: 79 | 80 | - To make a local copy of this Project on your machine, enter the following `GIT` Commmand in your Terminal: 81 | 82 | ```bash 83 | git clone https://github.com/Zemerik/Terminal 84 | ``` 85 | 86 | ### Installing Dependencies: 87 | 88 | - To run this project locally, we first need to download a few `npm` dependencies by using the command below: 89 | 90 | ```bash 91 | npm i 92 | ``` 93 | 94 | ### Locally Running: 95 | 96 | - We can locally run this Project on our Network and see the output using the following Command of `NodeJS`: 97 | 98 | ```bash 99 | npm run dev 100 | ``` 101 | 102 | ## 😎 Happy Coding!! 103 | 104 | ## 🤝 Contributing: 105 | 106 | Contributions are always welcome and appreciated! **Kindly visit the [CONTRIBUTING.md](https://github.com/Zemerik/Terminal/blob/main/CONTRIBUTING.md) file for more information** 107 | 108 | 109 | ## 💁 Support: 110 | 111 | For any kind of support or inforrmation, you are free to join our **Discord Server**, 112 | 113 | 114 | 115 | 116 | 117 | # 118 | 119 |

120 | Don't forget to leave a ⭐ 121 |
122 | Made with 💖 by Hemang Yadav (Zemerik) 123 |

124 | -------------------------------------------------------------------------------- /src/utils/funcs.ts: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import theme from "../components/styles/themes"; 3 | 4 | /** 5 | * Generates html tabs 6 | * @param {number} num - The number of tabs 7 | * @returns {string} tabs - Tab string 8 | */ 9 | export const generateTabs = (num = 0): string => { 10 | let tabs = "\xA0\xA0"; 11 | for (let i = 0; i < num; i++) { 12 | tabs += "\xA0"; 13 | } 14 | return tabs; 15 | }; 16 | 17 | /** 18 | * Check arg is valid 19 | * @param {string[]} arg - The arg array 20 | * @param {string} action - The action to compare | "go" | "set" 21 | * @param {string[]} options - Option array to compare | "dark" | "1" 22 | * @returns {boolean} boolean 23 | */ 24 | export const isArgInvalid = ( 25 | arg: string[], 26 | action: string, 27 | options: string[] 28 | ) => arg[0] !== action || !_.includes(options, arg[1]) || arg.length > 2; 29 | 30 | /** 31 | * Transform current cmd & arg into array 32 | * then return back the array 33 | * @param {string[]} history - The history array 34 | * @returns {string[]} array of cmd string 35 | */ 36 | export const getCurrentCmdArry = (history: string[]) => 37 | _.split(history[0].trim(), " "); 38 | 39 | /** 40 | * Check current render makes redirect 41 | * @param {boolean} rerender - is submitted or not 42 | * @param {string[]} currentCommand - current submitted command 43 | * @param {string} command - the command of the function 44 | * @returns {boolean} redirect - true | false 45 | */ 46 | export const checkRedirect = ( 47 | rerender: boolean, 48 | currentCommand: string[], 49 | command: string 50 | ): boolean => 51 | rerender && // is submitted 52 | currentCommand[0] === command && // current command starts with ('socials'|'projects') 53 | currentCommand[1] === "go" && // first arg is 'go' 54 | currentCommand.length > 1 && // current command has arg 55 | currentCommand.length < 4 && // if num of arg is valid (not `projects go 1 sth`) 56 | _.includes([1, 2, 3, 4], parseInt(currentCommand[2])); // arg last part is one of id 57 | 58 | /** 59 | * Perform advanced tab actions 60 | * @param {string} inputVal - current input value 61 | * @param {(value: React.SetStateAction) => void} setInputVal - setInputVal setState 62 | * @param {(value: React.SetStateAction) => void} setHints - setHints setState 63 | * @param {hintsCmds} hintsCmds - hints command array 64 | * @returns {string[] | undefined} hints command or setState action(undefined) 65 | */ 66 | export const argTab = ( 67 | inputVal: string, 68 | setInputVal: (value: React.SetStateAction) => void, 69 | setHints: (value: React.SetStateAction) => void, 70 | hintsCmds: string[] 71 | ): string[] | undefined => { 72 | // 1) if input is 'themes ' 73 | if (inputVal === "themes ") { 74 | setInputVal(`themes set`); 75 | return []; 76 | } 77 | 78 | // 2) if input is 'themes s' 79 | else if ( 80 | _.startsWith("themes", _.split(inputVal, " ")[0]) && 81 | _.split(inputVal, " ")[1] !== "set" && 82 | _.startsWith("set", _.split(inputVal, " ")[1]) 83 | ) { 84 | setInputVal(`themes set`); 85 | return []; 86 | } 87 | 88 | // 3) if input is 'themes set ' 89 | else if (inputVal === "themes set ") { 90 | setHints(_.keys(theme)); 91 | return []; 92 | } 93 | 94 | // 4) if input starts with 'themes set ' + theme 95 | else if (_.startsWith(inputVal, "themes set ")) { 96 | _.keys(theme).forEach(t => { 97 | if (_.startsWith(t, _.split(inputVal, " ")[2])) { 98 | hintsCmds = [...hintsCmds, t]; 99 | } 100 | }); 101 | return hintsCmds; 102 | } 103 | 104 | // 5) if input is 'projects' or 'socials' 105 | else if (inputVal === "projects " || inputVal === "socials ") { 106 | setInputVal(`${inputVal}go`); 107 | return []; 108 | } 109 | 110 | // 6) if input is 'projects g' or 'socials g' 111 | else if (inputVal === "projects g" || inputVal === "socials g") { 112 | setInputVal(`${inputVal}o`); 113 | return []; 114 | } 115 | 116 | // 7) if input is 'socials go ' 117 | else if (_.startsWith(inputVal, "socials go ")) { 118 | ["1.Github", "2.Dev.to", "3.Facebook", "4.Instagram"].forEach(t => { 119 | hintsCmds = [...hintsCmds, t]; 120 | }); 121 | return hintsCmds; 122 | } 123 | 124 | // 8) if input is 'projects go ' 125 | else if (_.startsWith(inputVal, "projects go ")) { 126 | ["", "", "", ""].forEach(t => { 127 | hintsCmds = [...hintsCmds, t]; 128 | }); 129 | return hintsCmds; 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | zemerikY@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/components/Terminal.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | createContext, 3 | useCallback, 4 | useEffect, 5 | useRef, 6 | useState, 7 | } from "react"; 8 | import _ from "lodash"; 9 | import Output from "./Output"; 10 | import TermInfo from "./TermInfo"; 11 | import { 12 | CmdNotFound, 13 | Empty, 14 | Form, 15 | Hints, 16 | Input, 17 | MobileBr, 18 | MobileSpan, 19 | Wrapper, 20 | } from "./styles/Terminal.styled"; 21 | import { argTab } from "../utils/funcs"; 22 | 23 | type Command = { 24 | cmd: string; 25 | desc: string; 26 | tab: number; 27 | }[]; 28 | 29 | export const commands: Command = [ 30 | { cmd: "about", desc: "About Zemerik", tab: 8 }, 31 | { cmd: "clear", desc: "Clear the terminal", tab: 8 }, 32 | { cmd: "education", desc: "Education background", tab: 4 }, 33 | { cmd: "email", desc: "Contact Me Through My Email", tab: 8 }, 34 | { cmd: "help", desc: "List Available Commands", tab: 9 }, 35 | { cmd: "history", desc: "View Command History", tab: 6 }, 36 | { cmd: "projects", desc: "View All My Projects", tab: 5 }, 37 | { cmd: "pwd", desc: "Your Current Working Directory", tab: 10 }, 38 | { cmd: "socials", desc: "Follow Zemerik around the Web", tab: 6 }, 39 | { cmd: "welcome", desc: "Display Welcome Section", tab: 6 }, 40 | { cmd: "whoami", desc: "Get to know who you are.", tab: 7 }, 41 | ]; 42 | 43 | type Term = { 44 | arg: string[]; 45 | history: string[]; 46 | rerender: boolean; 47 | index: number; 48 | clearHistory?: () => void; 49 | }; 50 | 51 | export const termContext = createContext({ 52 | arg: [], 53 | history: [], 54 | rerender: false, 55 | index: 0, 56 | }); 57 | 58 | const Terminal = () => { 59 | const containerRef = useRef(null); 60 | const inputRef = useRef(null); 61 | 62 | const [inputVal, setInputVal] = useState(""); 63 | const [cmdHistory, setCmdHistory] = useState(["welcome"]); 64 | const [rerender, setRerender] = useState(false); 65 | const [hints, setHints] = useState([]); 66 | const [pointer, setPointer] = useState(-1); 67 | 68 | const handleChange = useCallback( 69 | (e: React.ChangeEvent) => { 70 | setRerender(false); 71 | setInputVal(e.target.value); 72 | }, 73 | [inputVal] 74 | ); 75 | 76 | const handleSubmit = (e: React.FormEvent) => { 77 | e.preventDefault(); 78 | setCmdHistory([inputVal, ...cmdHistory]); 79 | setInputVal(""); 80 | setRerender(true); 81 | setHints([]); 82 | setPointer(-1); 83 | }; 84 | 85 | const clearHistory = () => { 86 | setCmdHistory([]); 87 | setHints([]); 88 | }; 89 | 90 | // focus on input when terminal is clicked 91 | const handleDivClick = () => { 92 | inputRef.current && inputRef.current.focus(); 93 | }; 94 | useEffect(() => { 95 | document.addEventListener("click", handleDivClick); 96 | return () => { 97 | document.removeEventListener("click", handleDivClick); 98 | }; 99 | }, [containerRef]); 100 | 101 | // Keyboard Press 102 | const handleKeyDown = (e: React.KeyboardEvent) => { 103 | setRerender(false); 104 | const ctrlI = e.ctrlKey && e.key.toLowerCase() === "i"; 105 | const ctrlL = e.ctrlKey && e.key.toLowerCase() === "l"; 106 | 107 | // if Tab or Ctrl + I 108 | if (e.key === "Tab" || ctrlI) { 109 | e.preventDefault(); 110 | if (!inputVal) return; 111 | 112 | let hintsCmds: string[] = []; 113 | commands.forEach(({ cmd }) => { 114 | if (_.startsWith(cmd, inputVal)) { 115 | hintsCmds = [...hintsCmds, cmd]; 116 | } 117 | }); 118 | 119 | const returnedHints = argTab(inputVal, setInputVal, setHints, hintsCmds); 120 | hintsCmds = returnedHints ? [...hintsCmds, ...returnedHints] : hintsCmds; 121 | 122 | // if there are many command to autocomplete 123 | if (hintsCmds.length > 1) { 124 | setHints(hintsCmds); 125 | } 126 | // if only one command to autocomplete 127 | else if (hintsCmds.length === 1) { 128 | const currentCmd = _.split(inputVal, " "); 129 | setInputVal( 130 | currentCmd.length !== 1 131 | ? `${currentCmd[0]} ${currentCmd[1]} ${hintsCmds[0]}` 132 | : hintsCmds[0] 133 | ); 134 | 135 | setHints([]); 136 | } 137 | } 138 | 139 | // if Ctrl + L 140 | if (ctrlL) { 141 | clearHistory(); 142 | } 143 | 144 | // Go previous cmd 145 | if (e.key === "ArrowUp") { 146 | if (pointer >= cmdHistory.length) return; 147 | 148 | if (pointer + 1 === cmdHistory.length) return; 149 | 150 | setInputVal(cmdHistory[pointer + 1]); 151 | setPointer(prevState => prevState + 1); 152 | inputRef?.current?.blur(); 153 | } 154 | 155 | // Go next cmd 156 | if (e.key === "ArrowDown") { 157 | if (pointer < 0) return; 158 | 159 | if (pointer === 0) { 160 | setInputVal(""); 161 | setPointer(-1); 162 | return; 163 | } 164 | 165 | setInputVal(cmdHistory[pointer - 1]); 166 | setPointer(prevState => prevState - 1); 167 | inputRef?.current?.blur(); 168 | } 169 | }; 170 | 171 | // For caret position at the end 172 | useEffect(() => { 173 | const timer = setTimeout(() => { 174 | inputRef?.current?.focus(); 175 | }, 1); 176 | return () => clearTimeout(timer); 177 | }, [inputRef, inputVal, pointer]); 178 | 179 | return ( 180 | 181 | {hints.length > 1 && ( 182 |
183 | {hints.map(hCmd => ( 184 | {hCmd} 185 | ))} 186 |
187 | )} 188 |
189 | 193 | 206 |
207 | 208 | {cmdHistory.map((cmdH, index) => { 209 | const commandArray = _.split(_.trim(cmdH), " "); 210 | const validCommand = _.find(commands, { cmd: commandArray[0] }); 211 | const contextValue = { 212 | arg: _.drop(commandArray), 213 | history: cmdHistory, 214 | rerender, 215 | index, 216 | clearHistory, 217 | }; 218 | return ( 219 |
220 |
221 | 222 | 223 | > 224 | {cmdH} 225 |
226 | {validCommand ? ( 227 | 228 | 229 | 230 | ) : cmdH === "" ? ( 231 | 232 | ) : ( 233 | 234 | command not found: {cmdH} 235 | 236 | )} 237 |
238 | ); 239 | })} 240 |
241 | ); 242 | }; 243 | 244 | export default Terminal; 245 | --------------------------------------------------------------------------------