From fb94381983bd05ef004bbfb5fab6cc7e7fbab586 Mon Sep 17 00:00:00 2001 From: n0rrman Date: Mon, 13 May 2024 23:01:40 +0700 Subject: [PATCH] new commands & code refactor --- package.json | 1 + src/actions/action-codes.ts | 8 +++ src/actions/commands.json | 32 +++++++++ src/actions/commands.ts | 32 --------- src/actions/index.ts | 74 +++++++++++++++----- src/app/fonts.tsx | 4 +- src/app/layout.tsx | 8 +-- src/app/page.tsx | 16 ++--- src/components/terminal-screen.tsx | 69 ++++++++++++++++++ src/components/terminal.tsx | 108 +++++++++++------------------ tailwind.config.ts | 8 ++- tsconfig.json | 1 + yarn.lock | 5 ++ 13 files changed, 228 insertions(+), 138 deletions(-) create mode 100644 src/actions/action-codes.ts create mode 100644 src/actions/commands.json delete mode 100644 src/actions/commands.ts create mode 100644 src/components/terminal-screen.tsx diff --git a/package.json b/package.json index 162772d..ab03bf9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "next": "14.0.4", "react": "^18", "react-dom": "^18", + "react-icons": "^5.2.1", "zod": "^3.23.4" }, "devDependencies": { diff --git a/src/actions/action-codes.ts b/src/actions/action-codes.ts new file mode 100644 index 0000000..6caf5b1 --- /dev/null +++ b/src/actions/action-codes.ts @@ -0,0 +1,8 @@ +export enum ActionCode { + NOOP = "noop", + CLEAR = "clear", + REDIRECT = "redirect", + ERROR = "error", + EXIT = "exit", + HELP = "help", +} \ No newline at end of file diff --git a/src/actions/commands.json b/src/actions/commands.json new file mode 100644 index 0000000..fdd701e --- /dev/null +++ b/src/actions/commands.json @@ -0,0 +1,32 @@ +[ + { + "cmd": "whoami", + "value": "Hello, World! πŸ‘‹ I'm \n \n β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \nβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘ β–‘β–‘β–ˆβ–ˆβ–ˆ \n β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \n β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ \n β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘ \n β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–‘ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ \n β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \nβ–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ \n \n β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \nβ–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆ \n β–‘β–ˆβ–ˆβ–ˆβ–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \n β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘β–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ \n β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘ β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ \n β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ \n β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ\nβ–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ \n \na software engineer based in Stockholm, Sweden πŸ‡ΈπŸ‡ͺ \n", + "actionCode": "noop" + }, + { + "cmd": "clear", + "value": "Clearning terminal...", + "actionCode": "clear" + }, + { + "cmd": "github", + "value": "https://github.com/n0rrman", + "actionCode": "redirect" + }, + { + "cmd": "source code", + "value": "https://github.com/n0rrman/henriknorrman.com", + "actionCode": "redirect" + }, + { + "cmd": "exit", + "value": "\nSaving session...completed.", + "actionCode": "exit" + }, + { + "cmd": "help", + "value": "These shell commands are defined internally. Type `help' to see this list.\n\twhoami\t\t\tdisplay effective user\n\tclear\t\t\tclear the terminal screen\n\tgithub\t\t\topen my github\n\tsource code\t\topen the source code repository\n\thelp\t\t\tlist available commands\n\texit\t\t\texit the terminal", + "actionCode": "help" + } +] \ No newline at end of file diff --git a/src/actions/commands.ts b/src/actions/commands.ts deleted file mode 100644 index 48be10c..0000000 --- a/src/actions/commands.ts +++ /dev/null @@ -1,32 +0,0 @@ -export default function callCommand(cmd: string) { - - if (cmd === "whoami") - return ( - "Hello, World! πŸ‘‹ I'm \n"+ - " \n"+ - " β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \n"+ - "β–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘ β–‘β–‘β–ˆβ–ˆβ–ˆ \n"+ - " β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \n"+ - " β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ \n"+ - " β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘ \n"+ - " β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–‘ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ \n"+ - " β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \n"+ - "β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ \n"+ - " \n"+ - " β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \n"+ - "β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆ \n"+ - " β–‘β–ˆβ–ˆβ–ˆβ–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ \n"+ - " β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘β–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ \n"+ - " β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘ β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–‘ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ \n"+ - " β–‘β–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ β–‘β–ˆβ–ˆβ–ˆ \n"+ - " β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆ\n"+ - "β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘ β–‘β–‘β–‘β–‘β–‘ \n"+ - " \n"+ - "a software engineer based in Stockholm, Sweden πŸ‡ΈπŸ‡ͺ \n" - ); - - - else - return `cmd not found: ${cmd}` - -} \ No newline at end of file diff --git a/src/actions/index.ts b/src/actions/index.ts index 9a1953e..0922402 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -2,41 +2,81 @@ import { z } from "zod"; import { randomUUID } from 'crypto' -import callCommand from "./commands"; +import { promises as fs } from 'fs'; + +import { ActionCode } from "./action-codes"; const terminalSchema = z.object({ cmd: z.string().min(1).max(128), }); +interface Command { + cmd: string; + value: string; + description: string; + actionCode: ActionCode; +} + +export interface CommandAction { + id: string; + input: string; + output: string; + actionCode: ActionCode; +} + interface TerminalState { - payload?: { - id: string; - input: string; - output: string; + payload?: CommandAction +} + +async function callCommand(id: string, input: string, errorState: TerminalState) { + // Load command file to JSON + // success -> continue + // error -> return error state + let data: [Command]; + try { + const file = await fs.readFile(process.cwd() + '/src/actions/commands.json', 'utf8'); + data = (JSON.parse(file) as [Command]); + } catch (err: unknown) { + return ({payload: {...errorState.payload, output: "Error: internal error"}} as TerminalState); + } + + // Get command + const command = data.find(({cmd}: Command) => cmd === input); + + // -> Command not found + if (!command) { + return { + payload: {id, input, output: `${input}: command not found. Type 'help' for list of commands.`, actionCode: ActionCode.NOOP} + } + } + + // -> Command found + return { + payload: { + id, input, output: command!.value, actionCode: command!.actionCode + } } } export async function terminalState(formState: TerminalState, formData: FormData): Promise { - const result = terminalSchema.safeParse({ cmd: formData.get("cmd"), }); const input = formData.get("cmd")!.toString(); const id = randomUUID(); - - if(!result.success) { - return { - payload: { - id, input, output: "" - } + + // Default error state + const errorState = { + payload: { + id, input, output: "", actionCode: ActionCode.ERROR } } - return { - payload: { - id, input, output: callCommand(input) - } + // Input error -> return blank output + if(!result.success) { + return {payload: {...errorState.payload, output: ""}}; } + + return callCommand(id, input, errorState); } - \ No newline at end of file diff --git a/src/app/fonts.tsx b/src/app/fonts.tsx index 168947a..ca1066b 100644 --- a/src/app/fonts.tsx +++ b/src/app/fonts.tsx @@ -1,7 +1,7 @@ import { Montserrat, Source_Code_Pro } from "next/font/google"; import localFont from "next/font/local"; -export const courier = localFont({ src: "./courier.woff2" }); +export const courier = localFont({ src: "courier.woff2" }); export const code = Source_Code_Pro({ subsets: ["latin"], @@ -10,5 +10,5 @@ export const code = Source_Code_Pro({ export const main = Montserrat({ subsets: ["latin"], - weight: ["400"], + weight: ["400", "700"], }); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d4ddaad..bf99053 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -16,12 +16,6 @@ export const metadata: Metadata = { }, }; -export const viewport: Viewport = { - width: "device-width", - initialScale: 1, - maximumScale: 1, -}; - export default function RootLayout({ children, }: { @@ -29,7 +23,7 @@ export default function RootLayout({ }) { return ( - {children} + {children} ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 778900e..6af4f33 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,23 +1,15 @@ import Image from "next/image"; -import background from "/public/background.svg"; import logo from "/public/logo.svg"; import Terminal from "@/components/terminal"; export default function LandingPage() { return ( -
-
- -
-
- {/*
- -
*/} -
-
- +
+
+
+
); } diff --git a/src/components/terminal-screen.tsx b/src/components/terminal-screen.tsx new file mode 100644 index 0000000..cef97e8 --- /dev/null +++ b/src/components/terminal-screen.tsx @@ -0,0 +1,69 @@ +import Image from "next/image"; + +import screenImg from "/public/screen.svg"; +import { courier } from "../app/fonts"; +import { CommandAction } from "@/actions"; + +interface TerminalScreenProps { + prompt: React.ReactElement; + outputs: CommandAction[]; + inputRef: React.RefObject; + formRef: React.RefObject; + submitHandler: (event: React.FormEvent) => void; +} + +export default function TerminalScreen({ + prompt, + outputs, + inputRef, + formRef, + submitHandler, +}: TerminalScreenProps) { + const renderedOutputs = outputs.map( + ({ id, input, output }: CommandAction) => { + return ( +
+

+ {prompt} + {input} +

+ {output !== "" &&

{output}

} +
+ ); + } + ); + + return ( +
{ + inputRef!.current!.focus(); + }} + > +
+
+ +
+
+
+
+
{renderedOutputs}
+
+ {prompt} + +
+
+
+
+ ); +} diff --git a/src/components/terminal.tsx b/src/components/terminal.tsx index 074dcba..2d433cd 100644 --- a/src/components/terminal.tsx +++ b/src/components/terminal.tsx @@ -1,54 +1,54 @@ "use client"; -import Image from "next/image"; - -import { courier } from "../app/fonts"; - -import screenImg from "/public/screen.svg"; import { useFormState } from "react-dom"; import { useState, startTransition, useRef, useEffect } from "react"; -import { terminalState } from "@/actions"; +import { redirect } from "next/navigation"; + +import { terminalState, CommandAction } from "@/actions"; +import { ActionCode } from "@/actions/action-codes"; +import TerminalScreen from "./terminal-screen"; export default function Terminal() { - const [outputs, setOutputs] = useState([{ id: "0", input: "", output: "" }]); - const [input, setInput] = useState("whoami"); + const inputRef = useRef(null); + const formRef = useRef(null); - const formRef = useRef(null); - const inputRef = useRef(null); + const [outputs, setOutputs] = useState([]); const [formState, action] = useFormState(terminalState, {}); - const prompt = henriknorrman.com/>; - + // Initial whoami command useEffect(() => { - formRef.current!.requestSubmit(); + inputRef.current!.value = "whoami"; inputRef.current!.focus(); + formRef.current!.requestSubmit(); }, []); + // On new submit -> add to outputs & handle command useEffect(() => { if (formState.payload) { - const { id, input, output } = formState.payload; - setOutputs((outputs) => [...outputs, { id, input, output }]); + const { id, input, output, actionCode } = formState.payload; + + setOutputs((outputs) => [...outputs!, { id, input, output, actionCode }]); + handleTerminalAction(output, actionCode); } }, [formState]); - const renderedOutputs = outputs.map(({ id, input, output }) => { - return ( -
-

- {prompt} - {input} -

- {output !== "" &&

{output}

} -
- ); - }); + // Handle action codes + const handleTerminalAction = (value: string, actionCode: string) => { + if (actionCode === ActionCode.CLEAR) { + setOutputs([]); + } + if (actionCode === ActionCode.REDIRECT) { + redirect(value); + } + if (actionCode === ActionCode.EXIT) { + setOutputs([]); + window.history.back(); + } + }; - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - setInput(""); + // Input submit handler + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); if (formRef.current) { const formData = new FormData(formRef.current); @@ -57,43 +57,17 @@ export default function Terminal() { action(formData); }); } + inputRef!.current!.value = ""; }; + const prompt = henriknorrman.com/>; return ( -
-
{ - inputRef.current?.focus(); - }} - className="flex justify-center items-center absolute overflow-hidden" - > -
- -
-
-
{ - inputRef.current?.focus(); - }} - className={`${courier.className} px-[1.5ch] py-[0.5ch] mb-[13ch] bg-slate-800 text-slate-200 flex items-end w-[90ch] leading-snug tracking-wide h-[57ch] overflow-hidden`} - > -
-
{renderedOutputs}
-
- {prompt} - { - setInput(event.target.value); - }} - name="cmd" - type="text" - /> -
-
-
-
+ ); } diff --git a/tailwind.config.ts b/tailwind.config.ts index a6f0126..2c5101d 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -6,7 +6,13 @@ const config: Config = { './src/components/**/*.{js,ts,jsx,tsx,mdx}', './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], - theme: {}, + theme: { + extend: { + screens: { + 'sm': '711px' + } + } + }, plugins: [], } export default config diff --git a/tsconfig.json b/tsconfig.json index e59724b..7deb671 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "isolatedModules": true, "jsx": "preserve", "incremental": true, + "plugins": [ { "name": "next" diff --git a/yarn.lock b/yarn.lock index 33d9bf6..f42a0d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1970,6 +1970,11 @@ react-dom@^18: loose-envify "^1.1.0" scheduler "^0.23.0" +react-icons@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-5.2.1.tgz#28c2040917b2a2eda639b0f797bff1888e018e4a" + integrity sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw== + react-is@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"