Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4314c846dc | ||
|
|
3fabe8ad2e | ||
|
|
5ed028d00c | ||
|
|
5a5718abe4 | ||
|
|
7b18ef039a | ||
|
|
c672dba977 | ||
|
|
6575ef74e9 | ||
|
|
ca4d50e3f8 | ||
|
|
2acaa304f7 | ||
|
|
c0af8fe953 | ||
|
|
95f5cce8ab | ||
|
|
54af98ed3d | ||
|
|
b7895487e2 | ||
|
|
4ec726d4e7 | ||
|
|
20350d4b03 | ||
|
|
13f4ad3b94 | ||
|
|
e1f41778f3 | ||
|
|
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 |
@@ -1,36 +1,92 @@
|
|||||||
name: Build Website
|
name: Build Website
|
||||||
run-name: Build Website
|
run-name: Build Website
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build NPM Project
|
name: Build NPM Project
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: node
|
container: node
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository
|
- name: Clone Repository
|
||||||
uses: ztimson/actions/clone@develop
|
uses: ztimson/actions/clone@develop
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm i
|
run: npm i
|
||||||
|
|
||||||
- name: Build Project
|
- name: Build Project
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: website
|
name: website
|
||||||
path: dist
|
path: dist
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
|
|
||||||
publish:
|
tag:
|
||||||
name: Build & Push Dockerfile
|
name: Tag Version
|
||||||
needs: build
|
needs: build
|
||||||
uses: ztimson/actions/.github/workflows/docker.yaml@develop
|
runs-on: ubuntu-latest
|
||||||
with:
|
container: node
|
||||||
name: ztimson/zakscode
|
steps:
|
||||||
repository: ${{github.server_url}}/${{github.repository}}.git
|
- name: Clone Repository
|
||||||
pass: ${{secrets.DEPLOY_TOKEN}}
|
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}}
|
||||||
|
|
||||||
|
container:
|
||||||
|
name: Build Container
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: docker
|
||||||
|
steps:
|
||||||
|
- name: Build Container
|
||||||
|
run: |
|
||||||
|
git clone -b "${{github.ref_name}}" "$(echo ${{github.server_url}}/${{github.repository}}.git | sed s%://%://${{github.token}}@% )" .
|
||||||
|
DOCKER_HUB=$([ -n "${{secrets.DOCKER_HUB_USER}}" ] && [ -n "${{secrets.DOCKER_HUB_TOKEN}}" ] && [ -n "${{secrets.DOCKER_HUB_IMAGE}}" ] && echo "true" || echo "false")
|
||||||
|
REGISTRY="$(echo "${{github.server_url}}" | sed -E 's|https?://||')"
|
||||||
|
VERSION=$(cat package.json | grep version | grep -Eo '[0-9][[:alnum:]\.\/\-]+')
|
||||||
|
|
||||||
|
docker login -u "${{github.repository_owner}}" -p "${{secrets.DEPLOY_TOKEN}}" "$REGISTRY"
|
||||||
|
if [ "$DOCKER_HUB" = "true" ]; then docker login -u "${{secrets.DOCKER_HUB_USER}}" -p "${{secrets.DOCKER_HUB_TOKEN}}" docker.io; fi
|
||||||
|
|
||||||
|
docker build -t "$REGISTRY/${{github.repository}}:${{github.ref_name}}" .
|
||||||
|
docker push "$REGISTRY/${{github.repository}}:${{github.ref_name}}"
|
||||||
|
if [ "$DOCKER_HUB" = "true" ]; then
|
||||||
|
docker tag "$REGISTRY/${{github.repository}}:${{github.ref_name}}" "docker.io/${{secrets.DOCKER_HUB_IMAGE}}:${{github.ref_name}}"
|
||||||
|
docker push "docker.io/${{secrets.DOCKER_HUB_IMAGE}}:${{github.ref_name}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${{github.ref_name}}" = "master" ]; then
|
||||||
|
docker tag "$REGISTRY/${{github.repository}}:${{github.ref_name}}" "$REGISTRY/${{github.repository}}:$VERSION"
|
||||||
|
docker push "$REGISTRY/${{github.repository}}:$VERSION"
|
||||||
|
if [ "$DOCKER_HUB" = "true" ]; then
|
||||||
|
docker tag "$REGISTRY/${{github.repository}}:${{github.ref_name}}" "docker.io/${{secrets.DOCKER_HUB_IMAGE}}:$VERSION"
|
||||||
|
docker push "docker.io/${{secrets.DOCKER_HUB_IMAGE}}:$VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker tag "$REGISTRY/${{github.repository}}:${{github.ref_name}}" "$REGISTRY/${{github.repository}}:latest"
|
||||||
|
docker push "$REGISTRY/${{github.repository}}:latest"
|
||||||
|
if [ "$DOCKER_HUB" = "true" ]; then
|
||||||
|
docker tag "$REGISTRY/${{github.repository}}:${{github.ref_name}}" "docker.io/${{secrets.DOCKER_HUB_IMAGE}}:latest"
|
||||||
|
docker push "docker.io/${{secrets.DOCKER_HUB_IMAGE}}:latest"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy Container
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: node
|
||||||
|
needs:
|
||||||
|
- container
|
||||||
|
steps:
|
||||||
|
- name: Deploy
|
||||||
|
run: curl -X POST https://manage.zakscode.com/api/stacks/webhooks/3c8ec201-e8ad-4878-b35a-0f6a99d5f6cf
|
||||||
|
|||||||
+3
-13
@@ -16,16 +16,6 @@ COPY . .
|
|||||||
RUN if [ ! -d "dist" ]; then npm install && npm run build; fi
|
RUN if [ ! -d "dist" ]; then npm install && npm run build; fi
|
||||||
|
|
||||||
# Use Nginx to serve
|
# Use Nginx to serve
|
||||||
FROM nginx:1.23-alpine
|
FROM git.zakscode.com/momentum/momentum:latest
|
||||||
COPY --from=build /app/dist /usr/share/nginx/html
|
RUN rm -rf /app/server/public/assets /app/server/public/index.html
|
||||||
|
COPY --from=build /app/dist /app/server/public
|
||||||
# Copy aditional files
|
|
||||||
COPY package.json /usr/share/nginx/html
|
|
||||||
COPY docker/robots.txt /usr/share/nginx/html/robots.txt
|
|
||||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
|
||||||
|
|
||||||
# Setup environment varible script
|
|
||||||
COPY docker/setup-environment.sh /docker-entrypoint.d/setup-environment.sh
|
|
||||||
RUN chmod +x /docker-entrypoint.d/setup-environment.sh
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,18 +1,93 @@
|
|||||||
# ZaksCode
|
<!-- Header -->
|
||||||
Business website for ZaksCode. Provides some background information, contact inforation & a portfolio of projects.
|
<div id="top" align="center">
|
||||||
[View here](https://zakscode.com).
|
<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
|
## Table of Contents
|
||||||
[[_TOC_]]
|
- [ZaksCode](#top)
|
||||||
|
- [About](#about)
|
||||||
|
- [Demo](#demo)
|
||||||
|
- [Built With](#built-with)
|
||||||
|
- [Setup](#setup)
|
||||||
|
- [Production](#production)
|
||||||
|
- [Development](#development)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
## Cheatsheet
|
## About
|
||||||
```bash
|
|
||||||
# Start Angular development server
|
This the source code for `zakscode.com`.
|
||||||
npm run start
|
|
||||||
# Build production site
|
ZaksCode is the personal website of Zakary Timson & is used as his business page & portfolio.
|
||||||
npm run build
|
### Demo
|
||||||
# Build docker image
|
|
||||||
docker build -t gitlab.zakscode.com:5050/zakcode/zakscode:latest .
|
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.
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ http {
|
|||||||
autoindex off;
|
autoindex off;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
try_files $uri$args $uri$args/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-13
@@ -2,21 +2,13 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>ZaksCode</title>
|
<title>ZaksCode</title>
|
||||||
<link rel="icon" href="/logo.png">
|
<link rel="icon" href="/favicon.png">
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||||
|
<!-- momentum:meta -->
|
||||||
|
|
||||||
<meta property=”og:type” content=”website” />
|
<!-- momentum:theme -->
|
||||||
<meta property=”og:title” content=”Zakary Timson” />
|
|
||||||
<!-- <meta name=”twitter:title” content=”Zakary Timson” />-->
|
|
||||||
<meta name="description" content="Devops & Software Engineer" />
|
|
||||||
<meta property=”og:description” content=”Devops & Software Engineer” />
|
|
||||||
<!-- <meta name=”twitter:description” content=”Devops & Software Engineer” />-->
|
|
||||||
<meta property=”og:image” content=”https://zakscode.com/cloud.gif” />
|
|
||||||
<!-- <meta name=”twitter:image” content=”https://zakscode.com/cloud.gif” />-->
|
|
||||||
<meta property=”og:url” content=”https://zakscode.com” />
|
|
||||||
<meta property=”og:site_name” content=”ZaksCode” />
|
|
||||||
|
|
||||||
<script id="environment"></script>
|
<script id="environment"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
Generated
+1326
-720
File diff suppressed because it is too large
Load Diff
+7
-3
@@ -1,29 +1,33 @@
|
|||||||
{
|
{
|
||||||
"name": "zakscode",
|
"name": "zakscode",
|
||||||
"version": "2.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"start": "vite",
|
||||||
"build": "run-p type-check \"build-only {@}\" --",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build --force"
|
"type-check": "vue-tsc --build --force"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ztimson/momentum": "^1.5.4",
|
||||||
|
"@ztimson/utils": "^0.30.2",
|
||||||
|
"qrcode": "^1.5.4",
|
||||||
"vue": "^3.3.11",
|
"vue": "^3.3.11",
|
||||||
"vue-router": "^4.2.5"
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node18": "^18.2.2",
|
"@tsconfig/node18": "^18.2.2",
|
||||||
"@types/node": "^18.19.3",
|
"@types/node": "^18.19.3",
|
||||||
|
"@types/qrcode": "^1.5.6",
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
"@vue/tsconfig": "^0.5.0",
|
"@vue/tsconfig": "^0.5.0",
|
||||||
"bootstrap": "^5.3.2",
|
"bootstrap": "^5.3.2",
|
||||||
"npm-run-all2": "^6.1.1",
|
"npm-run-all2": "^6.1.1",
|
||||||
"sass": "^1.69.7",
|
"sass": "^1.69.7",
|
||||||
"typescript": "~5.3.0",
|
"typescript": "~5.3.0",
|
||||||
"vite": "^5.0.10",
|
"vite": "5.4.6",
|
||||||
"vue-tsc": "^1.8.25"
|
"vue-tsc": "^1.8.25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-23
@@ -1,28 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Foot from '@/components/foot.vue';
|
import {momentum} from '@/services/momentum.service';
|
||||||
import Profile from '@/components/profile.vue';
|
momentum.client.inject();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Spacer -->
|
<router-view />
|
||||||
<div class="w-100" style="height: min(75vh, 500px)"></div>
|
|
||||||
|
|
||||||
<!-- Content -->
|
|
||||||
<div class="cap-width mb-3 bg-white">
|
|
||||||
<!-- Header -->
|
|
||||||
<header class="px-4 d-flex justify-content-center justify-content-sm-start" style="background: #732222">
|
|
||||||
<profile style="transform: translateY(-33%)" />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Body -->
|
|
||||||
<main class="p-3">
|
|
||||||
<router-view></router-view>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<foot />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spacer -->
|
|
||||||
<div class="d-none d-sm-block w-100" style="height: 40px"></div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+151
-60
@@ -1,6 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Icon from '@/components/icon.vue';
|
import {momentum} from '@/services/momentum.service';
|
||||||
import {environment} from '@/environments/environment';
|
|
||||||
import {ref} from 'vue';
|
import {ref} from 'vue';
|
||||||
|
|
||||||
const disable = ref(false);
|
const disable = ref(false);
|
||||||
@@ -30,27 +29,6 @@ function validateEmail(email: string) {
|
|||||||
return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(email);
|
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) {
|
|
||||||
function formEncode(data: any): string {
|
|
||||||
return Object.entries(data).map(([key, value]) =>
|
|
||||||
encodeURIComponent(key) + '=' + encodeURIComponent(<any>value)
|
|
||||||
).join('&');
|
|
||||||
}
|
|
||||||
|
|
||||||
return fetch('https://postmail.invotes.com/send', {
|
|
||||||
method: 'post',
|
|
||||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
|
||||||
body: formEncode({
|
|
||||||
access_token: environment.postMailKey,
|
|
||||||
subject: `ZaksCode: ${subject}`,
|
|
||||||
text: `App: ZaksCode\nFrom: ${name} <${email}>\nSubject: ${subject}\n\nMessage:\n${message}`
|
|
||||||
})
|
|
||||||
}).then(async resp => {
|
|
||||||
if(!resp.ok) throw new Error(resp.statusText);
|
|
||||||
return await resp.text();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit() {
|
function submit() {
|
||||||
disable.value = true;
|
disable.value = true;
|
||||||
const d = data.value;
|
const d = data.value;
|
||||||
@@ -64,7 +42,7 @@ function submit() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if(errors.value.name || errors.value.email || errors.value.subject || errors.value.message) return disable.value = false;
|
if(errors.value.name || errors.value.email || errors.value.subject || errors.value.message) return disable.value = false;
|
||||||
send(d.name, d.email, d.subject, d.message).then(() => {
|
momentum.data.create('Contact', d).then(() => {
|
||||||
done.value = true;
|
done.value = true;
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -75,43 +53,156 @@ function submit() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Success Banner -->
|
<div>
|
||||||
<div v-if="done" class="alert alert-success">
|
<!-- Contact Form -->
|
||||||
<span>Success! We will be intouch shortly.</span>
|
<form class="cf">
|
||||||
</div>
|
<!-- Success Banner -->
|
||||||
<!-- Error Banner -->
|
<div v-if="done" class="cf-banner cf-banner--success">
|
||||||
<div v-if="errors?.banner" class="alert alert-danger">
|
<i class="fa fa-check-circle"/> Success! We will be in touch shortly.
|
||||||
Error: {{ (errors as any).banner }}
|
</div>
|
||||||
</div>
|
<!-- Error Banner -->
|
||||||
<!-- Contact Form -->
|
<div v-if="errors?.banner" class="cf-banner cf-banner--error">
|
||||||
<form>
|
<i class="fa fa-exclamation-circle"/> Error: {{ (errors as any).banner }}
|
||||||
<div class="d-flex flex-column flex-md-row flex-wrap justify-content-between">
|
</div>
|
||||||
<!-- Name -->
|
<div class="d-flex flex-c flex-md-r gap-2">
|
||||||
<div class="mb-2 p-0 pe-md-1 p-md-0" style="flex: 1 0 0;">
|
<!-- Name -->
|
||||||
<div class="input-group">
|
<div class="cf-field">
|
||||||
<span class="input-group-text"><icon name="user"/></span>
|
<i class="fa fa-user cf-icon"/>
|
||||||
<input class="form-control" type="text" placeholder="Name" v-model="data.name" v-bind:class="{'is-invalid': errors?.name}" :disabled="disable" required>
|
<input class="cf-input" type="text" placeholder="Name" v-model="data.name" :class="{'cf-input--error': errors?.name}" :disabled="disable" required>
|
||||||
|
</div>
|
||||||
|
<!-- Email -->
|
||||||
|
<div class="cf-field">
|
||||||
|
<i class="fa fa-envelope cf-icon"/>
|
||||||
|
<input class="cf-input" type="email" placeholder="Email" v-model="data.email" :class="{'cf-input--error': errors?.email}" :disabled="disable" required>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Email -->
|
<!-- Subject -->
|
||||||
<div class="mb-2 p-0 ps-md-1" style="flex: 1 0 0;">
|
<div class="cf-field mt-2">
|
||||||
<div class="input-group">
|
<i class="fa fa-book cf-icon"/>
|
||||||
<span class="input-group-text"><icon name="envelope"/></span>
|
<input class="cf-input" type="text" placeholder="Subject" v-model="data.subject" :class="{'cf-input--error': errors?.subject}" :disabled="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>
|
||||||
</div>
|
<!-- Message Body -->
|
||||||
<!-- Subject -->
|
<textarea class="cf-input cf-textarea mt-2" placeholder="Message" rows="5" v-model="data.message" :class="{'cf-input--error': errors?.message}" :disabled="disable" required></textarea>
|
||||||
<div class="input-group mb-2">
|
<!-- Buttons -->
|
||||||
<span class="input-group-text"><icon name="book"/></span>
|
<div class="cf-actions">
|
||||||
<input class="form-control" type="text" placeholder="Subject" v-model="data.subject" v-bind:class="{'is-invalid': errors?.subject}" :disabled="disable" required>
|
<button type="button" class="cf-btn" @click="reset()">Reset</button>
|
||||||
</div>
|
<button type="button" class="cf-btn cf-btn--primary" @click="submit" :disabled="disable">
|
||||||
<!-- Message Body -->
|
<i class="fa fa-paper-plane"/> Send Message
|
||||||
<textarea class="form-control" placeholder="Message" rows="5" v-model="data.message" v-bind:class="{'is-invalid': errors?.message}" :disabled="disable" required></textarea>
|
</button>
|
||||||
<!-- Buttons -->
|
</div>
|
||||||
<div class="text-end pt-3">
|
</form>
|
||||||
<button type="button" class="btn rounded-pill ms-3" @click="reset()">Reset</button>
|
</div>
|
||||||
<button type="button" class="btn btn-primary rounded-pill ms-3" @click="submit" :disabled="disable">Send Message</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Fields */
|
||||||
|
.cf-field {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.85rem;
|
||||||
|
color: #2c2c2c;
|
||||||
|
opacity: 0.4;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.65rem 0.85rem 0.65rem 2.25rem;
|
||||||
|
background: #fff;
|
||||||
|
border: 1.5px solid rgba(44, 44, 44, 0.15);
|
||||||
|
border-radius: 0;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-input::placeholder {
|
||||||
|
color: rgba(44, 44, 44, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-input:focus {
|
||||||
|
border-color: var(--theme-primary);
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--theme-primary) 25%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-input:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-input--error {
|
||||||
|
border-color: #c0392b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-textarea {
|
||||||
|
padding-left: 0.85rem;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Banners */
|
||||||
|
.cf-banner {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-banner--success {
|
||||||
|
background: #ecfdea;
|
||||||
|
border-left: 3px solid #55c02b;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-banner--error {
|
||||||
|
background: #fdecea;
|
||||||
|
border-left: 3px solid #c0392b;
|
||||||
|
color: #2c2c2c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
.cf-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-top: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-btn {
|
||||||
|
padding: 0.6rem 1.4rem;
|
||||||
|
border: 1.5px solid rgba(44, 44, 44, 0.2);
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: brightness(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cf-btn--primary {
|
||||||
|
position: relative;
|
||||||
|
background: var(--theme-primary);
|
||||||
|
border-color: var(--theme-primary);
|
||||||
|
color: var(--theme-primary-contrast);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ footer {
|
|||||||
<footer class="p-2 text-center">
|
<footer class="p-2 text-center">
|
||||||
Copyright © ZaksCode 2024 | All Rights Reserved
|
Copyright © ZaksCode 2024 | All Rights Reserved
|
||||||
<br>
|
<br>
|
||||||
Created by <a href="https://zakscode.com" target="_blank">Zak Timson</a>
|
Created by <a href="https://zakscode.com" target="_blank">Zak Timson</a> | Built with <a href="https://momentum.zakscode.com" target="_blank">Momentum</a>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import '../misc/konsole';
|
import '../modules/konsole';
|
||||||
import {onMounted, ref} from 'vue';
|
import {onMounted, ref} from 'vue';
|
||||||
|
|
||||||
const animate = ref(true);
|
const animate = ref(true);
|
||||||
@@ -28,48 +28,6 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.cli-container {
|
|
||||||
padding: 1rem;
|
|
||||||
background: #333;
|
|
||||||
font-family: monospace !important;
|
|
||||||
overflow-y: auto;
|
|
||||||
min-height: 150px;
|
|
||||||
max-height: 300px;
|
|
||||||
|
|
||||||
.cli-stdout {
|
|
||||||
flex-grow: 1;
|
|
||||||
color: #0f0;
|
|
||||||
|
|
||||||
.cli-stdout-line {
|
|
||||||
padding: 0;
|
|
||||||
min-height: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cli-stdin {
|
|
||||||
display: flex;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
.cli-stdin-prompt {
|
|
||||||
padding-right: 0.55em;
|
|
||||||
text-wrap: nowrap;
|
|
||||||
color: #0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cli-stdin-input {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
font-size: 1rem;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
color: #0f0;
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="konsole" @click="animate = false"></div>
|
<div id="konsole" @click="animate = false"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ import Icon from '@/components/icon.vue';
|
|||||||
<div class="me-sm-5">
|
<div class="me-sm-5">
|
||||||
<h1 class="m-0" style="font-size: 2.5rem">Zakary Timson</h1>
|
<h1 class="m-0" style="font-size: 2.5rem">Zakary Timson</h1>
|
||||||
<h2 class="mt-0 text-muted" style="font-size: 1.25rem">
|
<h2 class="mt-0 text-muted" style="font-size: 1.25rem">
|
||||||
DEVOPS & SOFTWARE ENGINEER
|
SOFTWARE ENGINEER
|
||||||
</h2>
|
</h2>
|
||||||
<ul class="m-0 p-0 text-start" style="list-style: none">
|
<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="map-marker-alt" class="me-1"/> London Ontario, Canada</li>
|
||||||
<li><icon name="envelope" class="me-1"/> <a href="mailto:zaktimson@gmail.com" target="_blank">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="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>
|
<li><icon name="git-alt" class="fa-brands me-1"/> <a href="https://git.zakscode.com" target="_blank">git.zakscode.com</a></li>
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type {Project} from '@/models/project';
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
projects: {type: Array as () => Project[], required: true}
|
projects: {type: Array as () => any[], required: true}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.invert {
|
||||||
|
filter: invert(100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="(p, i) in projects">
|
<div v-for="(p, i) in projects">
|
||||||
<div class="d-flex align-items-center border p-2" :class="i == 0 ? '' : 'border-top-0'">
|
<div class="d-flex align-items-center border p-2" :class="i == 0 ? '' : 'border-top-0'">
|
||||||
<div class="me-2">
|
<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>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
|
||||||
const resume = 'https://drive.google.com/file/d/1N1L2SYvqY49OJAR97cjjHANj0ModmZSy/view?usp=drive_link';
|
const resume = 'https://drive.google.com/file/d/1km4XqtXrYtoUnrDpEtDmAeKWh1qyp9nQ/view?usp=drive_link';
|
||||||
|
|
||||||
const refrences = [
|
const refrences = [
|
||||||
// ['Andre Mourinho', ''],
|
['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'],
|
['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'],
|
['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'],
|
['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'],
|
['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'],
|
['Ray Power', 'https://drive.google.com/file/d/1aP8Xpg3hlP0yQV8_sg9yeTgsrTRCcJZH/view?usp=drive_link'],
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="d-block d-md-none">
|
<div class="d-block d-md-none">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<a class="btn btn-outline-danger w-100" :href="resume" target="_blank">CSV / Resume</a>
|
<a class="btn btn-outline-danger w-100" :href="resume" target="_blank">Resume</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group-vertical w-100" role="group">
|
<div class="btn-group-vertical w-100" role="group">
|
||||||
<a v-for="ref in refrences" class="btn btn-outline-primary" :href="ref[1]" target="_blank">{{ref[0]}}</a>
|
<a v-for="ref in refrences" class="btn btn-outline-primary" :href="ref[1]" target="_blank">{{ref[0]}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-none d-md-block">
|
<div class="d-none d-md-flex align-items-start">
|
||||||
<a class="btn btn-outline-danger me-3" :href="resume" target="_blank">CSV / Resume</a>
|
<a class="btn btn-outline-danger me-3" :href="resume" target="_blank">Resume</a>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<a v-for="ref in refrences" class="btn btn-outline-primary" :href="ref[1]" target="_blank">{{ref[0]}}</a>
|
<a v-for="ref in refrences" class="btn btn-outline-primary" :href="ref[1]" target="_blank">{{ref[0]}}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
const devMode = location?.port == '5173';
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
postMailKey: (<any>window)?.env?.APP_POSTMAIL_KEY || (<any>import.meta).env.APP_POSTMAIL_ACCESS_TOKEN,
|
apiUrl: 'https://zakscode.com',
|
||||||
|
devMode
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import './main.scss'
|
import './main.scss'
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './routes'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
|||||||
@@ -1,344 +0,0 @@
|
|||||||
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',
|
|
||||||
|
|
||||||
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.prompt()}</label>
|
|
||||||
<input id="${elementId}-cli-stdin-input" class="cli-stdin-input" type="text" />
|
|
||||||
</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.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('/') : '');
|
|
||||||
},
|
|
||||||
prompt: () => `${window.cli.user}@${window.cli.hostname}:${window.cli.pwd}${window.cli.user == 'root' ? '#' : '$'}`,
|
|
||||||
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, silent=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) {
|
|
||||||
window.cli.stdErr(`${window.cli.prompt()} ${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(!!out && !silent) window.cli.stdOut(`${window.cli.prompt()} ${command}\n${out}`);
|
|
||||||
} catch(err) {
|
|
||||||
console.error(err);
|
|
||||||
window.cli.stdErr(`${window.cli.prompt()} ${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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.cli.exec['banner'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Display login banner';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
window.cli.stdOut(`Konsole ${window.cli.version} LTS ${window.cli.hostname} tty1\n\n${window.cli.hostname} login: ${window.cli.user}\npassword:\n\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['cd'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Clear console output';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
const path = window.cli.fs(args[0]);
|
|
||||||
if(!path) throw new Error(`cd: \'${args[0]}\': No such file or directory`)
|
|
||||||
|
|
||||||
window.cli.pwd = window.cli.path(args[0]);
|
|
||||||
window.cli._prompt.innerText = window.cli.prompt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['clear'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Clear console output';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
window.cli._output.innerHTML = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['date'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Get current date & time';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
return (new Date()).toLocaleString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['echo'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Output text to console';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
return args.join(' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['exit'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'End session';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
window.cli.stdIn('clear');
|
|
||||||
window.cli._history = [];
|
|
||||||
window.cli._index = 0;
|
|
||||||
window.cli.stdIn('banner');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['help'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Display all commands';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
return `Konsole v${window.cli.version} - A prototype bash emulator written by Zakary Timson\n\n` +
|
|
||||||
Object.keys(window.cli.exec).map(command => `${command} - ${window.cli.exec[command].help()}`).join('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['hostname'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Get computer hostname';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
return window.cli.hostname;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['ls'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Display directory contents';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
const target = window.cli.fs(args[0]);
|
|
||||||
if(!target) throw new Error(`ls: cannot access \'${args[0]}\': No such file or directory`)
|
|
||||||
return Object.keys(target).reduce((acc, p) => acc + `${p}\n`, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['man'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'View command\'s manual';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
return window.cli.exec[args[0]].help();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['mkdir'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Create new directory';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
if(!args[0]) throw new Error('mkdir: missing operand');
|
|
||||||
window.cli.fs(args[0], {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['pwd'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Get present working directory';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
return window.cli.pwd;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['rm'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Delete file or directory';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
if(!args[0]) throw new Error('rm: missing operand');
|
|
||||||
window.cli.fs(args[0], null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
window.cli.exec['whoami'] = {
|
|
||||||
autocomplete: () => {
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
help: () => {
|
|
||||||
return 'Get current user account';
|
|
||||||
},
|
|
||||||
run: args => {
|
|
||||||
return window.cli.user;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface Project {
|
|
||||||
icon?: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
link: string;
|
|
||||||
source?: string;
|
|
||||||
}
|
|
||||||
@@ -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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
window.cli.exec['clear'] = {
|
||||||
|
autocomplete: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
help: () => {
|
||||||
|
return 'Clear console output';
|
||||||
|
},
|
||||||
|
run: args => {
|
||||||
|
setTimeout(() => window.cli._output.innerHTML = '', 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
window.cli.exec['date'] = {
|
||||||
|
autocomplete: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
help: () => {
|
||||||
|
return 'Get current date & time';
|
||||||
|
},
|
||||||
|
run: args => {
|
||||||
|
return new Date().toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
window.cli.exec['echo'] = {
|
||||||
|
autocomplete: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
help: () => {
|
||||||
|
return 'Output text to console';
|
||||||
|
},
|
||||||
|
run: args => {
|
||||||
|
return args.join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
window.cli.exec['hostname'] = {
|
||||||
|
autocomplete: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
help: () => {
|
||||||
|
return 'Get computer hostname';
|
||||||
|
},
|
||||||
|
run: args => {
|
||||||
|
return window.cli.hostname;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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], {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
window.cli.exec['pwd'] = {
|
||||||
|
autocomplete: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
help: () => {
|
||||||
|
return 'Get present working directory';
|
||||||
|
},
|
||||||
|
run: args => {
|
||||||
|
return window.cli.pwd;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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], '');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
window.cli.exec['whoami'] = {
|
||||||
|
autocomplete: () => {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
help: () => {
|
||||||
|
return 'Get current user account';
|
||||||
|
},
|
||||||
|
run: args => {
|
||||||
|
return window.cli.user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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('');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import BusinessCard from '@/views/BusinessCard.vue';
|
||||||
|
import Layout from '@/views/Layout.vue';
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Home from '@/views/Home.vue'
|
import Home from '@/views/Home.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory((<any>import.meta).env.BASE_URL),
|
history: createWebHistory((<any>import.meta).env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{path: '/', name: 'home', component: Home}
|
{path: '/', component: Layout, children: [{path: '', component: Home}]},
|
||||||
|
{path: '/card', name: 'home', component: BusinessCard}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import {environment} from '@/environments/environment';
|
||||||
|
import {Momentum} from '@ztimson/momentum';
|
||||||
|
import {PathEvent} from '@ztimson/utils';
|
||||||
|
import {ref, onUnmounted} from 'vue';
|
||||||
|
|
||||||
|
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",
|
||||||
|
analytics: "prompt",
|
||||||
|
logLevel: "ERROR",
|
||||||
|
});
|
||||||
|
|
||||||
|
momentum.auth.on('logout', () => location.reload());
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Icon from '@/components/icon.vue';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
|
import {computed, onMounted, ref} from 'vue';
|
||||||
|
|
||||||
|
// ---------- Your info (edit here) ----------
|
||||||
|
const person = {
|
||||||
|
first: 'Zakary',
|
||||||
|
last: 'Timson',
|
||||||
|
title: 'Software Engineer',
|
||||||
|
org: 'ZaksCode',
|
||||||
|
email: 'zaktimson@gmail.com',
|
||||||
|
phone: '+1 (519) 917-1046', // e.g. '+1 519 555 0123' - leave empty to hide
|
||||||
|
city: 'London',
|
||||||
|
region: 'Ontario',
|
||||||
|
country: 'Canada',
|
||||||
|
website: 'https://zakscode.com',
|
||||||
|
github: 'https://github.com/ztimson',
|
||||||
|
git: 'https://git.zakscode.com',
|
||||||
|
photo: '/profile.jpg'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------- Rows displayed on the card ----------
|
||||||
|
const rows = computed(() => [
|
||||||
|
{key: 'email', label: 'Email', value: person.email, href: `mailto:${person.email}`, icon: 'envelope'},
|
||||||
|
person.phone && {key: 'phone', label: 'Phone', value: person.phone, href: `tel:${person.phone.replace(/[^+\d]/g, '')}`, icon: 'phone'},
|
||||||
|
{key: 'website', label: 'Website', value: person.website.replace(/^https?:\/\//, ''), copy: person.website, href: person.website, icon: 'globe'},
|
||||||
|
].filter(Boolean) as any[]);
|
||||||
|
|
||||||
|
// ---------- Copy to clipboard ----------
|
||||||
|
const copied = ref('');
|
||||||
|
let copyTimer: any;
|
||||||
|
|
||||||
|
async function copy(key: string, text: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} catch {
|
||||||
|
const el = document.createElement('textarea');
|
||||||
|
el.value = text;
|
||||||
|
el.style.position = 'fixed';
|
||||||
|
el.style.opacity = '0';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
el.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
copied.value = key;
|
||||||
|
clearTimeout(copyTimer);
|
||||||
|
copyTimer = setTimeout(() => copied.value = '', 1600);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- vCard ----------
|
||||||
|
const esc = (s: string) => s.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\r?\n/g, '\\n');
|
||||||
|
const fold = (line: string) => line.match(/.{1,74}/g)?.join('\r\n ') ?? line;
|
||||||
|
|
||||||
|
function buildVCard(photoBase64?: string) {
|
||||||
|
const lines = [
|
||||||
|
'BEGIN:VCARD',
|
||||||
|
'VERSION:3.0',
|
||||||
|
`N:${esc(person.last)};${esc(person.first)};;;`,
|
||||||
|
`FN:${esc(person.first + ' ' + person.last)}`,
|
||||||
|
`ORG:${esc(person.org)}`,
|
||||||
|
`TITLE:${esc(person.title)}`,
|
||||||
|
`EMAIL;TYPE=INTERNET,WORK:${person.email}`,
|
||||||
|
person.phone && `TEL;TYPE=CELL:${person.phone}`,
|
||||||
|
`ADR;TYPE=WORK:;;;${esc(person.city)};${esc(person.region)};;${esc(person.country)}`,
|
||||||
|
`URL:${person.website}`,
|
||||||
|
`item1.URL:${person.github}`,
|
||||||
|
'item1.X-ABLabel:GitHub',
|
||||||
|
`item2.URL:${person.git}`,
|
||||||
|
'item2.X-ABLabel:Git',
|
||||||
|
photoBase64 && `PHOTO;ENCODING=b;TYPE=JPEG:${photoBase64}`,
|
||||||
|
'END:VCARD'
|
||||||
|
].filter(Boolean) as string[];
|
||||||
|
return lines.map(fold).join('\r\n') + '\r\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shrink the profile picture so the .vcf stays small
|
||||||
|
async function loadPhoto(size = 256): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
const blob = await (await fetch(person.photo)).blob();
|
||||||
|
const bmp = await createImageBitmap(blob);
|
||||||
|
const side = Math.min(bmp.width, bmp.height);
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = canvas.height = size;
|
||||||
|
canvas.getContext('2d')!.drawImage(
|
||||||
|
bmp, (bmp.width - side) / 2, (bmp.height - side) / 2, side, side, 0, 0, size, size
|
||||||
|
);
|
||||||
|
return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
async function downloadContact() {
|
||||||
|
saving.value = true;
|
||||||
|
try {
|
||||||
|
const vcf = buildVCard(await loadPhoto());
|
||||||
|
const url = URL.createObjectURL(new Blob([vcf], {type: 'text/vcard;charset=utf-8'}));
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${person.first}-${person.last}.vcf`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- QR code (no photo, so it stays easy to scan) ----------
|
||||||
|
const qr = ref('');
|
||||||
|
onMounted(async () => {
|
||||||
|
qr.value = await QRCode.toDataURL(buildVCard(), {
|
||||||
|
errorCorrectionLevel: 'M',
|
||||||
|
margin: 1,
|
||||||
|
width: 600,
|
||||||
|
color: {dark: '#1c1c1e', light: '#ffffff'}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bc-page">
|
||||||
|
<article class="bc">
|
||||||
|
<!-- Banner -->
|
||||||
|
<div class="bc-banner">
|
||||||
|
<span class="bc-ring bc-ring--1"></span>
|
||||||
|
<span class="bc-ring bc-ring--2"></span>
|
||||||
|
<span class="bc-ring bc-ring--3"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Identity -->
|
||||||
|
<div class="bc-identity">
|
||||||
|
<img class="bc-avatar" :src="person.photo" :alt="`${person.first} ${person.last}`">
|
||||||
|
<h1 class="bc-name">{{person.first}} {{person.last}}</h1>
|
||||||
|
<div class="bc-title">{{person.title}}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details -->
|
||||||
|
<ul class="bc-list">
|
||||||
|
<li v-for="r in rows" :key="r.key" class="bc-row">
|
||||||
|
<span class="bc-row-icon"><icon :name="r.icon" :class="`fa fa-${r.icon}`" /></span>
|
||||||
|
<div class="bc-row-text">
|
||||||
|
<span class="bc-row-label">{{r.label}}</span>
|
||||||
|
<a v-if="r.href" class="bc-row-value" :href="r.href" :target="r.href.startsWith('http') ? '_blank' : undefined" rel="noopener">{{r.value}}</a>
|
||||||
|
<span v-else class="bc-row-value">{{r.value}}</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="bc-copy" :class="{'bc-copy--done': copied == r.key}" :aria-label="`Copy ${r.label}`" @click="copy(r.key, r.copy || r.value)">
|
||||||
|
<icon :name="copied == r.key ? 'check' : 'copy'" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- QR -->
|
||||||
|
<div class="bc-qr">
|
||||||
|
<div class="bc-qr-box">
|
||||||
|
<img v-if="qr" :src="qr" alt="Contact QR code">
|
||||||
|
</div>
|
||||||
|
<div class="bc-qr-caption">Scan to add me to your contacts</div>
|
||||||
|
<a v-if="qr" class="bc-qr-link" :href="qr" :download="`${person.first}-${person.last}-QR.png`">Download QR</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Primary action -->
|
||||||
|
<div class="bc-section">
|
||||||
|
<button type="button" class="bc-save" :disabled="saving" @click="downloadContact">
|
||||||
|
<icon name="user-plus" /> Save to Contacts
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Toast -->
|
||||||
|
<div class="bc-toast" :class="{'bc-toast--show': copied}" role="status">Copied to clipboard</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.bc-page {
|
||||||
|
--bc-accent: #732222;
|
||||||
|
--bc-accent-2: #a63a3a;
|
||||||
|
--bc-ink: #1c1c1e;
|
||||||
|
--bc-muted: #7a7a82;
|
||||||
|
--bc-line: #ececf0;
|
||||||
|
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 16px;
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 600px at 10% -10%, #8f2d2d 0%, transparent 60%),
|
||||||
|
radial-gradient(900px 500px at 110% 110%, #3b1010 0%, transparent 60%),
|
||||||
|
#2a0d0d;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
color: var(--bc-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card */
|
||||||
|
.bc {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Banner */
|
||||||
|
.bc-banner {
|
||||||
|
position: relative;
|
||||||
|
height: 130px;
|
||||||
|
background: linear-gradient(135deg, var(--bc-accent) 0%, var(--bc-accent-2) 100%);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-ring {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1.5px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
.bc-ring--1 { width: 220px; height: 220px; right: -60px; top: -90px; }
|
||||||
|
.bc-ring--2 { width: 140px; height: 140px; right: -20px; top: -50px; }
|
||||||
|
.bc-ring--3 { width: 300px; height: 300px; left: -140px; bottom: -190px; }
|
||||||
|
|
||||||
|
.bc-org {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
left: 22px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Identity */
|
||||||
|
.bc-identity {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 24px 4px;
|
||||||
|
margin-top: -60px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-avatar {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 5px solid #fff;
|
||||||
|
box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.35);
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-name {
|
||||||
|
margin: 14px 0 2px;
|
||||||
|
font-size: 1.65rem;
|
||||||
|
line-height: 1.15;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-title {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--bc-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Save button */
|
||||||
|
.bc-section {
|
||||||
|
padding: 20px 24px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-save {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(135deg, var(--bc-accent) 0%, var(--bc-accent-2) 100%);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 10px 20px -10px var(--bc-accent);
|
||||||
|
transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s;
|
||||||
|
}
|
||||||
|
.bc-save:hover { transform: translateY(-1px); box-shadow: 0 14px 24px -10px var(--bc-accent); }
|
||||||
|
.bc-save:active { transform: translateY(0); }
|
||||||
|
.bc-save:disabled { opacity: 0.6; cursor: progress; }
|
||||||
|
|
||||||
|
/* Rows */
|
||||||
|
.bc-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px 12px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.bc-row:hover { background: #f7f7f9; }
|
||||||
|
|
||||||
|
.bc-row-icon {
|
||||||
|
flex: none;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: color-mix(in srgb, var(--bc-accent) 10%, white);
|
||||||
|
color: var(--bc-accent);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-row-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-row-label {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--bc-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-row-value {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--bc-ink);
|
||||||
|
text-decoration: none;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
a.bc-row-value:hover { color: var(--bc-accent); }
|
||||||
|
|
||||||
|
.bc-copy {
|
||||||
|
flex: none;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1.5px solid var(--bc-line);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--bc-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.bc-copy:hover { border-color: var(--bc-accent); color: var(--bc-accent); }
|
||||||
|
.bc-copy:active { transform: scale(0.92); }
|
||||||
|
.bc-copy--done { background: #2e9e5b; border-color: #2e9e5b; color: #fff; }
|
||||||
|
.bc-copy--done:hover { background: #2e9e5b; border-color: #2e9e5b; color: #fff; }
|
||||||
|
|
||||||
|
/* QR */
|
||||||
|
.bc-qr {
|
||||||
|
padding: 24px 20px 18px;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px dashed var(--bc-line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-qr-box {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 0 0 1.5px var(--bc-line), 0 12px 24px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.bc-qr-box img { width: 100%; height: 100%; display: block; border-radius: 8px; }
|
||||||
|
|
||||||
|
.bc-qr-caption {
|
||||||
|
margin-top: 14px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--bc-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bc-qr-link {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--bc-accent);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.bc-qr-link:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Toast */
|
||||||
|
.bc-toast {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 28px;
|
||||||
|
transform: translate(-50%, 20px);
|
||||||
|
padding: 0.65rem 1.1rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(28, 28, 30, 0.92);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.bc-toast--show { opacity: 1; transform: translate(-50%, 0); }
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
.bc-page { padding: 0; align-items: stretch; }
|
||||||
|
.bc { max-width: none; border-radius: 0; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+30
-34
@@ -4,33 +4,33 @@ import Contact from '@/components/contact.vue';
|
|||||||
import Konsole from '@/components/konsole.vue';
|
import Konsole from '@/components/konsole.vue';
|
||||||
import Projects from '@/components/projects.vue';
|
import Projects from '@/components/projects.vue';
|
||||||
import Refrences from '@/components/refrences.vue';
|
import Refrences from '@/components/refrences.vue';
|
||||||
import type {Project as P} from '@/models/project';
|
import {momentum} from '@/services/momentum.service';
|
||||||
import {ref} from 'vue';
|
import {sortByProp} from '@ztimson/utils';
|
||||||
|
import {computed, onMounted, ref, watch} from 'vue';
|
||||||
|
|
||||||
const services: P[] = [
|
// Get favorites
|
||||||
{name: 'Formula Manager', icon: 'https://git.zakscode.com/avatars/7ec6bfd66b2bf9bad5c43c75a33f9cb3f6609b05c33a31f5d1e524a567cd09c1?size=280', link: 'https://screenprintingsuppliescanada.com/formulation-manager', description: 'A web & computer application used by FH&Sons to record chemical formulas & distribute them to clients'},
|
const showcase = ref<any[]>([]);
|
||||||
{name: 'Map Alliance', icon: 'https://maps.zakscode.com/assets/images/logo.png', link: 'https://maps.zakscode.com', description: 'An online GIS tool which enables users to view, edit & share various "marked-up" maps'},
|
momentum.data.read('Repos').then(resp =>
|
||||||
{name: 'Phone Reminders', icon: 'https://phone-reminders.com/phone-reminders.png', link: 'https://phone-reminders.com', description: 'Automatically call & send SMS reminders to clients for events using Google Calendar'},
|
showcase.value = resp.toSorted(sortByProp('order'))
|
||||||
];
|
);
|
||||||
|
|
||||||
const openSource: P[] = [
|
const groups = computed(() => showcase.value.reduce((acc, r: any) => {
|
||||||
{name: 'ETF Demo', icon: 'https://git.zakscode.com/repo-avatars/0709db0c51d295d2d29b709865bd95f26e351f72a5c993ca63cd9ec4b4a07f43', link: 'https://etf.zakscode.com', source: 'https://git.zakscode.com/ztimson/etf-demo', description: 'Compare CSV files containing "Electronically Traded Funds" data (Check source for CSV files)'},
|
if(!acc[r.type]) acc[r.type] = [];
|
||||||
{name: 'Legio 30', icon: 'https://git.zakscode.com/repo-avatars/f66e3d6f5ff4646b45e859f6bf00c0e0de0621d8a45a47481d53d67b67700f2a', link: 'https://legio-30.org', source: 'https://git.zakscode.com/ztimson/legio-30', description: 'Website for a non-profit Roman re-enactment group from Southern Ontario'},
|
acc[r.type].push(r);
|
||||||
{name: 'Pelican Landing', icon: 'https://git.zakscode.com/ztimson/pelican-landing/raw/branch/develop/src/assets/logo.png', link: 'https://pelican-landing.zakscode.com', source: 'https://git.zakscode.com/ztimson/pelican-landing', description: 'Business website for a hunting & fishing lodge on the Lage of Woods in Northern Ontario '},
|
return acc;
|
||||||
{name: 'Persist', icon: 'https://git.zakscode.com/repo-avatars/89f6c36caf75762ed9f7f98b69044b7db30da5230be7c5cea54f8a1158f1669a', link: 'https://www.npmjs.com/package/@ztimson/persist', source: 'https://git.zakscode.com/ztimson/persist', description: 'Typescript library to sync variables with LocalStorage & persist state through page reloads'},
|
}, {}))
|
||||||
{name: 'PyBar', icon: 'https://git.zakscode.com/repo-avatars/002f97340c2781ccfa5d09fde97403fd499c39a9ad5675dc0edf05a8396e9ac5', link: 'https://git.zakscode.com/ztimson/py-bar', source: 'https://git.zakscode.com/ztimson/py-bar', description: 'Python library to display ASCII progress bars using iterators'},
|
|
||||||
{name: 'Transmute', icon: 'https://git.zakscode.com/repo-avatars/b497daaf22a214fe6d6cc35b8ec217cd22401b668dff93dcfcc7557bd8a46d96', link: 'https://git.zakscode.com/ztimson/transmute', source: 'https://git.zakscode.com/ztimson/transmute', description: 'Distributed video conversion tool with built in WebUI'},
|
|
||||||
{name: 'ZaksCode', icon: 'https://git.zakscode.com/repo-avatars/590279cb4b176c6a7924364c7b0ef78afa80696703abe5bef8d9ce7e12477f3d', link: 'https://zakscode.com', source: 'https://git.zakscode.com/ztimson/zakscode', description: 'Source code for this website, ZaksCode.com'},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Get repository count
|
// Get repository count
|
||||||
let remainder = ref(0);
|
let projects = ref(0);
|
||||||
fetch('https://git.zakscode.com/api/v1/repos/search', {
|
const remainder = computed(() => projects.value - showcase.value.filter(r => r.source).length);
|
||||||
method: 'get',
|
|
||||||
headers: {"Content-Type": "application/json"}
|
onMounted(() => {
|
||||||
}).then(async repos => {
|
fetch('https://git.zakscode.com/api/v1/repos/search?limit=1000', {
|
||||||
const data = (await repos.json())?.data;
|
method: 'get',
|
||||||
remainder.value = data.length - openSource.length;
|
headers: {"Content-Type": "application/json"}
|
||||||
|
}).then(async (resp: any) => {
|
||||||
|
projects.value = (await resp.json())?.data?.length;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -48,9 +48,9 @@ fetch('https://git.zakscode.com/api/v1/repos/search', {
|
|||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-wrap justify-content-around">
|
<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="#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="#6d9eeb" icon="code" offset="2px" title="Develop" text="Goals are broken down into tasks and prioritized in our ticketing system. We then quickly prototype using AI and CI/CD to deliver requests with short delay"/>
|
||||||
<card color="#e69138" icon="message" offset="3px" title="Feedback" text="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."/>
|
<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."/>
|
<card color="#674ea7" icon="play" offset="2px" title="Release" text="Once all goals are complete we do a final cleanup and work with you to automatically deploy the product & future updates to any location"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ fetch('https://git.zakscode.com/api/v1/repos/search', {
|
|||||||
<hr class="mb-4">
|
<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%;">
|
<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 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 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>
|
<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">
|
<div class="mt-4">
|
||||||
<h4 class="mb-3 text-muted">CSV & References</h4>
|
<h4 class="mb-3 text-muted">CSV & References</h4>
|
||||||
@@ -72,13 +72,9 @@ fetch('https://git.zakscode.com/api/v1/repos/search', {
|
|||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<h3 class="m-0">Projects</h3>
|
<h3 class="m-0">Projects</h3>
|
||||||
<hr class="mb-4">
|
<hr class="mb-4">
|
||||||
<div class="mb-4">
|
<div class="mb-4" v-for="(group, name) in groups">
|
||||||
<h4 class="mb-3 text-muted">Products & Services</h4>
|
<h4 class="mb-3 text-muted">{{name}}</h4>
|
||||||
<projects :projects="services"/>
|
<projects :projects="group"/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 class="mb-3 text-muted">Open Source</h4>
|
|
||||||
<projects :projects="openSource"/>
|
|
||||||
</div>
|
</div>
|
||||||
<a v-if="remainder" class="float-end m-2" href="https://git.zakscode.com/explore" target="_blank">See {{remainder}} More...</a>
|
<a v-if="remainder" class="float-end m-2" href="https://git.zakscode.com/explore" target="_blank">See {{remainder}} More...</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Foot from '@/components/foot.vue';
|
||||||
|
import Profile from '@/components/profile.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- Spacer -->
|
||||||
|
<div class="w-100" style="height: min(75vh, 500px)"></div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="cap-width mb-3 bg-white">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="px-4 d-flex justify-content-center justify-content-sm-start" style="background: #732222">
|
||||||
|
<profile style="transform: translateY(-33%)" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<main class="p-3">
|
||||||
|
<router-view></router-view>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<foot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Spacer -->
|
||||||
|
<div class="d-none d-lg-block w-100" style="height: 40px"></div>
|
||||||
|
</template>
|
||||||
+4
-3
@@ -9,10 +9,11 @@
|
|||||||
"src/**/__tests__/*"
|
"src/**/__tests__/*"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"composite": true,
|
||||||
|
"lib": ["DOM", "ESNext"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"noEmit": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
|
|||||||
Reference in New Issue
Block a user