@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: ['prettier'],
|
||||
extends: ['next/core-web-vitals'],
|
||||
rules: {
|
||||
'no-console': 'error',
|
||||
'prettier/prettier': 'warn',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
};
|
@ -0,0 +1,35 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
.gitattributes
|
@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint
|
@ -0,0 +1,33 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"tabWidth": 2
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Vincent Wu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,117 @@
|
||||
# [LiveTerm - Make Terminal styled websites in minutes!](https://cveinnt.com)
|
||||
|
||||
Highly customizable, easy-to-use, and minimal terminal styled website template, written in Next.js.
|
||||
|
||||
# Table of Contents:
|
||||
|
||||
- [LiveTerm - Make Terminal styled websites in minutes!](#liveterm---make-terminal-styled-websites-in-minutes)
|
||||
- [Table of Contents:](#table-of-contents)
|
||||
- [Showcase](#showcase)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Configuration](#configuration)
|
||||
- [Basic Configuration](#basic-configuration)
|
||||
- [Favicons](#favicons)
|
||||
- [Banner](#banner)
|
||||
- [Advanced Configuration](#advanced-configuration)
|
||||
- [Deploy on Vercel](#deploy-on-vercel)
|
||||
- [Credit](#credit)
|
||||
|
||||
## Showcase
|
||||
|
||||
<p align="center">
|
||||
<img src="./demo/demo.png" width="800"><br>
|
||||
<strong>Default LiveTerm</strong>
|
||||
</p>
|
||||
|
||||
Live version [here](https://cveinnt.com)
|
||||
|
||||
<p align="center">
|
||||
<img src="./demo/cveinnt.png" width="800"><br>
|
||||
<strong>my personal website</strong>
|
||||
</p>
|
||||
|
||||
Live version [here](https://cveinnt.com)
|
||||
|
||||
## Quick Start
|
||||
|
||||
First, clone this repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Cveinnt/LiveTerm.git
|
||||
```
|
||||
|
||||
Then, install dependencies:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
Now you can start development!
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Or, you can build the project:
|
||||
|
||||
```bash
|
||||
yarn build && yarn start
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
Most of the configuration is done through the `config.json` file.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"readmeUrl": //create a Github README and link it here!
|
||||
"title": //title of the website
|
||||
"name": //returned by the command of the same name
|
||||
"social": {
|
||||
"github": // your handle
|
||||
"linkedin": // your handle
|
||||
},
|
||||
"email": // your email
|
||||
"ps1_hostname": //hostname in prompt
|
||||
"ps1_username": "guest", // username in prompt
|
||||
"non_terminal_url": "W",
|
||||
"colors": {
|
||||
... // you can use existing templates in themes.json or use your own!
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Feel free to change it as you see fit!
|
||||
|
||||
You can find several pre-configured themes in `themes.json`, and you can replace the colors in `config.json` with the theme color you like! The themes are based on the themes on [this website](https://glitchbone.github.io/vscode-base16-term/#/).
|
||||
|
||||
<p align="center">
|
||||
<img src="./demo/themes.png" width="800"><br>
|
||||
<strong>different LiveTerm themes</strong>
|
||||
</p>
|
||||
|
||||
Just replace `"light"` or `"dark"` in the `"color"` part of the config file!
|
||||
|
||||
### Favicons
|
||||
|
||||
Favicons are located in `public/`, along with other files you may want to upload. I used this [website](https://www.favicon-generator.org/) to generate favicons.
|
||||
|
||||
### Banner
|
||||
|
||||
You may also want to change the output of `banner` command. To do that, simply paste your generated banner in `src/utils/bin/utils.ts`. I used this [website](https://manytools.org/hacker-tools/ascii-banner/) to generate my banner.
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
If you want to further customize your page, feel free to change the source code to your preference!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy a Next.js app is to use the [Vercel Platform](https://vercel.com/) from the creators of Next.js.
|
||||
|
||||
Check out [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
|
||||
## Credit
|
||||
|
||||
Based on M4TT72's awesome [Terminal](https://github.com/m4tt72/terminal).
|
@ -0,0 +1,39 @@
|
||||
{
|
||||
"readmeUrl": "https://raw.githubusercontent.com/cveinnt/cveinnt/master/README.md",
|
||||
"title": "LiveTerm",
|
||||
"name": "John Doe",
|
||||
"ascii": "liveterm",
|
||||
"social": {
|
||||
"github": "",
|
||||
"linkedin": ""
|
||||
},
|
||||
"email": "contact@johndoe.com",
|
||||
"ps1_hostname": "liveterm",
|
||||
"ps1_username": "visitor",
|
||||
"non_terminal_url": "https://github.com/Cveinnt/LiveTerm",
|
||||
"resume_url": "https://upload.wikimedia.org/wikipedia/commons/c/cc/Resume.pdf",
|
||||
"donate_urls": {
|
||||
"paypal": "https://paypal.me/cveinnt",
|
||||
"patreon": "https://patreon.com/cveinnt"
|
||||
},
|
||||
"colors": {
|
||||
"light": {
|
||||
"background": "#FBF1C9",
|
||||
"foreground": "#3C3836",
|
||||
"yellow": "#D79921",
|
||||
"green": "#98971A",
|
||||
"gray": "#7C6F64",
|
||||
"blue": "#458588",
|
||||
"red": "#CA2124"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#2E3440",
|
||||
"foreground": "#E5E9F0",
|
||||
"yellow": "#5E81AC",
|
||||
"green": "#A3BE8C",
|
||||
"gray": "#88C0D0",
|
||||
"blue": "#EBCB8B",
|
||||
"red": "#BF616A"
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 352 KiB |
After Width: | Height: | Size: 310 KiB |
After Width: | Height: | Size: 334 KiB |
After Width: | Height: | Size: 335 KiB |
After Width: | Height: | Size: 334 KiB |
After Width: | Height: | Size: 332 KiB |
After Width: | Height: | Size: 335 KiB |
After Width: | Height: | Size: 332 KiB |
After Width: | Height: | Size: 330 KiB |
After Width: | Height: | Size: 216 KiB |
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
@ -0,0 +1 @@
|
||||
module.exports = {};
|
@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "liveterm",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Vincent Wu",
|
||||
"url": "https://cveinnt.com",
|
||||
"email": "contact@wensenwu.com"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"next": "12.1.6",
|
||||
"react": "18.1.0",
|
||||
"react-dom": "18.1.0",
|
||||
"react-icons": "^4.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "17.0.32",
|
||||
"@types/react": "18.0.9",
|
||||
"@types/react-dom": "18.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
||||
"@typescript-eslint/parser": "^5.23.0",
|
||||
"autoprefixer": "^10.4.7",
|
||||
"eslint": "8.15.0",
|
||||
"eslint-config-next": "^12.1.6",
|
||||
"eslint-plugin-next": "^0.0.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"husky": "^8.0.1",
|
||||
"postcss": "^8.4.13",
|
||||
"prettier": "^2.6.2",
|
||||
"tailwindcss": "^3.0.24",
|
||||
"typescript": "^4.6.4"
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 5.4 KiB |
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 7.3 KiB |
After Width: | Height: | Size: 22 KiB |
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
|
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "LiveTerm",
|
||||
"short_name": "LiveTerm",
|
||||
"theme_color": "#2E3440",
|
||||
"background_color": "#2E3440",
|
||||
"display": "fullscreen",
|
||||
"orientation": "portrait",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-icon-36x36.png",
|
||||
"sizes": "36x36",
|
||||
"type": "image/png",
|
||||
"density": "0.75"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png",
|
||||
"density": "1.0"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"density": "1.5"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"density": "2.0"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"density": "3.0"
|
||||
},
|
||||
{
|
||||
"src": "/android-icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"density": "4.0"
|
||||
}
|
||||
],
|
||||
"splash_pages": null
|
||||
}
|
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 6.5 KiB |
@ -0,0 +1,3 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
Disallow: /cgi-bin/
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import config from '../../config.json';
|
||||
|
||||
export const Ps1 = () => {
|
||||
return (
|
||||
<div>
|
||||
<span className="text-light-yellow dark:text-dark-yellow">
|
||||
{config.ps1_username}
|
||||
</span>
|
||||
<span className="text-light-gray dark:text-dark-gray">@</span>
|
||||
<span className="text-light-green dark:text-dark-green">
|
||||
{config.ps1_hostname}
|
||||
</span>
|
||||
<span className="text-light-gray dark:text-dark-gray">:$ ~ </span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Ps1;
|
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { History as HistoryInterface } from './interface';
|
||||
import { Ps1 } from '../Ps1';
|
||||
|
||||
export const History: React.FC<{ history: Array<HistoryInterface> }> = ({
|
||||
history,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{history.map((entry: HistoryInterface, index: number) => (
|
||||
<div key={entry.command + index}>
|
||||
<div className="flex flex-row space-x-2">
|
||||
<div className="flex-shrink">
|
||||
<Ps1 />
|
||||
</div>
|
||||
|
||||
<div className="flex-grow">{entry.command}</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="whitespace-pre-wrap mb-2"
|
||||
// style={{ lineHeight: 'normal' }}
|
||||
dangerouslySetInnerHTML={{ __html: entry.output }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default History;
|
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { History } from './interface';
|
||||
|
||||
export const useHistory = (defaultValue: Array<History>) => {
|
||||
const [history, setHistory] = React.useState<Array<History>>(defaultValue);
|
||||
const [command, setCommand] = React.useState<string>('');
|
||||
const [lastCommandIndex, setLastCommandIndex] = React.useState<number>(0);
|
||||
|
||||
return {
|
||||
history,
|
||||
command,
|
||||
lastCommandIndex,
|
||||
setHistory: (value: string) =>
|
||||
setHistory([
|
||||
...history,
|
||||
{
|
||||
id: history.length,
|
||||
date: new Date(),
|
||||
command,
|
||||
output: value,
|
||||
},
|
||||
]),
|
||||
setCommand,
|
||||
setLastCommandIndex,
|
||||
clearHistory: () => setHistory([]),
|
||||
};
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
export interface History {
|
||||
id: number;
|
||||
date: Date;
|
||||
command: string;
|
||||
output: string;
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { commandExists } from '../utils/commandExists';
|
||||
import { shell } from '../utils/shell';
|
||||
import { handleTabCompletion } from '../utils/tabCompletion';
|
||||
import { Ps1 } from './Ps1';
|
||||
|
||||
export const Input = ({
|
||||
inputRef,
|
||||
containerRef,
|
||||
command,
|
||||
history,
|
||||
lastCommandIndex,
|
||||
setCommand,
|
||||
setHistory,
|
||||
setLastCommandIndex,
|
||||
clearHistory,
|
||||
}) => {
|
||||
const onSubmit = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
const commands: [string] = history
|
||||
.map(({ command }) => command)
|
||||
.filter((command: string) => command);
|
||||
|
||||
if (event.key === 'c' && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
setCommand('');
|
||||
setHistory('');
|
||||
setLastCommandIndex(0);
|
||||
}
|
||||
|
||||
if (event.key === 'l' && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
clearHistory();
|
||||
}
|
||||
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
handleTabCompletion(command, setCommand);
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' || event.code === '13') {
|
||||
event.preventDefault();
|
||||
setLastCommandIndex(0);
|
||||
await shell(command, setHistory, clearHistory, setCommand);
|
||||
containerRef.current.scrollTo(0, containerRef.current.scrollHeight);
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
if (!commands.length) {
|
||||
return;
|
||||
}
|
||||
const index: number = lastCommandIndex + 1;
|
||||
if (index <= commands.length) {
|
||||
setLastCommandIndex(index);
|
||||
setCommand(commands[commands.length - index]);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
if (!commands.length) {
|
||||
return;
|
||||
}
|
||||
const index: number = lastCommandIndex - 1;
|
||||
if (index > 0) {
|
||||
setLastCommandIndex(index);
|
||||
setCommand(commands[commands.length - index]);
|
||||
} else {
|
||||
setLastCommandIndex(0);
|
||||
setCommand('');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = ({
|
||||
target: { value },
|
||||
}: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCommand(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-row space-x-2">
|
||||
<label htmlFor="prompt" className="flex-shrink">
|
||||
<Ps1 />
|
||||
</label>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
id="prompt"
|
||||
type="text"
|
||||
className={`bg-light-background dark:bg-dark-background focus:outline-none flex-grow ${
|
||||
commandExists(command) || command === ''
|
||||
? 'text-dark-green'
|
||||
: 'text-dark-red'
|
||||
}`}
|
||||
value={command}
|
||||
onChange={onChange}
|
||||
autoFocus
|
||||
onKeyDown={onSubmit}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Input;
|
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const NotFoundPage = () => {
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
router.replace('/');
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import '../styles/global.css';
|
||||
import Head from 'next/head';
|
||||
|
||||
const App = ({ Component, pageProps }) => {
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const onClickAnywhere = () => {
|
||||
inputRef.current.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="initial-scale=1.0, width=device-width"
|
||||
key="viewport"
|
||||
maximum-scale="1"
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<div
|
||||
className="text-light-foreground dark:text-dark-foreground min-w-max text-xs md:min-w-full md:text-base"
|
||||
onClick={onClickAnywhere}
|
||||
>
|
||||
<main className="bg-light-background dark:bg-dark-background w-full h-full p-2">
|
||||
<Component {...pageProps} inputRef={inputRef} />
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
@ -0,0 +1,64 @@
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
import config from '../../config.json';
|
||||
import { Input } from '../components/input';
|
||||
import { useHistory } from '../components/history/hook';
|
||||
import { History } from '../components/history/History';
|
||||
import { banner } from '../utils/bin';
|
||||
|
||||
interface IndexPageProps {
|
||||
inputRef: React.MutableRefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
const IndexPage: React.FC<IndexPageProps> = ({ inputRef }) => {
|
||||
const containerRef = React.useRef(null);
|
||||
const {
|
||||
history,
|
||||
command,
|
||||
lastCommandIndex,
|
||||
setCommand,
|
||||
setHistory,
|
||||
clearHistory,
|
||||
setLastCommandIndex,
|
||||
} = useHistory([]);
|
||||
|
||||
const init = React.useCallback(() => setHistory(banner()), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
init();
|
||||
}, [init]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [history]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{config.title}</title>
|
||||
</Head>
|
||||
|
||||
<div className="p-8 overflow-hidden h-full border-2 rounded border-light-yellow dark:border-dark-yellow">
|
||||
<div ref={containerRef} className="overflow-y-auto h-full">
|
||||
<History history={history} />
|
||||
|
||||
<Input
|
||||
inputRef={inputRef}
|
||||
containerRef={containerRef}
|
||||
command={command}
|
||||
history={history}
|
||||
lastCommandIndex={lastCommandIndex}
|
||||
setCommand={setCommand}
|
||||
setHistory={setHistory}
|
||||
setLastCommandIndex={setLastCommandIndex}
|
||||
clearHistory={clearHistory}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IndexPage;
|
@ -0,0 +1,41 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hack';
|
||||
src: url(/assets/fonts/Hack-NF.ttf);
|
||||
display: swap;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: 'Hack', monospace;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
body > div:first-child,
|
||||
div#__next,
|
||||
div#__next > div {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1e252e;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ebdbb2;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #ff8037;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
import axios from 'axios';
|
||||
import config from '../../config.json';
|
||||
|
||||
export const getProjects = async () => {
|
||||
const { data } = await axios.get(
|
||||
`https://api.github.com/users/${config.social.github}/repos`,
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getReadme = async () => {
|
||||
const { data } = await axios.get(config.readmeUrl);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getWeather = async (city: string) => {
|
||||
try {
|
||||
const { data } = await axios.get(`/api/weather/${city}`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getQuote = async () => {
|
||||
const { data } = await axios.get('http://api.quotable.io/random');
|
||||
return {
|
||||
quote: `“${data.content}” — ${data.author}`,
|
||||
};
|
||||
};
|
@ -0,0 +1,36 @@
|
||||
// // List of commands that require API calls
|
||||
|
||||
import { getProjects } from '../api';
|
||||
import { getQuote } from '../api';
|
||||
import { getReadme } from '../api';
|
||||
import { getWeather } from '../api';
|
||||
|
||||
export const projects = async (args: string[]): Promise<string> => {
|
||||
const projects = await getProjects();
|
||||
return projects
|
||||
.map(
|
||||
(repo) =>
|
||||
`${repo.name} - <a class="text-light-blue dark:text-dark-blue underline" href="${repo.html_url}" target="_blank">${repo.html_url}</a>`,
|
||||
)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
export const quote = async (args: string[]): Promise<string> => {
|
||||
const data = await getQuote();
|
||||
return data.quote;
|
||||
};
|
||||
|
||||
export const readme = async (args: string[]): Promise<string> => {
|
||||
const readme = await getReadme();
|
||||
return `Opening GitHub README...\n
|
||||
${readme}`;
|
||||
};
|
||||
|
||||
export const weather = async (args: string[]): Promise<string> => {
|
||||
const city = args.join('+');
|
||||
if (!city) {
|
||||
return 'Usage: weather [city]. Example: weather casablanca';
|
||||
}
|
||||
const weather = await getWeather(city);
|
||||
return weather;
|
||||
};
|
@ -0,0 +1,136 @@
|
||||
// List of commands that do not require API calls
|
||||
|
||||
import * as bin from './index';
|
||||
import config from '../../../config.json';
|
||||
|
||||
// Help
|
||||
export const help = async (args: string[]): Promise<string> => {
|
||||
const commands = Object.keys(bin).sort().join(', ');
|
||||
var c = '';
|
||||
for (let i = 1; i <= Object.keys(bin).sort().length; i++) {
|
||||
if (i % 7 === 0) {
|
||||
c += Object.keys(bin).sort()[i - 1] + '\n';
|
||||
} else {
|
||||
c += Object.keys(bin).sort()[i - 1] + ' ';
|
||||
}
|
||||
}
|
||||
return `Welcome! Here are all the available commands:
|
||||
\n${c}\n
|
||||
[tab]: trigger completion.
|
||||
[ctrl+l]/clear: clear terminal.\n
|
||||
Type 'sumfetch' to display summary.
|
||||
Type 'gui' or click <u><a href="${config.non_terminal_url}" target="_blank">here</a></u> for a simpler version.
|
||||
`;
|
||||
};
|
||||
|
||||
// Redirection
|
||||
export const gui = async (args: string[]): Promise<string> => {
|
||||
window.open(`${config.non_terminal_url}`);
|
||||
return 'Opening GUI version...';
|
||||
};
|
||||
|
||||
// About
|
||||
export const about = async (args: string[]): Promise<string> => {
|
||||
return `Hi, I am ${config.name}.
|
||||
Welcome to my website!
|
||||
More about me:
|
||||
'sumfetch' - short summary.
|
||||
'resume' - my latest resume.
|
||||
'readme' - my github readme.`;
|
||||
};
|
||||
|
||||
export const resume = async (args: string[]): Promise<string> => {
|
||||
window.open(`${config.resume_url}`);
|
||||
return 'Opening resume...';
|
||||
};
|
||||
|
||||
// Donate
|
||||
export const donate = async (args: string[]): Promise<string> => {
|
||||
return `thank you for your interest.
|
||||
here are the ways you can support my work:
|
||||
- <u><a class="text-light-blue dark:text-dark-blue underline" href="${config.donate_urls.paypal}" target="_blank">paypal</a></u>
|
||||
- <u><a class="text-light-blue dark:text-dark-blue underline" href="${config.donate_urls.patreon}" target="_blank">patreon</a></u>
|
||||
`;
|
||||
};
|
||||
|
||||
// Contact
|
||||
export const email = async (args: string[]): Promise<string> => {
|
||||
window.open(`mailto:${config.email}`);
|
||||
return `Opening mailto:${config.email}...`;
|
||||
};
|
||||
|
||||
export const github = async (args: string[]): Promise<string> => {
|
||||
window.open(`https://github.com/${config.social.github}/`);
|
||||
|
||||
return 'Opening github...';
|
||||
};
|
||||
|
||||
export const linkedin = async (args: string[]): Promise<string> => {
|
||||
window.open(`https://www.linkedin.com/in/${config.social.linkedin}/`);
|
||||
|
||||
return 'Opening linkedin...';
|
||||
};
|
||||
|
||||
// Typical linux commands
|
||||
export const echo = async (args: string[]): Promise<string> => {
|
||||
return args.join(' ');
|
||||
};
|
||||
|
||||
export const whoami = async (args: string[]): Promise<string> => {
|
||||
return `${config.ps1_username}`;
|
||||
};
|
||||
|
||||
export const ls = async (args: string[]): Promise<string> => {
|
||||
return `a
|
||||
bunch
|
||||
of
|
||||
fake
|
||||
directories`;
|
||||
};
|
||||
|
||||
export const cd = async (args: string[]): Promise<string> => {
|
||||
return `unfortunately, i cannot afford more directories.
|
||||
if you want to help, you can type 'donate'.`;
|
||||
};
|
||||
|
||||
export const date = async (args: string[]): Promise<string> => {
|
||||
return new Date().toString();
|
||||
};
|
||||
|
||||
export const vi = async (args: string[]): Promise<string> => {
|
||||
return `woah, you still use 'vi'? just try 'vim'.`;
|
||||
};
|
||||
|
||||
export const vim = async (args: string[]): Promise<string> => {
|
||||
return `'vim' is so outdated. how about 'nvim'?`;
|
||||
};
|
||||
|
||||
export const nvim = async (args: string[]): Promise<string> => {
|
||||
return `'nvim'? too fancy. why not 'emacs'?`;
|
||||
};
|
||||
|
||||
export const emacs = async (args?: string[]): Promise<string> => {
|
||||
return `you know what? just use vscode.`;
|
||||
};
|
||||
|
||||
export const sudo = async (args?: string[]): Promise<string> => {
|
||||
window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank'); // good ol' rick roll
|
||||
return `Permission denied: with little power comes... no responsibility? `;
|
||||
};
|
||||
|
||||
// Banner
|
||||
export const banner = (args?: string[]): string => {
|
||||
return `
|
||||
█████ ███ ███████████
|
||||
░░███ ░░░ ░█░░░███░░░█
|
||||
░███ ████ █████ █████ ██████ ░ ░███ ░ ██████ ████████ █████████████
|
||||
░███ ░░███ ░░███ ░░███ ███░░███ ░███ ███░░███░░███░░███░░███░░███░░███
|
||||
░███ ░███ ░███ ░███ ░███████ ░███ ░███████ ░███ ░░░ ░███ ░███ ░███
|
||||
░███ █ ░███ ░░███ ███ ░███░░░ ░███ ░███░░░ ░███ ░███ ░███ ░███
|
||||
███████████ █████ ░░█████ ░░██████ █████ ░░██████ █████ █████░███ █████
|
||||
░░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░ ░░░ ░░░░░
|
||||
Type 'help' to see the list of available commands.
|
||||
Type 'sumfetch' to display summary.
|
||||
Type 'gui' or click <u><a class="text-light-blue dark:text-dark-blue underline" href="${config.non_terminal_url}" target="_blank">here</a></u> for a simpler version.
|
||||
`;
|
||||
};
|
@ -0,0 +1,3 @@
|
||||
export * from './commands';
|
||||
export * from './api_commands';
|
||||
export { default as sumfetch } from './sumfetch';
|
@ -0,0 +1,6 @@
|
||||
import * as bin from './bin';
|
||||
|
||||
export const commandExists = (command: string) => {
|
||||
const commands = ['clear', ...Object.keys(bin)];
|
||||
return commands.indexOf(command.split(' ')[0].toLowerCase()) !== -1;
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import * as bin from './bin';
|
||||
|
||||
export const shell = async (
|
||||
command: string,
|
||||
setHistory: (value: string) => void,
|
||||
clearHistory: () => void,
|
||||
setCommand: React.Dispatch<React.SetStateAction<string>>,
|
||||
) => {
|
||||
const args = command.split(' ');
|
||||
args[0] = args[0].toLowerCase();
|
||||
|
||||
if (args[0] === 'clear') {
|
||||
clearHistory();
|
||||
} else if (command === '') {
|
||||
setHistory('');
|
||||
} else if (Object.keys(bin).indexOf(args[0]) === -1) {
|
||||
setHistory(
|
||||
`shell: command not found: ${args[0]}. Try 'help' to get started.`,
|
||||
);
|
||||
} else {
|
||||
const output = await bin[args[0]](args.slice(1));
|
||||
setHistory(output);
|
||||
}
|
||||
|
||||
setCommand('');
|
||||
};
|
@ -0,0 +1,14 @@
|
||||
import * as bin from './bin';
|
||||
|
||||
export const handleTabCompletion = (
|
||||
command: string,
|
||||
setCommand: React.Dispatch<React.SetStateAction<string>>,
|
||||
) => {
|
||||
const commands = Object.keys(bin).filter((entry) =>
|
||||
entry.startsWith(command),
|
||||
);
|
||||
|
||||
if (commands.length === 1) {
|
||||
setCommand(commands[0]);
|
||||
}
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
const { colors } = require('./config.json');
|
||||
|
||||
module.exports = {
|
||||
content: [
|
||||
'./src/pages/**/*.{js,ts,jsx,tsx}',
|
||||
'./src/components/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
darkMode: 'media', // or 'media' or 'class'
|
||||
theme: {
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
...colors,
|
||||
},
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
@ -0,0 +1,162 @@
|
||||
{
|
||||
"default": {
|
||||
"light": {
|
||||
"background": "#FBF1C9",
|
||||
"foreground": "#3C3836",
|
||||
"yellow": "#D79921",
|
||||
"green": "#98971A",
|
||||
"gray": "#7C6F64",
|
||||
"blue": "#458588",
|
||||
"red": "#CA2124"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#2E3440",
|
||||
"foreground": "#E5E9F0",
|
||||
"yellow": "#5E81AC",
|
||||
"green": "#A3BE8C",
|
||||
"gray": "#88C0D0",
|
||||
"blue": "#EBCB8B",
|
||||
"red": "#BF616A"
|
||||
}
|
||||
},
|
||||
"gruvbox": {
|
||||
"light": {
|
||||
"background": "#FBF1C9",
|
||||
"foreground": "#3C3836",
|
||||
"yellow": "#D79921",
|
||||
"green": "#98971A",
|
||||
"gray": "#7C6F64",
|
||||
"blue": "#458588",
|
||||
"red": "#CA2124"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#1c1c1c",
|
||||
"foreground": "#EBDBB2",
|
||||
"yellow": "#D79921",
|
||||
"green": "#98971A",
|
||||
"gray": "#A89984",
|
||||
"blue": "#458588",
|
||||
"red": "#CA2124"
|
||||
}
|
||||
},
|
||||
"dracula": {
|
||||
"light": {
|
||||
"background": "#FFFFDB",
|
||||
"foreground": "#282a36",
|
||||
"yellow": "#ffb86c",
|
||||
"green": "#50fa7b",
|
||||
"gray": "#8B6BB9",
|
||||
"blue": "#67AFC0",
|
||||
"red": "#ff5555"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#282a36",
|
||||
"foreground": "#f8f8f2",
|
||||
"yellow": "#ffb86c",
|
||||
"green": "#50fa7b",
|
||||
"gray": "#bd93f9",
|
||||
"blue": "#8be9fd",
|
||||
"red": "#ff5555"
|
||||
}
|
||||
},
|
||||
"Nord": {
|
||||
"light": {
|
||||
"background": "#E5E9F0",
|
||||
"foreground": "#2E3440",
|
||||
"yellow": "#5E81AC",
|
||||
"green": "#A3BE8C",
|
||||
"gray": "#88C0D0",
|
||||
"blue": "#EBCB8B",
|
||||
"red": "#BF616A"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#2E3440",
|
||||
"foreground": "#E5E9F0",
|
||||
"yellow": "#5E81AC",
|
||||
"green": "#A3BE8C",
|
||||
"gray": "#88C0D0",
|
||||
"blue": "#EBCB8B",
|
||||
"red": "#BF616A"
|
||||
}
|
||||
},
|
||||
"Monokai": {
|
||||
"light": {
|
||||
"background": "#F8F8F2",
|
||||
"foreground": "#272822",
|
||||
"yellow": "#F4BF75",
|
||||
"green": "#A6E22E",
|
||||
"gray": "#AE81FF",
|
||||
"blue": "#66D9EF",
|
||||
"red": "#F92672"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#272822",
|
||||
"foreground": "#F8F8F2",
|
||||
"yellow": "#F4BF75",
|
||||
"green": "#A6E22E",
|
||||
"gray": "#AE81FF",
|
||||
"blue": "#66D9EF",
|
||||
"red": "#F92672"
|
||||
}
|
||||
},
|
||||
"Mocha": {
|
||||
"light": {
|
||||
"background": "#D0C8C6",
|
||||
"foreground": "#3B3228",
|
||||
"yellow": "#F4BC87",
|
||||
"green": "#BEB55B",
|
||||
"gray": "#A89BB9",
|
||||
"blue": "#8AB3B5",
|
||||
"red": "#CB6077"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#3B3228",
|
||||
"foreground": "#D0C8C6",
|
||||
"yellow": "#F4BC87",
|
||||
"green": "#BEB55B",
|
||||
"gray": "#A89BB9",
|
||||
"blue": "#8AB3B5",
|
||||
"red": "#CB6077"
|
||||
}
|
||||
},
|
||||
"Solarized": {
|
||||
"light": {
|
||||
"background": "#FDF6E3",
|
||||
"foreground": "#586E75",
|
||||
"yellow": "#B58900",
|
||||
"green": "#859900",
|
||||
"gray": "#6C71C4",
|
||||
"blue": "#268BD2",
|
||||
"red": "#DC322F"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#002B36",
|
||||
"foreground": "#93A1A1",
|
||||
"yellow": "#B58900",
|
||||
"green": "#859900",
|
||||
"gray": "#6C71C4",
|
||||
"blue": "#268BD2",
|
||||
"red": "#DC322F"
|
||||
}
|
||||
},
|
||||
"Paraiso": {
|
||||
"light": {
|
||||
"background": "#A39E9B",
|
||||
"foreground": "#2F1E2E",
|
||||
"yellow": "#FEC418",
|
||||
"green": "#48B685",
|
||||
"gray": "#815BA4",
|
||||
"blue": "#06B6EF",
|
||||
"red": "#EF6155"
|
||||
},
|
||||
"dark": {
|
||||
"background": "#2F1E2E",
|
||||
"foreground": "#A39E9B",
|
||||
"yellow": "#FEC418",
|
||||
"green": "#48B685",
|
||||
"gray": "#815BA4",
|
||||
"blue": "#06B6EF",
|
||||
"red": "#EF6155"
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"github": {
|
||||
"silent": true
|
||||
}
|
||||
}
|