Initial commit (history reset)
3
.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
40
.gitignore
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
5
.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
15
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
|
@ -0,0 +1,15 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="ignoredPackages">
|
||||
<value>
|
||||
<list size="1">
|
||||
<item index="0" class="java.lang.String" itemvalue="psycopg2" />
|
||||
</list>
|
||||
</value>
|
||||
</option>
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
8
.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/ronniie.dev.iml" filepath="$PROJECT_DIR$/.idea/ronniie.dev.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
12
.idea/ronniie.dev.iml
generated
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
108
README.md
Normal file
|
@ -0,0 +1,108 @@
|
|||
# Ronniie.dev
|
||||
|
||||
A fully interactive, terminal-style web application built with **Next.js**. This project features dynamic social links, self-hosted tools, command history navigation, and a stylish welcome message.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
- **Interactive Terminal:**
|
||||
- Accepts user input commands like `help` and `socials`.
|
||||
- Supports command history navigation with `↑` and `↓` arrow keys.
|
||||
|
||||
- **Dynamic MOTD (Message of the Day):**
|
||||
- Includes a welcome message with styled text.
|
||||
- Highlights developer information and self-hosted services.
|
||||
|
||||
- **Command List:**
|
||||
- `help`: Displays a list of available commands.
|
||||
- `socials`: Displays links to GitHub, YouTube, Discord, Reddit, and BlueSky profiles.
|
||||
- More coming soon!
|
||||
|
||||
- **Tech Stack:**
|
||||
- **Next.js** for SSR and frontend.
|
||||
- **React Icons** for elegant icons.
|
||||
- **TailwindCSS** for modern styling.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Follow these steps to set up and run the project locally:
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- [Node.js](https://nodejs.org/) (version 16 or higher)
|
||||
- [npm](https://www.npmjs.com/) or [yarn](https://yarnpkg.com/)
|
||||
|
||||
### Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Ronniie/ronniie.dev.git
|
||||
cd ronniie.dev
|
||||
```
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### Development Server
|
||||
|
||||
To start the development server, run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
The application will be available at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
---
|
||||
|
||||
## Styling
|
||||
|
||||
This project uses **TailwindCSS** for styling. The terminal has a dark theme with highlighted text elements to provide a sleek and modern appearance.
|
||||
|
||||
### Tailwind Configuration
|
||||
To customize the styles, edit the `tailwind.config.js` file.
|
||||
|
||||
---
|
||||
|
||||
## Dynamic Icons
|
||||
|
||||
Dynamic importing of icons from `react-icons` ensures minimal client-side overhead. The following icons are used:
|
||||
|
||||
- **Socials:**
|
||||
- GitHub, BlueSky, Discord, YouTube, Reddit
|
||||
- **Self-Hosted Services:**
|
||||
- Docker, Plex, Proxmox, Home Assistant, Paperless-ng
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
If you'd like to contribute:
|
||||
1. Fork the repository.
|
||||
2. Create a feature branch: `git checkout -b feature-name`.
|
||||
3. Commit your changes: `git commit -m "Add feature-name"`.
|
||||
4. Push to the branch: `git push origin feature-name`.
|
||||
5. Open a pull request.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
||||
|
||||
---
|
||||
|
||||
Enjoy using the terminal and let me know how it works for you! 🚀
|
7
next.config.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
5562
package-lock.json
generated
Normal file
27
package.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "ronniie.dev",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "15.0.3",
|
||||
"react": "19.0.0-rc-66855b96-20241106",
|
||||
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||
"react-icons": "^5.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
8
postcss.config.mjs
Normal file
|
@ -0,0 +1,8 @@
|
|||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
84
public/StonkManager.js
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Completely experimental Stock Market manager.
|
||||
|
||||
/*
|
||||
// Get current resource averages. Run whenever curious. Displays Ticker Symbol, % Change In Last Tick, Current Average in console.
|
||||
// You will have to copy this code into the console separately.
|
||||
for (i = 0; i < resourceAverage.length; i++) {
|
||||
console.log(document.querySelector("#bankGood-" + i + " > div:nth-child(1) > div:nth-child(2)").innerText + " " + resourceAverage[i]);
|
||||
}
|
||||
*/
|
||||
|
||||
// Stops the machine from marching forward.
|
||||
clearInterval(StonkManager);
|
||||
|
||||
// Begins tracking the number of ticks which have occurred since starting the script. Useful for long averages.
|
||||
var ticks = 1;
|
||||
|
||||
// Initializes resource values for averaging over time
|
||||
var resourceAverage = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
|
||||
for (i = 0; i < resourceAverage.length; i++) {
|
||||
var currValDollar = document.querySelector("#bankGood-" + i + "-val").innerText;
|
||||
var currVal = 1*currValDollar.substring(1);
|
||||
resourceAverage[i] = currVal;
|
||||
}
|
||||
|
||||
//These set how wide a swing in price you want.
|
||||
// The multiplier for the max price to buy. Will be multiplied against running average price.
|
||||
var buyMaxMult = .8;
|
||||
|
||||
// The multiplier for min price to sell. Will be multiplied against running average price.
|
||||
var sellMinMult = 1.2;
|
||||
|
||||
|
||||
// Timer for Stonks
|
||||
var StonkManager = setInterval(function() {
|
||||
|
||||
Stonks();
|
||||
|
||||
}, 1000);
|
||||
|
||||
// Controller
|
||||
function Stonks(){
|
||||
if (document.querySelector("#bankNextTick").innerText == "Next tick in 55 seconds.") {
|
||||
if (ticks < 2000) {ticks++};
|
||||
updateAverages(ticks);
|
||||
BuySell();
|
||||
}
|
||||
};
|
||||
|
||||
// Updates the knowledge of the stock's averages over time
|
||||
function updateAverages(ticks){
|
||||
var i;
|
||||
for (i = 0; i < resourceAverage.length; i++) {
|
||||
var currValDollar = document.querySelector("#bankGood-" + i + "-val").innerText;
|
||||
var currVal = 1*currValDollar.substring(1);
|
||||
resourceAverage[i] = (currVal - resourceAverage[i]) * (1 / ticks) + resourceAverage[i];
|
||||
}
|
||||
};
|
||||
|
||||
// Controls the Buy/Sell logic.
|
||||
function BuySell(){
|
||||
var i;
|
||||
for (i = 0; i < resourceAverage.length; i++) {
|
||||
var currValDollar = document.querySelector("#bankGood-" + i + "-val").innerText;
|
||||
var currVal = 1*currValDollar.substring(1);
|
||||
if (currVal < (resourceAverage[i] * buyMaxMult)) {
|
||||
buyResource(i);
|
||||
}
|
||||
else if (currVal > (resourceAverage[i] * sellMinMult)) {
|
||||
sellResource(i);
|
||||
}
|
||||
else {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Purchases a resource
|
||||
function buyResource(resNum){
|
||||
document.querySelector("#bankGood-" + resNum + "_Max").click();
|
||||
};
|
||||
|
||||
// Sells a resource
|
||||
function sellResource(resNum){
|
||||
document.querySelector("#bankGood-" + resNum + "_-All").click();
|
||||
};
|
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 98 KiB |
1
public/file.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
After Width: | Height: | Size: 391 B |
1
public/globe.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
After Width: | Height: | Size: 1 KiB |
16
public/manifest.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "ronniie.dev",
|
||||
"short_name": "ronniie.dev",
|
||||
"description": "A showcase of experimental projects hosted on *.ronniie.dev, and some fun tools.",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#f97316",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/favicon.png",
|
||||
"sizes": "any",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
]
|
||||
}
|
1
public/next.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
After Width: | Height: | Size: 1.3 KiB |
1
public/vercel.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
After Width: | Height: | Size: 128 B |
1
public/window.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
After Width: | Height: | Size: 385 B |
45
src/app/commands/MOTD.tsx
Normal file
|
@ -0,0 +1,45 @@
|
|||
const motdCommand = () => {
|
||||
return [
|
||||
<div key="motd" className="font-mono">
|
||||
<div className="text-orange-500">
|
||||
{` _____ _ _ _ `}
|
||||
{`\n | __ \\ (_|_) | | `}
|
||||
{`\n | |__) |___ _ __ _ __ _ _ ___ __| | _____ __`}
|
||||
{`\n | _ // _ \\| '_ \\| '_ \\| | |/ _ \\ / _\` |/ _ \\ \\ / /`}
|
||||
{`\n | | \\ \\ (_) | | | | | | | | | __/| (_| | __/\\ V / `}
|
||||
{`\n |_| \\_\\___/|_| |_|_| |_|_|_|\\___(_)__,_|\\___| \\_/ `}
|
||||
</div>
|
||||
<div className="text-orange-400 mt-4">
|
||||
<strong>Welcome to Ronnie's Project Terminal!</strong>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700 my-4"/>
|
||||
<strong className="text-orange-500">🚀 ABOUT THIS TERMINAL</strong>
|
||||
<p className="text-gray-400">
|
||||
This is a showcase of my experimental projects hosted on *.ronniie.dev.
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
Each project is a unique experiment, from fun visualizations to useful tools.
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
Feel free to explore and interact with them!
|
||||
</p>
|
||||
|
||||
<div className="border-t border-gray-700 my-4"/>
|
||||
<p className="text-orange-400">
|
||||
🌟 "Every project is an adventure in learning and creation." 🌟
|
||||
</p>
|
||||
|
||||
<div className="border-t border-gray-700 my-4"/>
|
||||
<div className="text-gray-400 mt-2">
|
||||
Type <span className="text-orange-500">'help'</span> to see available commands. Let's explore
|
||||
together!
|
||||
</div>
|
||||
<div className="text-gray-400 mt-2">
|
||||
Want to learn more about me? Visit <a href="https://ronniie.com" target="_blank" rel="noopener noreferrer" className="text-orange-400 hover:text-orange-300">ronniie.com</a> for my full portfolio and blog.
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
};
|
||||
|
||||
export default motdCommand;
|
15
src/app/commands/clear.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { Command } from "../../types/command";
|
||||
|
||||
const clearCommand: Command = {
|
||||
metadata: {
|
||||
name: "clear",
|
||||
description: "Clear the terminal screen",
|
||||
icon: "🧹",
|
||||
},
|
||||
execute: () => {
|
||||
// The actual clearing is handled in the Terminal component
|
||||
return [];
|
||||
},
|
||||
};
|
||||
|
||||
export default clearCommand;
|
28
src/app/commands/date.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { Command } from "../../types/command";
|
||||
|
||||
const dateCommand: Command = {
|
||||
metadata: {
|
||||
name: "date",
|
||||
description: "Display the current date and time",
|
||||
icon: "📅",
|
||||
},
|
||||
execute: () => {
|
||||
const now = new Date();
|
||||
return [
|
||||
<div key="date" className="text-gray-400">
|
||||
{now.toLocaleString('en-US', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: 'short'
|
||||
})}
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default dateCommand;
|
29
src/app/commands/echo.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Command } from "../../types/command";
|
||||
|
||||
const echoCommand: Command = {
|
||||
metadata: {
|
||||
name: "echo",
|
||||
description: "Print text to the terminal",
|
||||
icon: "📢",
|
||||
usage: "echo <text>",
|
||||
},
|
||||
execute: (args: string[] = []) => {
|
||||
if (args.length === 0) {
|
||||
return [
|
||||
<div key="echo-error" className="text-red-500">
|
||||
Error: Missing arguments
|
||||
<div className="text-gray-400 mt-1">
|
||||
Usage: echo <text>
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
return [
|
||||
<div key="echo" className="text-gray-400">
|
||||
{args.join(" ")}
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default echoCommand;
|
20
src/app/commands/exit.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Command } from "../../types/command";
|
||||
|
||||
const exitCommand: Command = {
|
||||
metadata: {
|
||||
name: "exit",
|
||||
description: "Close the terminal",
|
||||
icon: "❌",
|
||||
},
|
||||
execute: () => {
|
||||
// This is a special case where we need to handle the exit command differently
|
||||
// The actual execution is handled in the Terminal component
|
||||
return [
|
||||
<div key="exit" className="text-red-500">
|
||||
Terminal exited. Refresh the page to restart. 🚀
|
||||
</div>,
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default exitCommand;
|
104
src/app/commands/help.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import { Command } from "../../types/command";
|
||||
|
||||
const helpCommand: Command = {
|
||||
metadata: {
|
||||
name: "help",
|
||||
description: "Show available commands",
|
||||
icon: "❓",
|
||||
},
|
||||
execute: () => {
|
||||
return [
|
||||
<div key="help" className="text-gray-400">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-bold text-orange-500 mb-2">Available Commands</h2>
|
||||
<p className="text-gray-400">Type any command to execute it. Use ↑ and ↓ to navigate history.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* System Commands */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-orange-500 mb-2">System</h3>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">clear</span>
|
||||
<span className="text-gray-400">- Clear the terminal screen</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">date</span>
|
||||
<span className="text-gray-400">- Display current date and time</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">echo</span>
|
||||
<span className="text-gray-400">- Print text to the terminal</span>
|
||||
</div>
|
||||
<div className="text-gray-500 text-sm ml-6">Usage: echo <text></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">exit</span>
|
||||
<span className="text-gray-400">- Exit the terminal</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">help</span>
|
||||
<span className="text-gray-400">- Show this help message</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">neofetch</span>
|
||||
<span className="text-gray-400">- Display system information</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">whoami</span>
|
||||
<span className="text-gray-400">- Show current user</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File System Commands */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-orange-500 mb-2">File System</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">ls</span>
|
||||
<span className="text-gray-400">- List files and directories</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">pwd</span>
|
||||
<span className="text-gray-400">- Show current directory</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Project Commands */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-orange-500 mb-2">Projects</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">projects</span>
|
||||
<span className="text-gray-400">- Show my projects</span>
|
||||
</div>
|
||||
<div className="text-gray-500 text-sm ml-6">Usage: projects [project-name]</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-orange-500">source</span>
|
||||
<span className="text-gray-400">- View source code</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-gray-700">
|
||||
<p className="text-gray-400">
|
||||
💡 Pro tip: Commands are case-insensitive and support tab completion.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default helpCommand;
|
26
src/app/commands/index.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Command } from "../../types/command";
|
||||
import helpCommand from "./help";
|
||||
import exitCommand from "./exit";
|
||||
import sourceCommand from "./source";
|
||||
import projectsCommand from "./projects";
|
||||
import clearCommand from "./clear";
|
||||
import dateCommand from "./date";
|
||||
import neofetchCommand from "./neofetch";
|
||||
import echoCommand from "./echo";
|
||||
import whoamiCommand from "./whoami";
|
||||
import pwdCommand from "./pwd";
|
||||
import lsCommand from "./ls";
|
||||
|
||||
export const commands: Record<string, Command> = {
|
||||
help: helpCommand,
|
||||
exit: exitCommand,
|
||||
source: sourceCommand,
|
||||
projects: projectsCommand,
|
||||
clear: clearCommand,
|
||||
date: dateCommand,
|
||||
neofetch: neofetchCommand,
|
||||
echo: echoCommand,
|
||||
whoami: whoamiCommand,
|
||||
pwd: pwdCommand,
|
||||
ls: lsCommand,
|
||||
};
|
26
src/app/commands/ls.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { Command } from "../../types/command";
|
||||
import projects from "../../data/projects.json";
|
||||
|
||||
const lsCommand: Command = {
|
||||
metadata: {
|
||||
name: "ls",
|
||||
description: "List files and directories",
|
||||
icon: "📋",
|
||||
},
|
||||
execute: () => {
|
||||
return [
|
||||
<div key="ls" className="text-gray-400">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
{projects.projects.map((project) => (
|
||||
<div key={project.name} className="flex items-center gap-2">
|
||||
<span className="text-orange-500">📁</span>
|
||||
<span>{project.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default lsCommand;
|
133
src/app/commands/neofetch.tsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
import { Command, CommandContext } from "../../types/command";
|
||||
|
||||
// Add type declarations for Navigator properties
|
||||
declare global {
|
||||
interface Navigator {
|
||||
deviceMemory?: number;
|
||||
hardwareConcurrency?: number;
|
||||
platform: string;
|
||||
userAgent: string;
|
||||
language: string;
|
||||
vendor: string;
|
||||
}
|
||||
}
|
||||
|
||||
const neofetchCommand: Command = {
|
||||
metadata: {
|
||||
name: "neofetch",
|
||||
description: "Display system information",
|
||||
icon: "🖥️",
|
||||
},
|
||||
execute: (_, context?: CommandContext) => {
|
||||
const platform = navigator.platform;
|
||||
const userAgent = navigator.userAgent;
|
||||
const language = navigator.language;
|
||||
const vendor = navigator.vendor;
|
||||
const memory = navigator.deviceMemory ? `${navigator.deviceMemory} GB` : "Unknown";
|
||||
const cores = navigator.hardwareConcurrency || "Unknown";
|
||||
|
||||
const osInfo = {
|
||||
platform: platform,
|
||||
userAgent: userAgent,
|
||||
language: language,
|
||||
vendor: vendor,
|
||||
memory: memory,
|
||||
cores: cores,
|
||||
};
|
||||
|
||||
// Enhanced OS detection
|
||||
let os = 'Unknown';
|
||||
let osVersion = '';
|
||||
|
||||
if (osInfo.userAgent.includes('Windows')) {
|
||||
os = 'Windows';
|
||||
if (osInfo.userAgent.includes('Windows NT 10.0')) {
|
||||
// Windows 11 has a specific pattern in the user agent
|
||||
// It includes "Windows NT 10.0" followed by specific identifiers
|
||||
const isWindows11 = osInfo.userAgent.includes('Windows NT 10.0; Win64; x64') &&
|
||||
(osInfo.userAgent.includes('Windows NT 10.0; Win64; x64; Windows NT 10.0') ||
|
||||
osInfo.userAgent.includes('Windows NT 10.0; Win64; x64; rv:') ||
|
||||
osInfo.userAgent.includes('Windows NT 10.0; Win64; x64; Edge/'));
|
||||
|
||||
osVersion = isWindows11 ? '11' : '10';
|
||||
}
|
||||
else if (osInfo.userAgent.includes('Windows NT 6.3')) osVersion = '8.1';
|
||||
else if (osInfo.userAgent.includes('Windows NT 6.2')) osVersion = '8';
|
||||
else if (osInfo.userAgent.includes('Windows NT 6.1')) osVersion = '7';
|
||||
}
|
||||
else if (osInfo.userAgent.includes('Mac')) {
|
||||
os = 'macOS';
|
||||
const match = osInfo.userAgent.match(/Mac OS X (\d+[._]\d+)/);
|
||||
if (match) osVersion = match[1].replace('_', '.');
|
||||
}
|
||||
else if (osInfo.userAgent.includes('Linux')) {
|
||||
os = 'Linux';
|
||||
if (osInfo.userAgent.includes('Ubuntu')) osVersion = 'Ubuntu';
|
||||
else if (osInfo.userAgent.includes('Fedora')) osVersion = 'Fedora';
|
||||
else if (osInfo.userAgent.includes('Debian')) osVersion = 'Debian';
|
||||
else if (osInfo.userAgent.includes('Android')) {
|
||||
os = 'Android';
|
||||
const match = osInfo.userAgent.match(/Android (\d+[._]\d+)/);
|
||||
if (match) osVersion = match[1].replace('_', '.');
|
||||
}
|
||||
}
|
||||
else if (osInfo.userAgent.includes('iOS')) {
|
||||
os = 'iOS';
|
||||
const match = osInfo.userAgent.match(/OS (\d+[._]\d+)/);
|
||||
if (match) osVersion = match[1].replace('_', '.');
|
||||
}
|
||||
|
||||
// Enhanced browser detection
|
||||
let browser = 'Unknown';
|
||||
let browserVersion = '';
|
||||
|
||||
if (osInfo.userAgent.includes('Chrome')) {
|
||||
browser = 'Chrome';
|
||||
const match = osInfo.userAgent.match(/Chrome\/(\d+[._]\d+)/);
|
||||
if (match) browserVersion = match[1].replace('_', '.');
|
||||
}
|
||||
else if (osInfo.userAgent.includes('Firefox')) {
|
||||
browser = 'Firefox';
|
||||
const match = osInfo.userAgent.match(/Firefox\/(\d+[._]\d+)/);
|
||||
if (match) browserVersion = match[1].replace('_', '.');
|
||||
}
|
||||
else if (osInfo.userAgent.includes('Safari')) {
|
||||
browser = 'Safari';
|
||||
const match = osInfo.userAgent.match(/Version\/(\d+[._]\d+)/);
|
||||
if (match) browserVersion = match[1].replace('_', '.');
|
||||
}
|
||||
else if (osInfo.userAgent.includes('Edge')) {
|
||||
browser = 'Edge';
|
||||
const match = osInfo.userAgent.match(/Edge\/(\d+[._]\d+)/);
|
||||
if (match) browserVersion = match[1].replace('_', '.');
|
||||
}
|
||||
|
||||
return [
|
||||
<div key="neofetch" className="font-mono">
|
||||
<div className="flex gap-8">
|
||||
<div className="text-orange-500">
|
||||
{` _____ _ _ _ `}
|
||||
{`\n | __ \\ (_|_) | | `}
|
||||
{`\n | |__) |___ _ __ _ __ _ _ ___ __| | _____ __`}
|
||||
{`\n | _ // _ \\| '_ \\| '_ \\| | |/ _ \\ / _\` |/ _ \\ \\ / /`}
|
||||
{`\n | | \\ \\ (_) | | | | | | | | | __/| (_| | __/\\ V / `}
|
||||
{`\n |_| \\_\\___/|_| |_|_| |_|_|_|\\___(_)__,_|\\___| \\_/ `}
|
||||
</div>
|
||||
<div className="text-gray-400">
|
||||
<div><span className="text-orange-500">OS:</span> {os}{osVersion ? ` ${osVersion}` : ''}</div>
|
||||
<div><span className="text-orange-500">Host:</span> ronniie.dev</div>
|
||||
<div><span className="text-orange-500">Kernel:</span> Next.js 15.0.3</div>
|
||||
<div><span className="text-orange-500">Shell:</span> React Terminal</div>
|
||||
<div><span className="text-orange-500">Terminal:</span> {browser}{browserVersion ? ` ${browserVersion}` : ''}</div>
|
||||
<div><span className="text-orange-500">CPU:</span> {osInfo.cores} cores</div>
|
||||
<div><span className="text-orange-500">Memory:</span> {osInfo.memory}</div>
|
||||
<div><span className="text-orange-500">Uptime:</span> {context?.uptime || 'Unknown'}</div>
|
||||
<div><span className="text-orange-500">Language:</span> {osInfo.language}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default neofetchCommand;
|
98
src/app/commands/projects.tsx
Normal file
|
@ -0,0 +1,98 @@
|
|||
import { Command } from "../../types/command";
|
||||
import projects from "../../data/projects.json";
|
||||
|
||||
const projectsCommand: Command = {
|
||||
metadata: {
|
||||
name: "projects",
|
||||
description: "Show my projects",
|
||||
icon: "🚀",
|
||||
usage: "projects [project-name]",
|
||||
},
|
||||
execute: (args: string[] = []) => {
|
||||
if (args.length === 0) {
|
||||
// Show all projects
|
||||
return [
|
||||
<div key="projects" className="text-orange-500">
|
||||
<div className="mb-4">Available Projects:</div>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{projects.projects.map((project) => (
|
||||
<div key={project.name} className="border border-gray-700 rounded p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-orange-500">{project.icon}</span>
|
||||
<span className="font-bold">{project.name}</span>
|
||||
</div>
|
||||
<p className="text-gray-400 mb-2">{project.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<span key={tag} className="text-sm bg-gray-800 text-gray-300 px-2 py-1 rounded">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
href={project.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-500 hover:text-orange-400"
|
||||
>
|
||||
Visit Project →
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
// Show specific project
|
||||
const projectName = args[0].toLowerCase();
|
||||
const project = projects.projects.find(
|
||||
(p) => p.name.toLowerCase() === projectName
|
||||
);
|
||||
|
||||
if (!project) {
|
||||
return [
|
||||
<div key="project-error" className="text-red-400">
|
||||
Error: Project not found
|
||||
<div className="text-gray-400 mt-1">
|
||||
Available projects: {projects.projects.map(p => p.name).join(", ")}
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
<div key="project-detail" className="text-orange-500">
|
||||
<div className="border border-gray-700 rounded p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<span className="text-2xl">{project.icon}</span>
|
||||
<h2 className="text-2xl font-bold">{project.name}</h2>
|
||||
</div>
|
||||
<p className="text-gray-300 mb-4">{project.description}</p>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-yellow-400 mb-2">Technologies</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{project.tags.map((tag) => (
|
||||
<span key={tag} className="text-sm bg-gray-800 text-gray-300 px-2 py-1 rounded">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<a
|
||||
href={project.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-500 hover:text-orange-400"
|
||||
>
|
||||
Visit Project →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default projectsCommand;
|
18
src/app/commands/pwd.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Command } from "../../types/command";
|
||||
|
||||
const pwdCommand: Command = {
|
||||
metadata: {
|
||||
name: "pwd",
|
||||
description: "Show current directory",
|
||||
icon: "📁",
|
||||
},
|
||||
execute: () => {
|
||||
return [
|
||||
<div key="pwd" className="text-gray-400">
|
||||
/home/ronnie
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default pwdCommand;
|
31
src/app/commands/source.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { Command } from "../../types/command";
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
const FaGithub = dynamic(() => import("react-icons/fa6").then((mod) => mod.FaGithub), { ssr: false });
|
||||
|
||||
const sourceCommand: Command = {
|
||||
metadata: {
|
||||
name: "source",
|
||||
description: "View the source code of this terminal",
|
||||
icon: "📄",
|
||||
},
|
||||
execute: () => [
|
||||
<div key="source">
|
||||
<strong className="text-orange-500">📄 Terminal Source Code:</strong>
|
||||
<div className="text-gray-400 flex items-center mt-2">
|
||||
<FaGithub className="text-gray-400 mr-2"/>
|
||||
GitHub:{" "}
|
||||
<a
|
||||
href="https://github.com/Ronniie/ronniie.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-500 hover:text-orange-400"
|
||||
>
|
||||
Ronniie/ronniie.dev
|
||||
</a>
|
||||
</div>
|
||||
</div>,
|
||||
],
|
||||
};
|
||||
|
||||
export default sourceCommand;
|
10
src/app/commands/unknown.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
const unknownCommand = (input: string) => () => [
|
||||
<div key="unknown-command" className="text-red-500">
|
||||
Unknown command: <span className="font-bold">{input}</span>
|
||||
</div>,
|
||||
<div key="suggestion" className="text-gray-400">
|
||||
Type <span className="text-orange-500">help</span> to see available commands.
|
||||
</div>,
|
||||
];
|
||||
|
||||
export default unknownCommand;
|
18
src/app/commands/whoami.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Command } from "../../types/command";
|
||||
|
||||
const whoamiCommand: Command = {
|
||||
metadata: {
|
||||
name: "whoami",
|
||||
description: "Show current user",
|
||||
icon: "👤",
|
||||
},
|
||||
execute: () => {
|
||||
return [
|
||||
<div key="whoami" className="text-gray-400">
|
||||
ronnie
|
||||
</div>
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default whoamiCommand;
|
BIN
src/app/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/app/fonts/GeistMonoVF.woff
Normal file
BIN
src/app/fonts/GeistVF.woff
Normal file
24
src/app/globals.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
html,body {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
body {
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
74
src/app/layout.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "ronniie.dev",
|
||||
description: "A showcase of experimental projects hosted on *.ronniie.dev. Each project is a unique experiment, from fun visualizations to useful tools.",
|
||||
keywords: "terminal, projects, web development, experiments, tools, utilities",
|
||||
authors: [{ name: "Ronniie" }],
|
||||
creator: "Ronniie",
|
||||
publisher: "Ronniie",
|
||||
robots: "index, follow",
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.png', sizes: 'any' },
|
||||
{ url: '/favicon.png', sizes: '32x32', type: 'image/x-icon' },
|
||||
{ url: '/favicon.png', sizes: '16x16', type: 'image/x-icon' },
|
||||
],
|
||||
apple: [
|
||||
{ url: '/favicon.png', sizes: '180x180', type: 'image/x-icon' },
|
||||
],
|
||||
},
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
url: 'https://ronniie.dev',
|
||||
title: "ronniie.dev",
|
||||
description: "A showcase of experimental projects hosted on *.ronniie.dev. Each project is a unique experiment, from fun visualizations to useful tools.",
|
||||
siteName: "ronniie.dev",
|
||||
images: [
|
||||
{
|
||||
url: '/favicon.png',
|
||||
width: 512,
|
||||
height: 512,
|
||||
alt: 'ronniie.dev favicon',
|
||||
type: 'image/x-icon',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary',
|
||||
title: "ronniie.dev",
|
||||
description: "A showcase of experimental projects hosted on *.ronniie.dev. Each project is a unique experiment, from fun visualizations to useful tools.",
|
||||
creator: '@ronniie',
|
||||
images: ['/favicon.png'],
|
||||
},
|
||||
manifest: '/manifest.json',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className="!scroll-smooth" suppressHydrationWarning>
|
||||
<head>
|
||||
{/* Theme Colors */}
|
||||
<meta name="theme-color" content="#f97316" />
|
||||
<meta name="msapplication-navbutton-color" content="#f97316" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="#f97316" />
|
||||
{/* Favicon */}
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/favicon.png" />
|
||||
</head>
|
||||
<body className={inter.className} suppressHydrationWarning>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
9
src/app/page.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import Terminal from "../components/Terminal";
|
||||
|
||||
export default function TerminalPage() {
|
||||
return (
|
||||
<div className="bg-black text-white w-full h-screen" suppressHydrationWarning >
|
||||
<Terminal />
|
||||
</div>
|
||||
);
|
||||
}
|
1
src/app/theme-color.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export default "#f97316";
|
4
src/app/viewport.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export default {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
298
src/components/Terminal.tsx
Normal file
|
@ -0,0 +1,298 @@
|
|||
"use client";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import { commands } from "../app/commands";
|
||||
import unknownCommand from "../app/commands/unknown";
|
||||
import projects from "../data/projects.json";
|
||||
|
||||
interface Project {
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const Terminal: React.FC = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const [output, setOutput] = useState<JSX.Element[]>([]);
|
||||
const [history, setHistory] = useState<string[]>([]);
|
||||
const [historyIndex, setHistoryIndex] = useState<number | null>(null);
|
||||
const [suggestion, setSuggestion] = useState<string | null>(null);
|
||||
const [inputDisabled, setInputDisabled] = useState(false);
|
||||
const [startTime] = useState<Date>(new Date());
|
||||
|
||||
const terminalEndRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const commandsList = Object.keys(commands);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
setOutput([<MOTD key="motd" />]);
|
||||
}, []);
|
||||
|
||||
const getUptime = () => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - startTime.getTime();
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours % 24}h ${minutes % 60}m`;
|
||||
} else if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m ${seconds % 60}s`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
} else {
|
||||
return `${seconds}s`;
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (inputDisabled) return;
|
||||
|
||||
if (input.trim() === "") {
|
||||
setOutput((prev) => [
|
||||
...prev,
|
||||
<div key={`blank-${prev.length}`}> </div>,
|
||||
]);
|
||||
setInput("");
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedInput = input.toLowerCase();
|
||||
// Split by spaces but preserve quoted strings
|
||||
const args = input.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
||||
if (args.length === 0) {
|
||||
setInput("");
|
||||
return;
|
||||
}
|
||||
const cmd = args[0]?.toLowerCase() || "";
|
||||
const commandArgs = args.slice(1).map(arg => arg.replace(/^"|"$/g, '')); // Remove quotes
|
||||
const command = commands[cmd];
|
||||
|
||||
if (normalizedInput === "clear") {
|
||||
setOutput([]);
|
||||
setInput("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (command) {
|
||||
if (normalizedInput === "exit") {
|
||||
setInputDisabled(true);
|
||||
}
|
||||
setOutput((prev) => [
|
||||
...prev,
|
||||
<div key={`cmd-${input}`} className="text-gray-400">
|
||||
<span className="text-orange-500">$</span> {input}
|
||||
</div>,
|
||||
...command.execute(commandArgs, { uptime: getUptime() }),
|
||||
]);
|
||||
} else {
|
||||
setOutput((prev) => [
|
||||
...prev,
|
||||
<div key={`cmd-${input}`} className="text-gray-400">
|
||||
<span className="text-orange-500">$</span> {input}
|
||||
</div>,
|
||||
...unknownCommand(normalizedInput)(),
|
||||
]);
|
||||
}
|
||||
|
||||
setHistory((prev) => [...prev, input]);
|
||||
setHistoryIndex(null);
|
||||
setInput("");
|
||||
setSuggestion(null);
|
||||
};
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
setInput(value);
|
||||
|
||||
const normalizedInput = value.toLowerCase();
|
||||
const parts = normalizedInput.split(" ");
|
||||
if (parts.length === 0) {
|
||||
setSuggestion(null);
|
||||
return;
|
||||
}
|
||||
const cmd = parts[0];
|
||||
const args = parts.slice(1);
|
||||
|
||||
// Handle project name suggestions
|
||||
if (cmd === "projects") {
|
||||
const projectPrefix = args.join(" ").toLowerCase().replace(/^"|"$/g, '');
|
||||
const matchedProject = projects.projects.find((project: Project) =>
|
||||
project.name.toLowerCase().startsWith(projectPrefix)
|
||||
);
|
||||
if (matchedProject) {
|
||||
setSuggestion(`projects "${matchedProject.name}"`);
|
||||
} else {
|
||||
setSuggestion(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle command suggestions
|
||||
const matchedCommand = commandsList.find((command) =>
|
||||
command.startsWith(normalizedInput)
|
||||
);
|
||||
setSuggestion(matchedCommand || null);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
if (history.length === 0) return;
|
||||
|
||||
const newIndex = historyIndex === null
|
||||
? history.length - 1
|
||||
: Math.max(0, historyIndex - 1);
|
||||
|
||||
setHistoryIndex(newIndex);
|
||||
setInput(history[newIndex]);
|
||||
} else if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
if (historyIndex === null) return;
|
||||
|
||||
const newIndex = historyIndex + 1;
|
||||
if (newIndex >= history.length) {
|
||||
setHistoryIndex(null);
|
||||
setInput("");
|
||||
} else {
|
||||
setHistoryIndex(newIndex);
|
||||
setInput(history[newIndex]);
|
||||
}
|
||||
} else if (e.key === "Tab" && suggestion) {
|
||||
e.preventDefault();
|
||||
setInput(suggestion);
|
||||
setSuggestion(null);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
terminalEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, [output]);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-black text-white font-mono w-full h-screen overflow-y-auto p-4"
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
>
|
||||
<div>
|
||||
{output.map((line, index) => (
|
||||
<div key={index} className="whitespace-pre-wrap">
|
||||
{line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<form
|
||||
onSubmit={handleInput}
|
||||
className="flex items-center mt-2 relative"
|
||||
>
|
||||
{!inputDisabled && (
|
||||
<span className="text-orange-500">$</span>
|
||||
)}
|
||||
{!inputDisabled && (
|
||||
<div className="relative flex-1 ml-2">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="bg-black text-white outline-none w-full"
|
||||
value={input}
|
||||
onChange={(e) => handleInputChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
disabled={inputDisabled}
|
||||
/>
|
||||
{suggestion && (
|
||||
<div
|
||||
className="absolute top-0 left-0 text-gray-500"
|
||||
style={{
|
||||
opacity: 0.5,
|
||||
pointerEvents: "none",
|
||||
paddingLeft: "2px",
|
||||
}}
|
||||
>
|
||||
{input}
|
||||
<span className="text-gray-400">
|
||||
{suggestion.slice(input.length)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div ref={terminalEndRef}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MOTD = () => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="overflow-x-auto whitespace-pre font-mono text-xs sm:text-sm md:text-base text-orange-500"
|
||||
>
|
||||
{` _____ _ _ _ `}
|
||||
{`\n | __ \\ (_|_) | | `}
|
||||
{`\n | |__) |___ _ __ _ __ _ _ ___ __| | _____ __`}
|
||||
{`\n | _ // _ \\| '_ \\| '_ \\| | |/ _ \\ / _\` |/ _ \\ \\ / /`}
|
||||
{`\n | | \\ \\ (_) | | | | | | | | | __/| (_| | __/\\ V / `}
|
||||
{`\n |_| \\_\\___/|_| |_|_| |_|_|_|\\___(_)__,_|\\___| \\_/ `}
|
||||
</div>
|
||||
<div className="text-orange-400 mt-4">
|
||||
<strong>Welcome to Ronnie's Project Terminal!</strong>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-700 my-4"/>
|
||||
<strong className="text-orange-500">🚀 ABOUT THIS TERMINAL</strong>
|
||||
<p className="text-gray-400">
|
||||
This is a showcase of my experimental projects hosted on *.ronniie.dev.
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
Each project is a unique experiment, from fun visualizations to useful tools.
|
||||
</p>
|
||||
<p className="text-gray-400">
|
||||
Feel free to explore and interact with them!
|
||||
</p>
|
||||
|
||||
<div className="border-t border-gray-700 my-4"/>
|
||||
<p className="text-orange-400">
|
||||
🌟 "Every project is an adventure in learning and creation." 🌟
|
||||
</p>
|
||||
|
||||
<div className="border-t border-gray-700 my-4"/>
|
||||
<div className="text-gray-400 mt-2">
|
||||
Type <span className="text-orange-500">'help'</span> to see available commands. Let's explore
|
||||
together!
|
||||
</div>
|
||||
<div className="text-gray-400 mt-2">
|
||||
Want to learn more about me? Visit <a href="https://ronniie.com" target="_blank" rel="noopener noreferrer" className="text-orange-400 hover:text-orange-300">ronniie.com</a> for my full portfolio and blog.
|
||||
</div>
|
||||
<div className="text-gray-400 mt-1">
|
||||
Available projects: {projects?.projects?.map(p => p.name).join(", ") || "None"}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Terminal;
|
39
src/data/projects.json
Normal file
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"projects": [
|
||||
{
|
||||
"name": "Is My Internet Working?",
|
||||
"description": "A simple tool to check if your internet connection is working. No fancy stuff, just a yes or no. Free, instant, and reliable internet connection checker.",
|
||||
"icon": "🌐",
|
||||
"url": "https://ismyinternetworking.ronniie.dev",
|
||||
"tags": ["Web", "Fun", "Interactive", "JavaScript"]
|
||||
},
|
||||
{
|
||||
"name": "What's My Vibe?",
|
||||
"description": "Check your current vibe with our fun and interactive vibe checker. Get instant vibe readings and discover your mood!",
|
||||
"icon": "😊",
|
||||
"url": "https://whatsmyvibe.ronniie.dev",
|
||||
"tags": ["Web", "Fun", "Interactive", "JavaScript"]
|
||||
},
|
||||
{
|
||||
"name": "HoldThisButton",
|
||||
"description": "Test your patience and endurance with our button-holding challenge. How long can you hold the button?",
|
||||
"icon": "🔘",
|
||||
"url": "https://holdthisbutton.ronniie.dev",
|
||||
"tags": ["Web", "Fun", "Interactive", "JavaScript"]
|
||||
},
|
||||
{
|
||||
"name": "TimelineCheck",
|
||||
"description": "Check if you're in the right timeline with our timeline checker. Get instant timeline readings and discover if you're in the correct reality!",
|
||||
"icon": "📰",
|
||||
"url": "https://timelinecheck.ronniie.dev",
|
||||
"tags": ["Web", "Fun", "Interactive", "JavaScript"]
|
||||
},
|
||||
{
|
||||
"name": "ZoneOut",
|
||||
"description": "A fast-paced square collection game where you select and clear bouncing squares. Choose your difficulty and game mode, then try to achieve the highest score! Game Modes: Levels (progress through increasingly challenging levels), Endless (play indefinitely with increasing difficulty). Difficulty Levels: Easy (infinite lives, slower squares, 20% red squares), Medium (5 lives, moderate speed, 30% red squares), Hard (3 lives, fast squares, 40% red squares). Features: Smooth selection, combo system, time bonuses, particle effects, modern UI, responsive design. Play at zoneout.ronniie.dev.",
|
||||
"icon": "🟧",
|
||||
"url": "https://zoneout.ronniie.dev",
|
||||
"tags": ["Web", "Game", "Fun", "Interactive", "JavaScript", "Canvas"]
|
||||
}
|
||||
]
|
||||
}
|
15
src/types/command.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export interface CommandMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
usage?: string;
|
||||
}
|
||||
|
||||
export interface CommandContext {
|
||||
uptime?: string;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
metadata: CommandMetadata;
|
||||
execute: (args?: string[], context?: CommandContext) => JSX.Element[];
|
||||
}
|
18
tailwind.config.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
27
tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|