Compare commits
35 Commits
4b3e89fa59
...
develop
Author | SHA1 | Date | |
---|---|---|---|
794c59e375 | |||
9b6d50fa42 | |||
f7211e3f87 | |||
8d84fc8274 | |||
94c2f336f6 | |||
accd08d568 | |||
521ba1902d | |||
36facdebe3 | |||
9439683b01 | |||
2ea1c345c7 | |||
2aebc4c54b | |||
81cf9c09f9 | |||
bdbaf799a8 | |||
e055e306df | |||
1c25802a2e | |||
20be9d170a | |||
120bd3ec50 | |||
8be3de8123 | |||
c0b348c5c6 | |||
b71b2092c3 | |||
0046dc1cb4 | |||
42d26503bc | |||
ec5c66a9c2 | |||
1217e86ac9 | |||
d07c1a4521 | |||
cc9c9ea1b8 | |||
f1d6c0a25c | |||
43f96b1ea9 | |||
a4d1f6b825 | |||
8038e79ad2 | |||
d7c257cb39 | |||
88f2a716b9 | |||
ae279b478b | |||
88761a6a66 | |||
6b28ed61bd |
53
.github/workflows/build.yaml
vendored
Normal file
53
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Build Website
|
||||
run-name: Build Website
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build NPM Project
|
||||
runs-on: ubuntu-latest
|
||||
container: node
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: ztimson/actions/clone@develop
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm i
|
||||
|
||||
- name: Build Project
|
||||
run: npm run build
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: website
|
||||
path: dist
|
||||
retention-days: 7
|
||||
|
||||
tag:
|
||||
name: Tag Version
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
container: node
|
||||
steps:
|
||||
- name: Clone Repository
|
||||
uses: ztimson/actions/clone@develop
|
||||
|
||||
- name: Get Version Number
|
||||
run: echo "VERSION=$(cat package.json | grep version | grep -Eo ':.+' | grep -Eo '[[:alnum:]\.\/\-]+')" >> $GITHUB_ENV
|
||||
|
||||
- name: Tag Version
|
||||
uses: ztimson/actions/tag@develop
|
||||
with:
|
||||
tag: ${{env.VERSION}}
|
||||
|
||||
publish:
|
||||
name: Build & Push Dockerfile
|
||||
needs: build
|
||||
uses: ztimson/actions/.github/workflows/docker.yaml@develop
|
||||
with:
|
||||
name: ztimson/zakscode
|
||||
repository: ${{github.server_url}}/${{github.repository}}.git
|
||||
pass: ${{secrets.DEPLOY_TOKEN}}
|
15
Dockerfile
15
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM node as build
|
||||
FROM node:20-alpine as build
|
||||
|
||||
# Variables
|
||||
ARG NODE_ENV=prod
|
||||
@@ -8,15 +8,14 @@ ENV NG_CLI_ANALYTICS=ci \
|
||||
NODE_OPTIONS=${NODE_OPTIONS}
|
||||
|
||||
# Setup
|
||||
RUN mkdir /app
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Install & build
|
||||
RUN if [ ! -d "dist" ]; then npm ci && npm run build; fi
|
||||
# Build
|
||||
RUN if [ ! -d "dist" ]; then npm install && npm run build; fi
|
||||
|
||||
# Use Nginx to serve
|
||||
FROM nginx:1.20-alpine
|
||||
COPY --from=build /app/dist/browser /usr/share/nginx/html
|
||||
COPY docker/robots.txt /usr/share/nginx/html/robots.txt
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
EXPOSE 80
|
||||
FROM git.zakscode.com/ztimson/momentum:latest
|
||||
RUN rm -rf /app/server/public/assets /app/server/public/index.html
|
||||
COPY --from=build /app/dist /app/server/public
|
||||
|
11
LICENSE
Normal file
11
LICENSE
Normal file
@@ -0,0 +1,11 @@
|
||||
Copyright (c) 2023 Zakary Timson
|
||||
|
||||
All Rights Reserved.
|
||||
|
||||
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.
|
103
README.md
103
README.md
@@ -1,18 +1,93 @@
|
||||
# ZaksCode
|
||||
Business website for ZaksCode. Provides some background information, contact inforation & a portfolio of projects.
|
||||
[View here](https://zakscode.com).
|
||||
<!-- Header -->
|
||||
<div id="top" align="center">
|
||||
<br />
|
||||
|
||||
Built using [Angular 13](https://angular.io).
|
||||
<!-- Logo -->
|
||||
<img src="./public/logo.png" alt="Logo" width="200" height="200">
|
||||
|
||||
<!-- Title -->
|
||||
### ZaksCode
|
||||
|
||||
<!-- Description -->
|
||||
Source Code for ZaksCode Website
|
||||
|
||||
<!-- Repo badges -->
|
||||
[](https://git.zakscode.com/ztimson/zakscode/tags)
|
||||
[](https://git.zakscode.com/ztimson/zakscode/pulls)
|
||||
[](https://git.zakscode.com/ztimson/zakscode/issues)
|
||||
|
||||
<!-- Links -->
|
||||
|
||||
---
|
||||
<div>
|
||||
<a href="https://git.zakscode.com/ztimson/zakscode/releases" target="_blank">Release Notes</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/zakscode/issues/new?template=.github%2fissue_template%2fbug.md" target="_blank">Report a Bug</a>
|
||||
• <a href="https://git.zakscode.com/ztimson/zakscode/issues/new?template=.github%2fissue_template%2fenhancement.md" target="_blank">Request a Feature</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
</div>
|
||||
|
||||
## Table of Contents
|
||||
[[_TOC_]]
|
||||
- [ZaksCode](#top)
|
||||
- [About](#about)
|
||||
- [Demo](#demo)
|
||||
- [Built With](#built-with)
|
||||
- [Setup](#setup)
|
||||
- [Production](#production)
|
||||
- [Development](#development)
|
||||
- [License](#license)
|
||||
|
||||
## Cheatsheet
|
||||
```bash
|
||||
# Start Angular development server
|
||||
npm run start
|
||||
# Build production site
|
||||
npm run build
|
||||
# Build docker image
|
||||
docker build -t gitlab.zakscode.com:5050/zakcode/zakscode:latest .
|
||||
```
|
||||
## About
|
||||
|
||||
This the source code for `zakscode.com`.
|
||||
|
||||
ZaksCode is the personal website of Zakary Timson & is used as his business page & portfolio.
|
||||
### Demo
|
||||
|
||||
Website: https://zakscode.com
|
||||
|
||||
### Built With
|
||||
[](https://vuejs.org/)
|
||||
[](https://getbootstrap.com)
|
||||
[](https://docker.com/)
|
||||
[](https://typescriptlang.org/)
|
||||
|
||||
## Setup
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h3 id="production" style="display: inline">
|
||||
Production
|
||||
</h3>
|
||||
</summary>
|
||||
|
||||
#### Prerequisites
|
||||
- [Docker](https://docs.docker.com/install/)
|
||||
|
||||
#### Instructions
|
||||
1. Run the docker image: `docker run -p 80:80 git.zakscode.com/ztimson/zakscode:latest`
|
||||
2. Open [http://localhost](http://localhost)
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h3 id="development" style="display: inline">
|
||||
Development
|
||||
</h3>
|
||||
</summary>
|
||||
|
||||
#### Prerequisites
|
||||
- [Node.js](https://nodejs.org/en/download)
|
||||
|
||||
#### Instructions
|
||||
1. Install the dependencies: `npm install`
|
||||
2. Start the dev server: `npm run start`
|
||||
3. Open [http://localhost:4200](http://localhost:5173)
|
||||
|
||||
</details>
|
||||
|
||||
## License
|
||||
Copyright © 2023 Zakary Timson | All Rights Reserved
|
||||
|
||||
See the [license](./LICENSE) for more information.
|
||||
|
@@ -25,7 +25,7 @@ http {
|
||||
autoindex off;
|
||||
|
||||
location / {
|
||||
try_files $uri$args $uri$args/ /index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
7
docker/setup-environment.sh
Normal file
7
docker/setup-environment.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
JSON_STRING='window.env = { \
|
||||
APP_POSTMAIL_ACCESS_TOKEN: "'"${APP_POSTMAIL_ACCESS_TOKEN}"'", \
|
||||
}'
|
||||
sed -i "s@<script id=\"environment\"></script>@<script>${JSON_STRING}</script>@" /usr/share/nginx/html/index.html
|
||||
exec "$@"
|
22
index.html
22
index.html
@@ -1,22 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ZaksCode</title>
|
||||
<link rel="icon" href="/logo.png">
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name=”robots” content=”index,nofollow” />
|
||||
|
||||
<meta property=”og:type” content=”website” />
|
||||
<meta property=”og:title” content=”Zakary Timson” />
|
||||
<meta property=”og:description” content=”Devops & Software Engineer” />
|
||||
<meta property=”og:image” content=”https://zakscode.com/cloud.gif” />
|
||||
<meta property=”og:url” content=”https://zakscode.com” />
|
||||
<meta property=”og:site_name” content=”ZaksCode” />
|
||||
<!-- <meta name=”twitter:title” content=”Zakary Timson” />-->
|
||||
<!-- <meta name=”twitter:description” content=”Devops & Software Engineer” />-->
|
||||
<!-- <meta name=”twitter:image” content=”https://zakscode.com/cloud.gif” />-->
|
||||
<meta property="og:title" content="Zakary Timson">
|
||||
<meta property="og:type" content="article">
|
||||
<meta property="og:url" content="https://zakscode.com">
|
||||
<meta property="og:image" content="https://zakscode.com/cloud.png">
|
||||
<meta property="og:description" content="DevOps & Software Engineer">
|
||||
<meta name="description" content="DevOps & Software Engineer">
|
||||
<meta name="twitter:card" content="DevOps & Software Engineer">
|
||||
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: blob: git.zakscode.com; style-src 'self' 'unsafe-inline'; object-src 'none';">-->
|
||||
|
||||
<script id="environment"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
1523
package-lock.json
generated
1523
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"name": "zakscode",
|
||||
"version": "2.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build --force"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ztimson/momentum": "^0.50.3",
|
||||
"vue": "^3.3.11",
|
||||
"vue-router": "^4.2.5"
|
||||
},
|
||||
@@ -23,7 +24,7 @@
|
||||
"npm-run-all2": "^6.1.1",
|
||||
"sass": "^1.69.7",
|
||||
"typescript": "~5.3.0",
|
||||
"vite": "^5.0.10",
|
||||
"vite": "5.4.6",
|
||||
"vue-tsc": "^1.8.25"
|
||||
}
|
||||
}
|
||||
|
BIN
public/logo.png
BIN
public/logo.png
Binary file not shown.
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 113 KiB |
@@ -10,7 +10,7 @@ import Profile from '@/components/profile.vue';
|
||||
<!-- Content -->
|
||||
<div class="cap-width mb-3 bg-white">
|
||||
<!-- Header -->
|
||||
<header class="p-3 mx-auto" style="background: #732222">
|
||||
<header class="px-4 d-flex justify-content-center justify-content-sm-start" style="background: #732222">
|
||||
<profile style="transform: translateY(-33%)" />
|
||||
</header>
|
||||
|
||||
@@ -24,5 +24,5 @@ import Profile from '@/components/profile.vue';
|
||||
</div>
|
||||
|
||||
<!-- Spacer -->
|
||||
<div class="d-none d-sm-block w-100" style="height: 40px"></div>
|
||||
<div class="d-none d-lg-block w-100" style="height: 40px"></div>
|
||||
</template>
|
||||
|
23
src/components/card.vue
Normal file
23
src/components/card.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import Icon from '@/components/icon.vue';
|
||||
|
||||
defineProps({
|
||||
color: {type: String, required: true},
|
||||
icon: {type: String, required: true},
|
||||
title: {type: String, required: true},
|
||||
text: {type: String, required: true},
|
||||
offset: {type: String, default: '0px'}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-column align-items-center border mb-3 p-3" style="width: 240px">
|
||||
<div class="m-3 p-3 rounded-circle text-white d-flex align-items-middle justify-content-center" :style="'height: 50px; width: 50px; background:' + color">
|
||||
<icon :name="icon" :style="'margin-top:' + offset"/>
|
||||
</div>
|
||||
<h4>{{title}}</h4>
|
||||
<p class="text-center">
|
||||
{{text}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Icon from '@/components/icon.vue';
|
||||
import {momentum} from '@/services/momentum.service';
|
||||
import {ref} from 'vue';
|
||||
|
||||
const disable = ref(false);
|
||||
@@ -10,7 +11,7 @@ const data = ref({
|
||||
subject: '',
|
||||
message: '',
|
||||
});
|
||||
const errors = ref({
|
||||
const errors = ref<any>({
|
||||
banner: false,
|
||||
name: false,
|
||||
email: false,
|
||||
@@ -29,6 +30,14 @@ function validateEmail(email: string) {
|
||||
return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(email);
|
||||
}
|
||||
|
||||
function send(name: string, email: string, subject: string, message: string) {
|
||||
return momentum.email.send({
|
||||
to: ['zaktimson@gmail.com', email],
|
||||
subject: `ZaksCode: ${subject}`,
|
||||
body: `From: ${name} <${email}><br><br>${message}`
|
||||
});
|
||||
}
|
||||
|
||||
function submit() {
|
||||
disable.value = true;
|
||||
const d = data.value;
|
||||
@@ -42,39 +51,51 @@ function submit() {
|
||||
};
|
||||
|
||||
if(errors.value.name || errors.value.email || errors.value.subject || errors.value.message) return disable.value = false;
|
||||
// Email.send(d.name, d.email, d.subject, d.message).then(() => {
|
||||
// done.value = true;
|
||||
// }).catch(err => {
|
||||
// console.error(err);
|
||||
// errors.value.banner = err?.message || err.toString();
|
||||
// disable.value = false;
|
||||
// });
|
||||
send(d.name, d.email, d.subject, d.message).then(() => {
|
||||
done.value = true;
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
errors.value.banner = err?.message || err.toString();
|
||||
disable.value = false;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Success Banner -->
|
||||
<div v-if="done" class="alert alert-success">
|
||||
<span>Success! We sent you a copy & we will be intouch shortly.</span>
|
||||
<span>Success! We will be intouch shortly.</span>
|
||||
</div>
|
||||
<!-- Error Banner -->
|
||||
<div v-if="errors?.banner" class="alert alert-danger">
|
||||
{{ errors.banner }}
|
||||
Error: {{ (errors as any).banner }}
|
||||
</div>
|
||||
<!-- Contact Form -->
|
||||
<form>
|
||||
<div class="d-flex flex-wrap justify-content-between mb-2">
|
||||
<div class="input-group mb-2 mb-sm-0" style="width: 49%; min-width: 250px">
|
||||
<div class="d-flex flex-column flex-md-row flex-wrap justify-content-between">
|
||||
<!-- Name -->
|
||||
<div class="mb-2 p-0 pe-md-1 p-md-0" style="flex: 1 0 0;">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><icon name="user"/></span>
|
||||
<input class="form-control" type="text" placeholder="Name" v-model="data.name" v-bind:class="{'is-invalid': errors?.name}" :disabled="!!user || disable" required>
|
||||
<input class="form-control" type="text" placeholder="Name" v-model="data.name" v-bind:class="{'is-invalid': errors?.name}" :disabled="disable" required>
|
||||
</div>
|
||||
<div class="input-group" style="width: 49%; min-width: 250px">
|
||||
</div>
|
||||
<!-- Email -->
|
||||
<div class="mb-2 p-0 ps-md-1" style="flex: 1 0 0;">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><icon name="envelope"/></span>
|
||||
<input class="form-control" type="email" placeholder="Email" v-model="data.email" v-bind:class="{'is-invalid': errors?.email}" :disabled="!!user || disable" required>
|
||||
<input class="form-control" type="email" placeholder="Email" v-model="data.email" v-bind:class="{'is-invalid': errors?.email}" :disabled="disable" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Subject -->
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text"><icon name="book"/></span>
|
||||
<input class="form-control" type="text" placeholder="Subject" v-model="data.subject" v-bind:class="{'is-invalid': errors?.subject}" :disabled="disable" required>
|
||||
</div>
|
||||
<!-- Message Body -->
|
||||
<textarea class="form-control" placeholder="Message" rows="5" v-model="data.message" v-bind:class="{'is-invalid': errors?.message}" :disabled="disable" required></textarea>
|
||||
<!-- Buttons -->
|
||||
<div class="text-end pt-3">
|
||||
<button type="button" class="btn rounded-pill ms-3" @click="reset()">Reset</button>
|
||||
<button type="button" class="btn btn-primary rounded-pill ms-3" @click="submit" :disabled="disable">Send Message</button>
|
||||
|
@@ -4,7 +4,11 @@
|
||||
<style scoped>
|
||||
footer {
|
||||
background: #343a40;
|
||||
color: darkgrey;
|
||||
color: #A3A3A3;
|
||||
|
||||
a {
|
||||
color: #75B8FF;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@@ -1,251 +1,33 @@
|
||||
<script setup>
|
||||
import {onMounted} from 'vue';
|
||||
<script setup lang="ts">
|
||||
import '../modules/konsole';
|
||||
import {onMounted, ref} from 'vue';
|
||||
|
||||
const hostname = 'virtual';
|
||||
let history = [];
|
||||
let historyIndex = 0;
|
||||
let prompt;
|
||||
let input;
|
||||
let output;
|
||||
const animate = ref(true);
|
||||
|
||||
function focus() {
|
||||
input.focus();
|
||||
function sleep(time: number) {
|
||||
return new Promise(res => setTimeout(res, time));
|
||||
}
|
||||
|
||||
function disable() {
|
||||
input.disabled = true;
|
||||
prompt.style.visibility = 'hidden';
|
||||
async function showerThought(s=10000) {
|
||||
if(!animate.value) return;
|
||||
await (<any>window).cli.type('shower-thought');
|
||||
if(!animate.value) return;
|
||||
await sleep(s);
|
||||
if(!animate.value) return;
|
||||
await(<any>window).cli.type('clear');
|
||||
}
|
||||
|
||||
function enable() {
|
||||
input.disabled = false;
|
||||
input.focus();
|
||||
prompt.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
function banner() {
|
||||
stdOut(`Konsole 0.2.0 LTS virtual tty1<br><br>${hostname} login: root<br>password:<br><br>`);
|
||||
}
|
||||
|
||||
function process(command) {
|
||||
(Array.isArray(command) ? command.join(' ') : command).split(';').filter(c => !!c).forEach(c => {
|
||||
const parts = c.split(' ').filter(c => !!c);
|
||||
if(window.cli[parts[0]] == undefined || window.cli[parts[0]].run == undefined) {
|
||||
stdErr(`${parts[0]}: command not found`);
|
||||
} else {
|
||||
try {
|
||||
const out = window.cli[parts[0]].run(parts.slice(1));
|
||||
if(!!out) stdOut(out);
|
||||
} catch(err) {
|
||||
console.error(err)
|
||||
stdErr(`${parts[0]}: exited with a non-zero status`);
|
||||
onMounted(async () => {
|
||||
(<any>window).cli.build('#konsole');
|
||||
while(animate) {
|
||||
if(!animate.value) break;
|
||||
await sleep(3000);
|
||||
if(!animate.value) break;
|
||||
await showerThought();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function stdErr(text) {
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('console-output-line');
|
||||
p.classList.add('console-output-error');
|
||||
p.innerText = text;
|
||||
output.appendChild(p);
|
||||
}
|
||||
|
||||
function stdOut(text, html=true) {
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('console-output-line');
|
||||
p[html ? 'innerHTML' : 'innerText'] = text;
|
||||
output.appendChild(p);
|
||||
}
|
||||
|
||||
function stdIn(event) {
|
||||
if(event.key == "Enter") {
|
||||
disable();
|
||||
let inputValue = input.value;
|
||||
input.value = '';
|
||||
stdOut(`root@localhost:~ # ${inputValue}`, false);
|
||||
if(!!inputValue) {
|
||||
history.push(inputValue);
|
||||
historyIndex = history.length;
|
||||
process(inputValue)
|
||||
}
|
||||
enable();
|
||||
} else if(event.key == 'Up' || event.key == 'ArrowUp') {
|
||||
if(historyIndex > 0) historyIndex--;
|
||||
input.value = historyIndex == history.length ? '' : history[historyIndex];
|
||||
setTimeout(() => {
|
||||
const end = input.value.length;
|
||||
input.setSelectionRange(end, end);
|
||||
input.focus();
|
||||
}, 1)
|
||||
} else if(event.key == 'Down' || event.key == 'ArrowDown') {
|
||||
if(historyIndex < history.length) historyIndex++;
|
||||
input.value = historyIndex == history.length ? '' : history[historyIndex];
|
||||
setTimeout(() => {
|
||||
const end = input.value.length;
|
||||
input.setSelectionRange(end, end);
|
||||
input.focus();
|
||||
}, 1)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
prompt = document.getElementsByClassName('console-input-prompt')[0];
|
||||
input = document.getElementsByClassName('console-input-field')[0];
|
||||
output = document.getElementsByClassName('console-output')[0];
|
||||
banner();
|
||||
});
|
||||
|
||||
window.cli = {};
|
||||
window.cli['clear'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Clear console output';
|
||||
},
|
||||
run: args => {
|
||||
output.innerHTML = '';
|
||||
}
|
||||
}
|
||||
window.cli['echo'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Output text to console';
|
||||
},
|
||||
run: args => {
|
||||
return args.join(' ');
|
||||
}
|
||||
}
|
||||
window.cli['exit'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'End session';
|
||||
},
|
||||
run: args => {
|
||||
process('clear');
|
||||
history = [];
|
||||
historyIndex = 0;
|
||||
banner();
|
||||
}
|
||||
}
|
||||
window.cli['help'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Display all commands';
|
||||
},
|
||||
run: args => {
|
||||
return Object.keys(window.cli).map(command => `${command} - ${window.cli[command].help()}`).join('<br>') + '<br><br>';
|
||||
}
|
||||
}
|
||||
window.cli['hostname'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Get computer hostname';
|
||||
},
|
||||
run: args => {
|
||||
return 'localhost'
|
||||
}
|
||||
}
|
||||
window.cli['man'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Command manual';
|
||||
},
|
||||
run: args => {
|
||||
return window.cli[args[0]].help();
|
||||
}
|
||||
}
|
||||
window.cli['whoami'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Get username';
|
||||
},
|
||||
run: args => {
|
||||
return 'root'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.console {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
background: #333;
|
||||
font-family: monospace !important;
|
||||
overflow-y: auto;
|
||||
|
||||
.console-output {
|
||||
flex-grow: 1;
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
.console-output-line {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 1.25rem;
|
||||
}
|
||||
|
||||
.console-input {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
|
||||
.console-input-prompt {
|
||||
padding-right: 0.55em;
|
||||
text-wrap: nowrap;
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
.console-input-field {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1rem;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
color: #0f0;
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
|
||||
animation: blink-empty 1s infinite linear;
|
||||
background-image: linear-gradient(#0f0, #0f0);
|
||||
background-position: 1px center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1px 1.1em;
|
||||
|
||||
&:focus {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink-empty {
|
||||
0% {background-size: 1px 1.1em;}
|
||||
50% {background-size: 1px 1.1em;}
|
||||
51% {background-size: 0 1.1em;}
|
||||
100% {background-size: 0 1.1em;}
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div class="console">
|
||||
<div class="console-output"></div>
|
||||
<div class="console-input" @click=" focus()">
|
||||
<div class="console-input-prompt">root@{{hostname}}:~ #</div>
|
||||
<input class="console-input-field" type="text" @keydown="stdIn($event)">
|
||||
</div>
|
||||
</div>
|
||||
<div id="konsole" @click="animate = false"></div>
|
||||
</template>
|
||||
|
@@ -21,7 +21,7 @@ import Icon from '@/components/icon.vue';
|
||||
</h2>
|
||||
<ul class="m-0 p-0 text-start" style="list-style: none">
|
||||
<li><icon name="map-marker-alt" class="me-1"/> Toronto Ontario, Canada</li>
|
||||
<li><icon name="envelope" class="me-1"/> <a href="mailto:zaktimson@gmail.com">zaktimson@gmail.com</a></li>
|
||||
<li><icon name="envelope" class="me-1"/> <a href="mailto:zaktimson@gmail.com" target="_blank">zaktimson@gmail.com</a></li>
|
||||
<li><icon name="github" class="fa-brands me-1"/> <a href="https://github.com/ztimson" target="_blank">github.com/ztimson</a></li>
|
||||
<li><icon name="git-alt" class="fa-brands me-1"/> <a href="https://git.zakscode.com" target="_blank">git.zakscode.com</a></li>
|
||||
</ul>
|
||||
|
@@ -1,24 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
export interface Project {
|
||||
icon?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
link: string;
|
||||
source?: string;
|
||||
}
|
||||
|
||||
defineProps({
|
||||
projects: {type: Array as () => Project[], required: true}
|
||||
projects: {type: Array as () => any[], required: true}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.invert {
|
||||
filter: invert(100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-for="(p, i) in projects">
|
||||
<hr v-if="i != 0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex align-items-center border p-2" :class="i == 0 ? '' : 'border-top-0'">
|
||||
<div class="me-2">
|
||||
<img :src="p.icon || '/git.png'" alt="Logo" style="height: 40px; width: 40px;">
|
||||
<img :src="p.icon || '/git.png'" alt="Logo" :class="{invert: p.invertIcon}" style="height: 40px; width: 40px;">
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<div>
|
||||
|
@@ -1,33 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.btn-outline-info:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
const resume = 'https://drive.google.com/file/d/1km4XqtXrYtoUnrDpEtDmAeKWh1qyp9nQ/view?usp=drive_link';
|
||||
|
||||
const refrences = [
|
||||
['Andre Mourinho', 'https://drive.google.com/file/d/1QSRzBg6xj1evwIhpAkrKZqPrbJOjwkYR/view?usp=drive_link'],
|
||||
['CD Projekt Red', 'https://drive.google.com/file/d/1QT02DBRFg4HcJXoxuH0WfiozjYHoJWrn/view?usp=drive_link'],
|
||||
['Chris Cartwright', 'https://drive.google.com/file/d/17JtSlVvMt_84XG0iQO8cHlDsi30oR34c/view?usp=drive_link'],
|
||||
['Garry Whyte', 'https://drive.google.com/file/d/18Mqv1vX2wmseQkqEPRIZiuSuGhDri-dB/view?usp=drive_link'],
|
||||
['Linda Nicodemo', 'https://drive.google.com/file/d/1c4rB1oSWW4OoaURHkSeCHpOEkdBdQ_fH/view?usp=drive_link'],
|
||||
['Ray Power', 'https://drive.google.com/file/d/1aP8Xpg3hlP0yQV8_sg9yeTgsrTRCcJZH/view?usp=drive_link'],
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-block d-md-none">
|
||||
<div class="mb-3">
|
||||
<a class="btn btn-outline-primary w-100">CSV / Resume</a>
|
||||
<a class="btn btn-outline-danger w-100" :href="resume" target="_blank">Resume</a>
|
||||
</div>
|
||||
<div class="btn-group-vertical w-100" role="group">
|
||||
<a class="btn btn-outline-info">CD Projekt Red</a>
|
||||
<a class="btn btn-outline-info">Chris Cartwright</a>
|
||||
<a class="btn btn-outline-info">Garry Whyte</a>
|
||||
<a class="btn btn-outline-info">Ray Power</a>
|
||||
<a class="btn btn-outline-info">Linda Nicodemo</a>
|
||||
<a v-for="ref in refrences" class="btn btn-outline-primary" :href="ref[1]" target="_blank">{{ref[0]}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-none d-md-block">
|
||||
<a class="btn btn-outline-primary me-3">CSV / Resume</a>
|
||||
<div class="d-none d-md-flex align-items-start">
|
||||
<a class="btn btn-outline-danger me-3" :href="resume" target="_blank">Resume</a>
|
||||
<div class="btn-group" role="group">
|
||||
<a class="btn btn-outline-info">CD Projekt Red</a>
|
||||
<a class="btn btn-outline-info">Chris Cartwright</a>
|
||||
<a class="btn btn-outline-info">Garry Whyte</a>
|
||||
<a class="btn btn-outline-info">Ray Power</a>
|
||||
<a class="btn btn-outline-info">Linda Nicodemo</a>
|
||||
<a v-for="ref in refrences" class="btn btn-outline-primary" :href="ref[1]" target="_blank">{{ref[0]}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
6
src/environments/environment.ts
Normal file
6
src/environments/environment.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const devMode = location?.port == '5173';
|
||||
|
||||
export const environment = {
|
||||
apiUrl: devMode ? 'http://localhost' : location.origin,
|
||||
devMode
|
||||
}
|
@@ -4,7 +4,7 @@
|
||||
|
||||
::-webkit-scrollbar { width: 10px; }
|
||||
::-webkit-scrollbar-track { background: #333; }
|
||||
::-webkit-scrollbar-thumb { background: #555; border-radius: 5px }
|
||||
::-webkit-scrollbar-thumb { background: #555; border-radius: 5px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: #aaa; }
|
||||
|
||||
html, body {
|
||||
@@ -14,11 +14,11 @@ html, body {
|
||||
padding: 0;
|
||||
|
||||
font-family: Roboto,sans-serif;
|
||||
background: #354B72 url(/cloud.gif) no-repeat fixed right 50% top -10vh;
|
||||
background: #354B72 url('/cloud.gif') no-repeat fixed right 50% top -10vh;
|
||||
}
|
||||
|
||||
a:not(.btn), a:not(.btn):visited {
|
||||
color: #007bff;
|
||||
color: #006FE6;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
|
11
src/modules/konsole/commands/banner.js
Normal file
11
src/modules/konsole/commands/banner.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.cli.exec['banner'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Display login banner';
|
||||
},
|
||||
run: args => {
|
||||
return `Konsole ${window.cli.version} LTS ${window.cli.hostname} tty1\n\n${window.cli.hostname} login: ${window.cli.user}\npassword:\n\n`;
|
||||
}
|
||||
}
|
14
src/modules/konsole/commands/cat.js
Normal file
14
src/modules/konsole/commands/cat.js
Normal file
@@ -0,0 +1,14 @@
|
||||
window.cli.exec['cat'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Display file contents';
|
||||
},
|
||||
run: args => {
|
||||
if(!args[0]) throw new Error('cat: missing operand');
|
||||
const file = window.cli.fs(args[0]);
|
||||
if(file == null) throw new Error('cat: File does not exist');
|
||||
return file;
|
||||
}
|
||||
}
|
16
src/modules/konsole/commands/cd.js
Normal file
16
src/modules/konsole/commands/cd.js
Normal file
@@ -0,0 +1,16 @@
|
||||
window.cli.exec['cd'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Change present working directory';
|
||||
},
|
||||
run: args => {
|
||||
const path = window.cli.fs(args[0]);
|
||||
if(!path) throw new Error(`cd: \'${args[0]}\': No such file or directory`);
|
||||
if(typeof path != 'object') throw new Error(`cd: \'${args[0]}\': Not a directory`);
|
||||
|
||||
window.cli.pwd = window.cli.path(args[0]);
|
||||
window.cli._prompt.innerText = window.cli._buildPrompt();
|
||||
}
|
||||
}
|
11
src/modules/konsole/commands/clear.js
Normal file
11
src/modules/konsole/commands/clear.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.cli.exec['clear'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Clear console output';
|
||||
},
|
||||
run: args => {
|
||||
setTimeout(() => window.cli._output.innerHTML = '', 1);
|
||||
}
|
||||
}
|
11
src/modules/konsole/commands/date.js
Normal file
11
src/modules/konsole/commands/date.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.cli.exec['date'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Get current date & time';
|
||||
},
|
||||
run: args => {
|
||||
return new Date().toLocaleString();
|
||||
}
|
||||
}
|
11
src/modules/konsole/commands/echo.js
Normal file
11
src/modules/konsole/commands/echo.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.cli.exec['echo'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Output text to console';
|
||||
},
|
||||
run: args => {
|
||||
return args.join(' ');
|
||||
}
|
||||
}
|
16
src/modules/konsole/commands/exit.js
Normal file
16
src/modules/konsole/commands/exit.js
Normal file
@@ -0,0 +1,16 @@
|
||||
window.cli.exec['exit'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'End session';
|
||||
},
|
||||
run: args => {
|
||||
setTimeout(() => {
|
||||
window.cli._history = [];
|
||||
window.cli._index = 0;
|
||||
window.cli._output.innerHTML = ''
|
||||
window.cli.stdOut(window.cli.exec['banner'].run());
|
||||
}, 1);
|
||||
}
|
||||
}
|
12
src/modules/konsole/commands/help.js
Normal file
12
src/modules/konsole/commands/help.js
Normal file
@@ -0,0 +1,12 @@
|
||||
window.cli.exec['help'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Display all commands';
|
||||
},
|
||||
run: args => {
|
||||
return `Konsole v${window.cli.version} - an experimental bash emulator written in JavaScript\nCreated By: Zakary Timson\n\n` +
|
||||
Object.keys(window.cli.exec).map(command => `${command} - ${window.cli.exec[command].help()}`).join('\n');
|
||||
}
|
||||
}
|
11
src/modules/konsole/commands/hostname.js
Normal file
11
src/modules/konsole/commands/hostname.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.cli.exec['hostname'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Get computer hostname';
|
||||
},
|
||||
run: args => {
|
||||
return window.cli.hostname;
|
||||
}
|
||||
}
|
20
src/modules/konsole/commands/ls.js
Normal file
20
src/modules/konsole/commands/ls.js
Normal file
@@ -0,0 +1,20 @@
|
||||
window.cli.exec['ls'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Display directory contents';
|
||||
},
|
||||
run: args => {
|
||||
const target = window.cli.fs(args[0]);
|
||||
if(!target || typeof target != 'object') throw new Error(`ls: cannot access \'${args[0]}\': No such file or directory`)
|
||||
return Object.keys(target)
|
||||
.sort((a, b) => {
|
||||
if(a > b) return 1;
|
||||
if(b > a) return -1;
|
||||
return 0;
|
||||
})
|
||||
.map(p => `-rwxrw---- 1 root root ${typeof target[p] =='object' ? '-' : target[p].length} ${p}`)
|
||||
.join('\n');
|
||||
}
|
||||
}
|
11
src/modules/konsole/commands/man.js
Normal file
11
src/modules/konsole/commands/man.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.cli.exec['man'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'View command\'s manual';
|
||||
},
|
||||
run: args => {
|
||||
return window.cli.exec[args[0]].help();
|
||||
}
|
||||
}
|
13
src/modules/konsole/commands/mkdir.js
Normal file
13
src/modules/konsole/commands/mkdir.js
Normal file
@@ -0,0 +1,13 @@
|
||||
window.cli.exec['mkdir'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Create new directory';
|
||||
},
|
||||
run: args => {
|
||||
if(!args[0]) throw new Error('mkdir: missing operand');
|
||||
if(window.cli.fs(args[0]) != null) throw new Error('mkdir: File or directory already exists');
|
||||
window.cli.fs(args[0], {});
|
||||
}
|
||||
}
|
11
src/modules/konsole/commands/pwd.js
Normal file
11
src/modules/konsole/commands/pwd.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.cli.exec['pwd'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Get present working directory';
|
||||
},
|
||||
run: args => {
|
||||
return window.cli.pwd;
|
||||
}
|
||||
}
|
12
src/modules/konsole/commands/rm.js
Normal file
12
src/modules/konsole/commands/rm.js
Normal file
@@ -0,0 +1,12 @@
|
||||
window.cli.exec['rm'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Delete file or directory';
|
||||
},
|
||||
run: args => {
|
||||
if(!args.length) throw new Error('rm: missing operand');
|
||||
args.forEach(a => window.cli.fs(a, null));
|
||||
}
|
||||
}
|
23
src/modules/konsole/commands/shower-thought.js
Normal file
23
src/modules/konsole/commands/shower-thought.js
Normal file
@@ -0,0 +1,23 @@
|
||||
window.cli.exec['shower-thought'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Random shower thought';
|
||||
},
|
||||
run: args => {
|
||||
const motd = [
|
||||
'Why do kamikaze pilots wear helmets?',
|
||||
'How are giraffes real, but unicorns made up',
|
||||
'When you are a kid you don\'t realize you are also watching your parents grow up',
|
||||
'Some one at Google was like "Yea, just have someone drive down every road on earth!"',
|
||||
'The number of people older than you never goes up',
|
||||
'When you brush your teeth you are cleaning your skeleton',
|
||||
'Pregnancy is like a group project where one person get\'s stuck with all the work',
|
||||
'If the universe wasn\'t infinite it would be even scarier',
|
||||
'Either we are alone in the universe or we are not. both are terrifying',
|
||||
'The object of golf is to play the least amount of golf.'
|
||||
];
|
||||
return motd[~~(Math.random() * motd.length)];
|
||||
}
|
||||
}
|
12
src/modules/konsole/commands/touch.js
Normal file
12
src/modules/konsole/commands/touch.js
Normal file
@@ -0,0 +1,12 @@
|
||||
window.cli.exec['touch'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Change file timestamps & create file if missing';
|
||||
},
|
||||
run: args => {
|
||||
if(!args[0]) throw new Error('touch: missing operand');
|
||||
if(!window.cli.fs(args[0])) window.cli.fs(args[0], '');
|
||||
}
|
||||
}
|
11
src/modules/konsole/commands/whoami.js
Normal file
11
src/modules/konsole/commands/whoami.js
Normal file
@@ -0,0 +1,11 @@
|
||||
window.cli.exec['whoami'] = {
|
||||
autocomplete: () => {
|
||||
return [];
|
||||
},
|
||||
help: () => {
|
||||
return 'Get current user account';
|
||||
},
|
||||
run: args => {
|
||||
return window.cli.user;
|
||||
}
|
||||
}
|
21
src/modules/konsole/index.js
Normal file
21
src/modules/konsole/index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import './konsole.js';
|
||||
import './konsole.css';
|
||||
|
||||
// CLI Commands
|
||||
import './commands/banner.js';
|
||||
import './commands/cat.js';
|
||||
import './commands/cd.js'
|
||||
import './commands/clear.js';
|
||||
import './commands/date.js';
|
||||
import './commands/echo.js';
|
||||
import './commands/exit.js';
|
||||
import './commands/help.js';
|
||||
import './commands/hostname.js';
|
||||
import './commands/ls.js';
|
||||
import './commands/man.js';
|
||||
import './commands/mkdir.js';
|
||||
import './commands/pwd.js';
|
||||
import './commands/rm.js';
|
||||
import './commands/shower-thought.js';
|
||||
import './commands/touch.js';
|
||||
import './commands/whoami.js';
|
40
src/modules/konsole/konsole.css
Normal file
40
src/modules/konsole/konsole.css
Normal file
@@ -0,0 +1,40 @@
|
||||
.cli-container {
|
||||
padding: 1em;
|
||||
background: #333;
|
||||
font-family: monospace !important;
|
||||
overflow-y: auto;
|
||||
min-height: 150px;
|
||||
max-height: 300px;
|
||||
tab-size: 4;
|
||||
}
|
||||
|
||||
.cli-stdout {
|
||||
flex-grow: 1;
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
.cli-stdout-line {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
min-height: 1em;
|
||||
}
|
||||
|
||||
.cli-stdin {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.cli-stdin-prompt {
|
||||
padding-right: 0.5em;
|
||||
text-wrap: nowrap;
|
||||
color: #0f0;
|
||||
}
|
||||
|
||||
.cli-stdin-input {
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 1em;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
color: #0f0;
|
||||
flex-grow: 1;
|
||||
padding: 0;
|
||||
}
|
156
src/modules/konsole/konsole.js
Normal file
156
src/modules/konsole/konsole.js
Normal file
@@ -0,0 +1,156 @@
|
||||
window.cli = {
|
||||
// Element references
|
||||
_input: null,
|
||||
_parent: null,
|
||||
_prompt: null,
|
||||
_output: null,
|
||||
|
||||
// CLI State
|
||||
_history: [],
|
||||
_index: 0,
|
||||
pwd: '/',
|
||||
hostname: 'virtual',
|
||||
env: {},
|
||||
exec: {},
|
||||
filesystem: {},
|
||||
user: 'root',
|
||||
version: '0.3.0',
|
||||
|
||||
_buildPrompt: () => `${window.cli.user}@${window.cli.hostname}:${window.cli.pwd}${window.cli.user == 'root' ? '#' : '$'}`,
|
||||
|
||||
build: (elementId) => {
|
||||
window.cli._parent = document.querySelector(elementId);
|
||||
if(!window.cli._parent)
|
||||
throw new Error(`Could not create konsole, element "${elementId}" does not exist`);
|
||||
|
||||
window.cli._parent.innerHTML = `
|
||||
<div class="cli-container">
|
||||
<div class="cli-stdout"></div>
|
||||
<div class="cli-stdin" onclick="window.cli._input.focus()">
|
||||
<label for="${elementId}-cli-stdin-input" class="cli-stdin-prompt">${window.cli._buildPrompt()}</label>
|
||||
<input id="${elementId}-cli-stdin-input" class="cli-stdin-input" type="text" autocomplete="off"/>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
window.cli._input = document.querySelector(elementId + ' .cli-stdin-input');
|
||||
window.cli._prompt = document.querySelector(elementId + ' .cli-stdin-prompt');
|
||||
window.cli._output = document.querySelector(elementId + ' .cli-stdout');
|
||||
window.cli._input.addEventListener('keyup', (e) => {
|
||||
if(e.key == "Enter") {
|
||||
window.cli.disable();
|
||||
if(!!window.cli._input.value) {
|
||||
window.cli._history.push(window.cli._input.value);
|
||||
window.cli._index = window.cli._history.length;
|
||||
window.cli.stdIn(window.cli._input.value)
|
||||
}
|
||||
window.cli._input.value = '';
|
||||
window.cli.enable();
|
||||
} else if(e.key == 'Up' || e.key == 'ArrowUp') {
|
||||
if(window.cli._index > 0) window.cli._index--;
|
||||
window.cli._input.value = window.cli._index == window.cli._history.length ? '' : window.cli._history[window.cli._index];
|
||||
setTimeout(() => {
|
||||
const end = window.cli._input.value.length;
|
||||
window.cli._input.setSelectionRange(end, end);
|
||||
window.cli._input.focus();
|
||||
}, 1)
|
||||
} else if(e.key == 'Down' || e.key == 'ArrowDown') {
|
||||
if(window.cli._index < window.cli._history.length) window.cli._index++;
|
||||
window.cli._input.value = window.cli._index == window.cli._history.length ? '' : window.cli._history[window.cli._index];
|
||||
setTimeout(() => {
|
||||
const end = window.cli._input.value.length;
|
||||
window.cli._input.setSelectionRange(end, end);
|
||||
window.cli._input.focus();
|
||||
}, 1)
|
||||
}
|
||||
});
|
||||
setTimeout(() => window.cli.stdOut(window.cli.exec['banner'].run()), 1);
|
||||
},
|
||||
disable: () => {
|
||||
window.cli._input.disabled = true;
|
||||
window.cli._prompt.style.visibility = 'hidden';
|
||||
},
|
||||
enable: () => {
|
||||
window.cli._input.disabled = false;
|
||||
window.cli._input.focus();
|
||||
window.cli._prompt.style.visibility = 'visible';
|
||||
},
|
||||
path: (path=window.cli.pwd) => {
|
||||
let p = path[0] == '/'? path : (window.cli.pwd + (window.cli.pwd.endsWith('/') ? '' : '/') + path.replace('./', ''))
|
||||
.replaceAll('//', '/');
|
||||
const parts = p.split('/').filter(p => !!p);
|
||||
for(let i = 0; i < parts.length; i++) {
|
||||
if(parts[i] == '..') {
|
||||
i--;
|
||||
parts.splice(i, 2);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
return '/' + (parts.length ? parts.join('/') : '');
|
||||
},
|
||||
fs: (path, set) => {
|
||||
return window.cli.path(path).split('/').filter(p => !!p).reduce((t, p, i, arr) => {
|
||||
if(!t?.hasOwnProperty(p)) {
|
||||
if(set == undefined) return undefined;
|
||||
t[p] = {};
|
||||
}
|
||||
if(set !== undefined && i == arr.length - 1) {
|
||||
if(set == null) delete t[p];
|
||||
else t[p] = set;
|
||||
}
|
||||
return t[p];
|
||||
}, window.cli.filesystem);
|
||||
},
|
||||
stdErr: (text) => {
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('cli-stdout-line');
|
||||
p.classList.add('cli-stdout-error');
|
||||
p.innerText = text;
|
||||
window.cli._output.appendChild(p);
|
||||
},
|
||||
stdIn:(command, suppress=false) => {
|
||||
(Array.isArray(command) ? command.join(' ') : command).split(';').filter(c => !!c).forEach(c => {
|
||||
const parts = c.match(/(?:[^\s"]+|"[^"]*")+/g);
|
||||
if(!parts) return;
|
||||
|
||||
const exec = window.cli.exec[parts[0]];
|
||||
if(!exec?.run) {
|
||||
if(!suppress) window.cli.stdErr(`${window.cli._buildPrompt()} ${command}\n${parts[0]}: command not found`);
|
||||
} else {
|
||||
try {
|
||||
const args = parts.slice(1).map(a => (a[0] == '"' || a[0] == "'") ? a.slice(1, -1) : a);
|
||||
const out = exec.run(args);
|
||||
if(!suppress) window.cli.stdOut(`${window.cli._buildPrompt()} ${command}${out != null ? '\n' + (out || '\n') : ''}`);
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
if(!suppress) {
|
||||
window.cli.stdErr(`${window.cli._buildPrompt()} ${command}\n${err.message || `${parts[0]}: exited with a non-zero status`}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
stdOut: (text='', html=false) => {
|
||||
const p = document.createElement('p');
|
||||
p.classList.add('cli-stdout-line');
|
||||
p[html ? 'innerHTML' : 'innerText'] = text;
|
||||
window.cli._output.appendChild(p);
|
||||
},
|
||||
type: (text, speed=150) => {
|
||||
let counter = 0;
|
||||
return new Promise(res => {
|
||||
let typing = setInterval(() => {
|
||||
if(counter < text.length) {
|
||||
window.cli._input.value += text[counter];
|
||||
} else {
|
||||
clearInterval(typing);
|
||||
setTimeout(() => {
|
||||
window.cli.stdIn(text);
|
||||
window.cli._input.value = '';
|
||||
res();
|
||||
}, 750);
|
||||
}
|
||||
counter++;
|
||||
}, speed);
|
||||
});
|
||||
}
|
||||
};
|
45
src/modules/konsole/models/file.js
Normal file
45
src/modules/konsole/models/file.js
Normal file
@@ -0,0 +1,45 @@
|
||||
export class CliFile {
|
||||
owner = window.cli.user;
|
||||
group = window.cli.user;
|
||||
permissions = 750;
|
||||
created = new Date();
|
||||
modified = new Date();
|
||||
|
||||
#data;
|
||||
get date() { return this.#data; }
|
||||
set date(d) {
|
||||
this.modified = new Date();
|
||||
this.#data = d;
|
||||
}
|
||||
|
||||
static instanceOf(f) {
|
||||
return f.hasOwnProperty('data');
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.#data.toString().length;
|
||||
}
|
||||
|
||||
permsString() {
|
||||
return '-' + this.permissions.toString().split('').map(p => {
|
||||
switch (p) {
|
||||
case 0:
|
||||
return '---';
|
||||
case 1:
|
||||
return '--x';
|
||||
case 2:
|
||||
return '-w-';
|
||||
case 3:
|
||||
return '-wx';
|
||||
case 4:
|
||||
return 'r--';
|
||||
case 5:
|
||||
return 'r-x';
|
||||
case 6:
|
||||
return 'rw-';
|
||||
case 7:
|
||||
return 'rwx';
|
||||
}
|
||||
}).join('');
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Home from '@/views/Home.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
history: createWebHistory((<any>import.meta).env.BASE_URL),
|
||||
routes: [
|
||||
{path: '/', name: 'home', component: Home}
|
||||
]
|
||||
|
36
src/services/momentum.service.ts
Normal file
36
src/services/momentum.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import {environment} from '@/environments/environment';
|
||||
import {PathEvent} from '@ztimson/utils';
|
||||
import {ref, onUnmounted} from 'vue';
|
||||
import {Momentum} from '@ztimson/momentum';
|
||||
|
||||
export function mRef<T>(event: string, get: (event?: PathEvent, ...args: any[]) => T | Promise<T>): any;
|
||||
export function mRef<T>(event: string, init: () => T | Promise<T>, get: (event: PathEvent, ...args: any[]) => T | Promise<T>): any;
|
||||
export function mRef<T>(event: string, fn1: (...args: any[]) => T | Promise<T>, fn2?: (event: PathEvent, ...args: any[]) => T | Promise<T>): any {
|
||||
// Init
|
||||
const resp = fn1(), promise = resp instanceof Promise;
|
||||
const r = ref<T | null>(promise ? null : resp);
|
||||
if(promise) resp.then(v => r.value = v);
|
||||
|
||||
// Get
|
||||
const get = fn2 || fn1;
|
||||
mWatch(event, async (event, ...args) => r.value = await get(<any>event, ...args));
|
||||
return r;
|
||||
}
|
||||
|
||||
export function mWatch(event: string, get: (event?: PathEvent, ...args: any[]) => any) {
|
||||
const unsubscribe = momentum.on(event, get);
|
||||
onUnmounted(() => unsubscribe());
|
||||
}
|
||||
|
||||
export const momentum = new Momentum(environment.apiUrl, {
|
||||
app: 'ZaksCode',
|
||||
logLevel: 'ERROR',
|
||||
persist: true,
|
||||
socket: true
|
||||
});
|
||||
momentum.client.inject(true);
|
||||
|
||||
momentum.on('auth/session-expired', () => {
|
||||
momentum.auth.logout();
|
||||
location.reload();
|
||||
});
|
@@ -1,96 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import Card from '@/components/card.vue';
|
||||
import Contact from '@/components/contact.vue';
|
||||
import Icon from '@/components/icon.vue';
|
||||
import Konsole from '@/components/konsole.vue';
|
||||
import Projects from '@/components/projects.vue';
|
||||
import Refrences from '@/components/refrences.vue';
|
||||
import {momentum} from '@/services/momentum.service';
|
||||
import {ref} from 'vue';
|
||||
|
||||
const services: Projects[] = [
|
||||
{name: 'Formula Manager', icon: 'https://git.zakscode.com/avatars/7ec6bfd66b2bf9bad5c43c75a33f9cb3f6609b05c33a31f5d1e524a567cd09c1?size=280', link: 'https://screenprintingsuppliescanada.com/formulation-manager', description: 'Screen Printing Supplies'},
|
||||
{name: 'Map Alliance', icon: 'https://maps.zakscode.com/assets/images/logo.png', link: 'https://maps.zakscode.com', description: 'Online GIS & map editing solution'},
|
||||
{name: 'Phone Reminders', icon: 'https://phone-reminders.com/phone-reminders.png', link: 'https://phone-reminders.com', description: 'Send SMS & Voice call reminders from Google Calendar'},
|
||||
];
|
||||
// Get favorites
|
||||
const products = ref<any[]>([]);
|
||||
const openSource = ref<any[]>([]);
|
||||
momentum.data.read('Repos').then(resp =>
|
||||
resp.forEach((r: any) => (r.product ? products : openSource).value.push(r)));
|
||||
|
||||
const openSource: Projects[] = [
|
||||
{name: 'Formula Manager', icon: 'https://git.zakscode.com/avatars/7ec6bfd66b2bf9bad5c43c75a33f9cb3f6609b05c33a31f5d1e524a567cd09c1?size=280', link: 'https://screenprintingsuppliescanada.com/formulation-manager', description: 'Screen Printing Supplies'},
|
||||
{name: 'Map Alliance', icon: 'https://maps.zakscode.com/assets/images/logo.png', link: 'https://maps.zakscode.com', description: 'Online GIS & map editing solution', source: 'https://google.com'},
|
||||
{name: 'Phone Reminders', icon: 'https://phone-reminders.com/phone-reminders.png', link: 'https://phone-reminders.com', description: 'Send SMS & Voice call reminders from Google Calendar'},
|
||||
];
|
||||
// Get repository count
|
||||
let remainder = ref(0);
|
||||
fetch('https://git.zakscode.com/api/v1/repos/search?limit=1000', {
|
||||
method: 'get',
|
||||
headers: {"Content-Type": "application/json"}
|
||||
}).then(async (resp: any) => {
|
||||
const data = (await resp.json())?.data;
|
||||
remainder.value = data.length - openSource.value.length;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-3">
|
||||
<konsole class="mb-5" style="max-height: 300px" />
|
||||
<!-- Terminal -->
|
||||
<konsole class="mb-5" />
|
||||
|
||||
<!-- Steps -->
|
||||
<div class="mb-5 pt-5">
|
||||
<h4 class="mb-0 text-center">Plan for Success</h4>
|
||||
<h3 class="mb-0 text-center">Plan for Success</h3>
|
||||
<hr class="mb-4">
|
||||
<div class="text-center my-5">
|
||||
<img src="/cycle.svg" alt="Development Cycle" style="width: 100%; max-width: 600px; height: auto;">
|
||||
</div>
|
||||
<div class="d-flex">
|
||||
<div class="d-flex flex-column align-items-center border m-3 p-3 text-center" style="flex: 1 0 0;">
|
||||
<div class="m-3 p-3 rounded-circle text-white d-flex align-items-middle justify-content-center" style="background: #6aa84f; height: 50px; width: 50px">
|
||||
<icon name="clipboard"/>
|
||||
</div>
|
||||
<h5>Plan</h5>
|
||||
<p class="text-center">
|
||||
Working with the client we will identify the goals of the project. This includes things like the target audience, use case, features, style, and deployment strategy.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center border m-3 p-3" style="flex: 1 0 0;">
|
||||
<div class="m-3 p-3 rounded-circle text-white d-flex align-items-middle justify-content-center" style="background: #6d9eeb; height: 50px; width: 50px">
|
||||
<icon name="code"/>
|
||||
</div>
|
||||
<h5>Develop</h5>
|
||||
<p class="text-center">
|
||||
Goals are broken down into tasks and are prioritized. Using CI/CD, tasks are automatically deployed for testing as they are completed.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center border m-3 p-3" style="flex: 1 0 0;">
|
||||
<div class="m-3 p-3 rounded-circle text-white d-flex align-items-middle justify-content-center" style="background: #e69138; height: 50px; width: 50px">
|
||||
<icon name="message"/>
|
||||
</div>
|
||||
<h5>Feedback</h5>
|
||||
<p class="text-center">
|
||||
Clients are notified with the release notes and can test at their convince. Any critiques can be communicated directly to us or through our ticketing system.
|
||||
</p>
|
||||
</div>
|
||||
<div class="d-flex flex-column align-items-center border m-3 p-3" style="flex: 1 0 0;">
|
||||
<div class="m-3 p-3 rounded-circle text-white" style="background: #674ea7; height: 50px; width: 50px">
|
||||
<h5 class="m-0">4</h5>
|
||||
</div>
|
||||
<h5>Release</h5>
|
||||
<p class="text-center">
|
||||
Once all goals are complete we will work with you to deploy the product to any location. Once setup, future updates are automatically rolled out.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
|
||||
</div>
|
||||
<div class="d-flex flex-wrap justify-content-around">
|
||||
<card color="#6aa84f" icon="clipboard" offset="1px" title="Plan" text="Working with the client we will identify the goals of the project. This includes things like the target audience, use case, features, style, and delivery."/>
|
||||
<card color="#6d9eeb" icon="code" offset="2px" title="Code" text="Goals are broken down into tasks and prioritized in our ticketing system. Using CI/CD, tasks are automatically deployed for testing as they are completed."/>
|
||||
<card color="#e69138" icon="message" offset="3px" title="Feedback" text="Clients are notified with the release notes and can test at their convenience. Any critiques can be communicated directly to us or through our ticketing system."/>
|
||||
<card color="#674ea7" icon="play" offset="2px" title="Release" text="Once all goals are complete we will work with you to deploy the product to any location. Once setup, future updates are automatically deployed to our clients."/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- About Section -->
|
||||
<div class="mb-5">
|
||||
<h3 class="mb-0">About</h3>
|
||||
<hr class="mb-4">
|
||||
<img alt="Childhood" src="/childhood.jpg" height="150px" width="auto" class="float-end m-3 m-md-0 ml-md-3" style="border-radius: 50%;">
|
||||
<p>
|
||||
Zak Timson is a software engineer with over 10 years of professional experience. Zak has had a love for
|
||||
computers since he was born & taught him self to code at the age of 13. Since then, he has gone to school
|
||||
for computer science & has worked for both small businesses and large corporations as a developer and team lead.
|
||||
</p>
|
||||
<p>
|
||||
Zak specializes in full-stack web development & server infrastructure, and primarily works on large enterprise
|
||||
grade "Software as a service" (SaaS) products. As a software architect & team lead he is able to work with
|
||||
business's to create a road map of their needs, build enterprise grade software solutions that meet those
|
||||
needs & work with clients to host & deliver automatic updates at scale using continuous integration.
|
||||
</p>
|
||||
<p>Zak Timson is a software engineer with over 10 years of professional experience. Zak has had a love for computers since he was born & taught himself to code at the age of 13. Since then, he has gone to school for computer science & has worked for both small businesses and large corporations as a developer and team lead.</p>
|
||||
<p>Zak specializes in full-stack web development, DevOps, cloud/server infrastructure, and primarily works on large enterprise grade "Software as a service" (SaaS) products. As a software architect & team lead he is able to work with businesses to create a road map of their needs, build enterprise grade software solutions that meet those needs & work with clients to host & deliver automatic updates at scale using continuous integration.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="mb-3 text-muted">CSV & References</h4>
|
||||
@@ -98,19 +58,22 @@ const openSource: Projects[] = [
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Projects List -->
|
||||
<div class="mb-5">
|
||||
<h3 class="m-0">Projects</h3>
|
||||
<hr class="mb-4">
|
||||
<div class="mb-4">
|
||||
<h4 class="mb-3 text-muted">Products & Services</h4>
|
||||
<projects :projects="services"/>
|
||||
<projects :projects="products"/>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-3 text-muted">Open Source</h4>
|
||||
<projects :projects="openSource"/>
|
||||
</div>
|
||||
<a v-if="remainder" class="float-end m-2" href="https://git.zakscode.com/explore" target="_blank">See {{remainder}} More...</a>
|
||||
</div>
|
||||
|
||||
<!-- Contact Form -->
|
||||
<div>
|
||||
<h3 class="m-0">Contact</h3>
|
||||
<hr class="mb-4">
|
||||
|
@@ -9,9 +9,11 @@
|
||||
"src/**/__tests__/*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"baseUrl": ".",
|
||||
"composite": true,
|
||||
"lib": ["DOM", "ESNext"],
|
||||
"moduleResolution": "Node",
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import {fileURLToPath, URL} from 'node:url';
|
||||
|
||||
import {defineConfig} from 'vite';
|
||||
import {fileURLToPath, URL} from 'node:url';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -12,5 +11,6 @@ export default defineConfig({
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
},
|
||||
envPrefix: 'APP',
|
||||
});
|
||||
|
Reference in New Issue
Block a user