Compare commits

...

4 Commits

Author SHA1 Message Date
88761a6a66 Added build 2024-01-04 19:59:19 -05:00
6b28ed61bd Website updates 2024-01-04 19:56:46 -05:00
4b3e89fa59 Website updates 2024-01-03 23:49:40 -05:00
0e277a1478 Website updates 2024-01-03 23:49:28 -05:00
68 changed files with 2786 additions and 1150 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
APP_POSTMAIL_ACCESS_TOKEN=s7uhce84sx6fayy5xlq0nrtx

36
.github/workflows/build.yaml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Build Website
run-name: Build Website
on:
push:
jobs:
build:
name: Build NPM Project
runs-on: ubuntu-latest
container: node
steps:
- name: Clone Repository
uses: ztimson/actions/clone@develop
- name: Install Dependencies
run: npm i
- name: Build Project
run: npm run build
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: website
path: dist
retention-days: 7
publish:
name: Build & Push Dockerfile
needs: build
uses: ztimson/actions/.github/workflows/docker.yaml@develop
with:
name: ztimson/zakscode
repository: ${{github.server_url}}/${{github.repository}}.git
pass: ${{secrets.DEPLOY_TOKEN}}

74
.gitignore vendored
View File

@ -1,48 +1,30 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# compiled output
/dist
/tmp
/out-tsc
# Only exists if Bazel was run
/bazel-out
# dependencies
/node_modules
# profiling files
chrome-profiler-events.json
speed-measure-plugin.json
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
node_modules
.DS_Store
Thumbs.db
/package-lock.json
/.angular/
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -1,79 +0,0 @@
image: node:16
npm:
stage: build
artifacts:
paths:
- dist
expire_in: 1 week
cache:
- key:
files:
- package.json
paths:
- node_modules
- package-lock.json
policy: pull-push
- key: $CI_PIPELINE_ID
paths:
- dist
policy: push
script:
- npm i
- npm run build
rules:
- if: $CI_COMMIT_BRANCH
audit:
stage: test
cache:
- key:
files:
- package.json
paths:
- node_modules
policy: pull
script:
- AUDIT=$(npm audit)
- echo "vulnerabilities_high $(echo $AUDIT | grep -oE '[0-9]+ high' | grep -oE '[0-9]+' || echo 0)" > metrics.txt
- echo "vulnerabilities_medium $(echo $AUDIT | grep -oE '[0-9]+ moderate' | grep -oE '[0-9]+' || echo 0)" >> metrics.txt
- echo "vulnerabilities_low $(echo $AUDIT | grep -oE '[0-9]+ low' | grep -oE '[0-9]+' || echo 0)" >> metrics.txt
- echo "$AUDIT"
artifacts:
reports:
metrics: metrics.txt
rules:
- if: $CI_COMMIT_BRANCH
registry:
stage: deploy
image: docker
cache:
- key: $CI_PIPELINE_ID
paths:
- dist
policy: pull
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_JOB_TOKEN $CI_REGISTRY
script:
- TAG=$([ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ] && echo "latest" || echo "$CI_COMMIT_BRANCH" | sed -E "s/[_/]/-/g")
- docker build --no-cache -t "$CI_REGISTRY_IMAGE:$TAG" .
- docker push "$CI_REGISTRY_IMAGE:$TAG"
rules:
- if: $CI_COMMIT_BRANCH
tag:
stage: deploy
image:
name: alpine/git
entrypoint: [ "" ]
cache: [ ]
before_script:
- git remote set-url origin "https://ReleaseBot:$DEPLOY_TOKEN@$CI_SERVER_HOST/$CI_PROJECT_PATH.git"
script:
- VERSION=$(cat package.json | grep version | grep -Eo ':.+' | grep -Eo '[[:alnum:]\.\/\-]+')
- git tag -f $VERSION $CI_COMMIT_SHA
- git push -f origin $VERSION
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
allow_failure: true

View File

@ -1,25 +1,31 @@
FROM node:16 as build
FROM node:20-alpine as build
# Variables
ARG NODE_ENV=prod
ARG NODE_OPTIONS=""
ARG NODE_OPTIONS="--max_old_space_size=4096"
ENV NG_CLI_ANALYTICS=ci \
NODE_ENV=${NODE_ENV} \
NODE_OPTIONS=${NODE_OPTIONS}
# Setup
RUN npm config set unsafe-perm true && \
mkdir /app
RUN mkdir /app
WORKDIR /app
COPY . .
# Install & build
RUN if [ ! -d "dist" ] && [ ! -d "node_modules" ]; then npm install; fi
RUN if [ ! -d "dist" ]; then npm run build; fi
# Build
RUN if [ ! -d "dist" ]; then npm install && npm run build; fi
# Use Nginx to serve
FROM nginx:1.20-alpine
COPY --from=build /app/dist/zakscode /usr/share/nginx/html
FROM nginx:1.23-alpine
COPY --from=build /app/dist /usr/share/nginx/html
# 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

View File

@ -1,111 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"zakscode": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/zakscode",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "zakscode:build:production"
},
"development": {
"browserTarget": "zakscode:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "zakscode:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.scss"
],
"scripts": []
}
}
}
}
},
"defaultProject": "zakscode"
}

View File

@ -2,38 +2,30 @@ worker_processes auto;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
include mime.types;
default_type application/octet-stream;
gzip on;
gzip_proxied any;
gzip_types text/plain text/css application/xml application/xhtml+xml application/rss+xml application/javascript application/x-javascript application/json application/x-font-woff;
gzip_vary on;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
gzip on;
gzip_proxied any;
gzip_types text/plain text/css application/xml application/xhtml+xml application/rss+xml application/javascript application/x-javascript application/json application/x-font-woff;
gzip_vary on;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";
sendfile off;
keepalive_timeout 65;
sendfile off;
keepalive_timeout 65;
server {
listen 80;
index index.html;
root /usr/share/nginx/html;
autoindex off;
server {
listen 80;
index index.html;
root /usr/share/nginx/html;
autoindex off;
location / {
try_files $uri$args $uri$args/ /index.html;
}
location ~ \.css {
add_header Content-Type text/css;
}
location ~ \.js {
add_header Content-Type application/x-javascript;
}
}
location / {
try_files $uri$args $uri$args/ /index.html;
}
}
}

View File

@ -0,0 +1,7 @@
#!/usr/bin/env sh
JSON_STRING='window.env = { \
APP_POSTMAIL_ACCESS_TOKEN: "'"${APP_POSTMAIL_ACCESS_TOKEN}"'", \
}'
sed -i "s@<script id=\"environment\"></script>@<script>${JSON_STRING}</script>@" /usr/share/nginx/html/index.html
exec "$@"

28
index.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>ZaksCode</title>
<link rel="icon" href="/logo.png">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta property=”og:type” content=”website” />
<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>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,44 +0,0 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, './coverage/zakscode'),
subdir: '.',
reporters: [
{ type: 'html' },
{ type: 'text-summary' }
]
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

1845
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,29 @@
{
"name": "zakscode",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "~13.3.0",
"@angular/cdk": "^13.3.5",
"@angular/common": "~13.3.0",
"@angular/compiler": "~13.3.0",
"@angular/core": "~13.3.0",
"@angular/forms": "~13.3.0",
"@angular/material": "^13.3.5",
"@angular/platform-browser": "~13.3.0",
"@angular/platform-browser-dynamic": "~13.3.0",
"@angular/router": "~13.3.0",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"zone.js": "~0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "~13.3.4",
"@angular/cli": "~13.3.4",
"@angular/compiler-cli": "~13.3.0",
"@types/jasmine": "~3.10.0",
"@types/node": "^12.11.1",
"jasmine-core": "~4.0.0",
"karma": "~6.3.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.1.0",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "~1.7.0",
"typescript": "~4.6.2"
}
"name": "zakscode",
"version": "2.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
},
"dependencies": {
"vue": "^3.3.11",
"vue-router": "^4.2.5"
},
"devDependencies": {
"@tsconfig/node18": "^18.2.2",
"@types/node": "^18.19.3",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/tsconfig": "^0.5.0",
"bootstrap": "^5.3.2",
"npm-run-all2": "^6.1.1",
"sass": "^1.69.7",
"typescript": "~5.3.0",
"vite": "^5.0.10",
"vue-tsc": "^1.8.25"
}
}

View File

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 58 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

1
public/cycle.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
public/logo.psd Normal file

Binary file not shown.

BIN
public/profile.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

28
src/App.vue Normal file
View File

@ -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-sm-block w-100" style="height: 40px"></div>
</template>

View File

@ -1,36 +0,0 @@
import {HttpClientModule} from '@angular/common/http';
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {ContactFormComponent} from './components/contact-form/contact-form.component';
import {ProjectsComponent} from './components/projects/projects.component';
import {HomeComponent} from './views/home/home.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MaterialModule} from './material.module';
import {FormsModule} from '@angular/forms';
import {TypewriterComponent} from './components/typewriter/typewriter.component';
import {SlideShowComponent} from './components/slideShow/slideShow.component';
import {AppComponent} from './views/app/app.component';
import {AppRouting} from './app.routing';
import {ConsoleComponent} from './components/console/console.component';
@NgModule({
declarations: [
AppComponent,
ConsoleComponent,
ContactFormComponent,
HomeComponent,
ProjectsComponent,
SlideShowComponent,
TypewriterComponent
],
imports: [
AppRouting,
BrowserModule,
BrowserAnimationsModule,
FormsModule,
HttpClientModule,
MaterialModule,
],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -1,14 +0,0 @@
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';
import {HomeComponent} from './views/home/home.component';
@NgModule({
imports: [
RouterModule.forRoot([
{path: '', component: HomeComponent},
{path: '**', redirectTo: ''}
])
],
exports: [RouterModule]
})
export class AppRouting {}

View File

@ -1,10 +0,0 @@
<div class="p-3 d-flex flex-column justify-content-end console overflow-hidden" [style.height]="height">
<div>
<h4 class="m-0" *ngFor="let o of output">{{o}}</h4>
</div>
<div class="mt-2">
<h4 class="m-0">
{{prompt}} <typewriter class="pl-2" [text]="input" (done)="done()"></typewriter>
</h4>
</div>
</div>

View File

@ -1,5 +0,0 @@
.console {
background-color: #333;
color: #00ff00;
font-family: monospace !important;
}

View File

@ -1,35 +0,0 @@
import {Component, Input, ViewChild} from '@angular/core';
import {sleep} from '../../misc/utils';
import {TypewriterComponent} from '../typewriter/typewriter.component';
@Component({
selector: 'console',
templateUrl: './console.component.html',
styleUrls: ['./console.component.scss']
})
export class ConsoleComponent {
done = () => {};
input = '';
output: string[] = [];
prompt = '>'
@Input() height: string = 'auto';
@ViewChild(TypewriterComponent) typewriter!: TypewriterComponent;
clear() { this.output = []; }
exec(input: string, output: () => any, pause = 1000) {
return new Promise<void>(res => {
this.done = async () => {
await sleep(pause);
this.input = '';
this.output.push(`${this.prompt} ${input}`);
const out = output();
if(typeof out == 'string') this.output.push(out);
res();
};
this.input = input;
});
}
}

View File

@ -1,19 +0,0 @@
<form>
<div *ngIf="success" class="alert alert-success">Email sent!</div>
<div *ngIf="error" class="alert alert-danger">Email failed to send</div>
<div class="form-group">
<label for="emailInput">Email</label>
<input type="email" class="form-control" id="emailInput" name="email" placeholder="username@example.com" [(ngModel)]="email">
</div>
<div class="form-group">
<label for="subjectInput">Subject</label>
<input type="text" class="form-control" id="subjectInput" name="subject" placeholder="Interested in Services" [(ngModel)]="subject">
</div>
<div class="form-group">
<label for="messageInput">Message</label>
<textarea class="form-control" id="messageInput" name="message" rows="5" [(ngModel)]="message"></textarea>
</div>
<button type="button" class="btn btn-primary float-right" (click)="send()" [disabled]="loading || !email || !subject || !message">
Send
</button>
</form>

View File

@ -1,44 +0,0 @@
import {ChangeDetectorRef, Component} from '@angular/core';
import {EmailService} from '../../services/email.service';
@Component({
selector: 'contact-form',
templateUrl: './contact-form.component.html'
})
export class ContactFormComponent {
email = '';
error = false;
loading = false;
message = '';
subject = '';
success = false;
constructor(private changeRef: ChangeDetectorRef, private emailService: EmailService) { }
async send() {
this.error = false;
this.success = false;
if(this.loading || !this.email || !this.subject || !this.message) return;
this.loading = true;
this.emailService.send(`ZaksCode: ${this.subject}`, `From: ${this.email}\n\n${this.message}`
).then(() => {
this.email = '';
this.message = '';
this.success = true;
this.subject = '';
}).catch(err => {
// Postmail seems to always return an error message
if(200 <= err.status && err.status < 300) {
this.email = '';
this.message = '';
this.success = true;
this.subject = '';
} else {
this.error = true;
}
}).finally(() => {
this.loading = false;
this.changeRef.detectChanges();
});
}
}

View File

@ -1,11 +0,0 @@
<mat-list dense>
<ng-container *ngFor="let p of projectsService.all; let first = first">
<mat-divider *ngIf="!first"></mat-divider>
<mat-list-item>
<img *ngIf="p.avatar_url" mat-list-avatar [src]="p.avatar_url" alt="Project icon">
<img *ngIf="!p.avatar_url" mat-list-avatar src="/assets/img/git.png" alt="Project icon">
<a mat-line [href]="p.web_url" target="_blank">{{p.name}}</a>
<div mat-line>{{p.description}}</div>
</mat-list-item>
</ng-container>
</mat-list>

View File

@ -1,10 +0,0 @@
import {Component} from '@angular/core';
import {ProjectsService} from '../../services/projects.service';
@Component({
selector: 'projects',
templateUrl: './projects.component.html'
})
export class ProjectsComponent {
constructor(public projectsService: ProjectsService) { }
}

View File

@ -1,22 +0,0 @@
<div id="carouselIndicators" class="carousel slide h-100" data-ride="carousel">
<ol class="carousel-indicators">
<li *ngFor="let ignore of slides; let i = index" data-target="#carouselIndicators" [attr.data-slide-to]="i"></li>
</ol>
<div class="carousel-inner">
<div *ngFor="let slide of slides" class="carousel-item">
<img class="d-block w-100" [src]="slide.image" alt="project.name">
<div class="carousel-caption d-none d-md-block">
<h5>{{slide.title}}</h5>
<p>{{slide.description}}</p>
</div>
</div>
</div>
<a class="carousel-control-prev" href="#carouselIndicators" role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#carouselIndicators" role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>

View File

@ -1,15 +0,0 @@
import {Component, Input} from '@angular/core';
export type Slide = {
title: string;
description: string;
image: string;
}
@Component({
selector: 'slideshow',
templateUrl: 'slideShow.component.html'
})
export class SlideShowComponent {
@Input() slides: Slide[] = [];
}

View File

@ -1,18 +0,0 @@
.typewriter {
& {
overflow: hidden;
margin: 0 auto;
letter-spacing: .15em;
animation: blink-caret 1s step-end infinite;
}
@keyframes typing {
from { width: 0 }
to { width: 100% }
}
@keyframes blink-caret {
from, to { border-right: .15em solid; }
50% { border: none; }
}
}

View File

@ -1,30 +0,0 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {Observable, of, timer} from 'rxjs';
import {filter, map, tap} from 'rxjs/operators';
@Component({
selector: 'typewriter',
template: `<span class="typewriter">{{output | async}}</span>`,
styleUrls: ['typewriter.component.scss']
})
export class TypewriterComponent {
output?: Observable<string>;
@Input() delay = 1500;
@Input() speed = 100;
@Input() set text(text: string) {
if(!text) {
this.output = of('');
} else {
this.output = timer(this.delay, this.speed).pipe(
filter(n => n <= (text.length || 0)),
tap(n => {
if(n == text.length) this.done.emit();
}),
map(n => text.slice(0, n))
);
}
}
@Output() done = new EventEmitter<void>();
}

View File

@ -1,22 +0,0 @@
import {NgModule} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {MatCardModule} from '@angular/material/card';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatListModule} from '@angular/material/list';
const MODULES = [
MatButtonModule,
MatCardModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatListModule,
];
@NgModule({
imports: [MODULES],
exports: [MODULES]
})
export class MaterialModule {}

View File

@ -1,30 +0,0 @@
/**
* Convert data into a form encoded format.
*
* @param {any} data - data to convert
* @returns {string} - Ecodeded form data
*/
export function formEncode(data: any): string {
return Object.entries(data).map(([key, value]) =>
encodeURIComponent(key) + '=' + encodeURIComponent(<any>value)
).join('&');
}
/**
* Use with await to pause the script for a specified amount of time (in miliseconds).
*
* **Example:**
* ```
* async () => {
* ...
* await sleep(1000) // Wait 1 second
* ...
* }
* ```
*
* @param {number} ms - Time to pause in miliseconds
* @returns {Promise<unknown>} - Promise you should await
*/
export function sleep(ms: number) {
return new Promise(res => setTimeout(res, ms));
}

View File

@ -1,25 +0,0 @@
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
import {formEncode} from '../misc/utils';
@Injectable({providedIn: 'root'})
export class EmailService {
constructor(private http: HttpClient) { }
/**
* Send an email to website admin.
*
* @param {string} subject - Email subject line
* @param {string} message - Email body
* @returns {Promise<Object | undefined>} - Response from Postmail API
*/
send(subject: string, message: string) {
return this.http.post('https://postmail.invotes.com/send', formEncode({
access_token: 's7uhce84sx6fayy5xlq0nrtx',
subject: subject,
text: message
}), {
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
}).toPromise();
}
}

View File

@ -1,47 +0,0 @@
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
export type Project = {
avatar_url: string;
created_at: string;
default_branch: string;
description: string;
forks_count: number;
http_url_to_repo: string;
id: number;
last_activity_at: string;
name: string;
name_with_namespace: string;
namespace: {
full_path: string;
id: number;
kind: string;
name: string;
parent_id: number;
path: string;
web_url: string;
};
path: string;
path_with_namespace: string;
readme_url: string;
ssh_url_to_repo: string;
star_count: number;
tag_list: string[];
topics: string[];
web_url: string;
}
@Injectable({providedIn: 'root'})
export class ProjectsService {
all: Project[] = [];
constructor(private http: HttpClient) {
this.http.get<Project[]>('https://gitlab.zakscode.com/api/v4/projects').toPromise().then(projects => {
this.all = (projects || []).sort((a, b) => {
if(a.name > b.name) return 1;
if(a.name < b.name) return -1;
return 0;
});
});
}
}

View File

@ -1,27 +0,0 @@
import {Injectable} from '@angular/core';
@Injectable({providedIn: 'root'})
export class QuoteService {
readonly quotes = [
'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!"',
'Anxiety is like when video game combat music is playing but you can\'t find the enemy',
'Why do kamikaze pilots wear helmets?',
'The cake is a lie!',
'How are unicorns fake but giraffes real?',
'The number of people older than you never goes up',
'When you brush your teeth you are cleaning your skeleton',
'Pregenancy 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'
];
/**
* Return random quote
*
* @returns {string} - Quote
*/
random() {
return this.quotes[Math.round(Math.random() * (this.quotes.length - 1))];
}
}

View File

@ -1 +0,0 @@
<router-outlet></router-outlet>

View File

@ -1,17 +0,0 @@
import {Component} from '@angular/core';
import {Title} from '@angular/platform-browser';
import {ActivatedRoute} from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html'
})
export class AppComponent {
constructor(private route: ActivatedRoute, private title: Title) {
this.route.url.subscribe(() => this.setTitle(''));
}
setTitle(title: string) {
this.title.setTitle(`Zak's Code${title ? ` - ${title}` : ''}`);
}
}

View File

@ -1,80 +0,0 @@
<div class="w-100">
<div class="w-100" style="height: calc(100vh - 140px)"></div>
<div class="container p-0 bg-white">
<div class="px-3" style="background-color: #732222">
<mat-card style="max-width: 600px; transform: translateY(-33%)">
<mat-card-content class="d-flex p-3">
<div class="d-none d-md-block pr-3">
<img src="assets/img/portrait.jpg" width="150px" height="150px" style="border-radius: 50%" alt="Zakary Timson">
</div>
<div>
<h1 class="mb-0">Zakary Timson</h1>
<h5 class="text-muted">DEVOPS & SOFTWARE ENGINEER</h5>
<div class="mt-3">
<div><i class="mr-2 fa fa-map-marker-alt"></i> London Ontario, Canada</div>
<div><i class="mr-2 fa fa-envelope"></i> <a href="mailto:zaktimson@gmail.com">zaktimson@gmail.com</a></div>
<div><i class="mr-2 fa-brands fa-gitlab"></i> <a href="https://gitlab.zakscode.com/explore/projects" target="_blank">gitlab.zakscode.com</a></div>
<div><i class="mr-2 fa-brands fa-github"></i> <a href="https://github.com/ztimson" target="_blank">github.com/ztimson</a></div>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="p-4">
<console height="12rem"></console>
</div>
<div class="p-4">
<h3>About Me</h3>
<div>
<img alt="Childhood" class="float-right m-3 m-md-0 ml-md-3" src="assets/img/keyboard-in-hand.jpg" height="150px" width="auto" style="border-radius: 50%">
<p>
Zak was born with a keyboard in hand and was learning his first programming language by thirteen. Nearly
entirely self taught, Zak challenged his programming courses through both high-school and college while
working in the industry to gain professional experience. He is very passionate about technology and as a
lifelong learner it has opened the door to many other hobbies like robotics, space and physics. Some of his
personal projects include a full sized arcade machine, home automation and a power wall for a home solar
system.
</p>
</div>
</div>
<div class="p-4 overflow-hidden">
<h3>Resume & References</h3>
<div class="d-md-none">
<ul class="list-group">
<a class="list-group-item list-group-item-action border-primary text-primary" href="https://docs.google.com/document/d/1xP6HASPerXKMJM_x6-PhHVvoYgq-Hym5IRO7g47EX8o/edit?usp=sharing" target="_blank">Resume</a>
</ul>
<ul class="list-group mt-3">
<a class="list-group-item list-group-item-action border-info text-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyNWw0UDFzT0ZTeVU/view?usp=sharing" target="_blank">Manager</a>
<a class="list-group-item list-group-item-action border-info text-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyaFBhcXBEaGp6YWc/view?usp=sharing" target="_blank">Contractor</a>
<a class="list-group-item list-group-item-action border-info text-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyM0YtTWcxQzk0dEE/view?usp=sharing" target="_blank">Teacher</a>
<a class="list-group-item list-group-item-action border-info text-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyX2owd0xURjh3RlE/view?usp=sharing" target="_blank">Principle</a>
<a class="list-group-item list-group-item-action border-info text-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyMHdaM1BjZ1MwbWxva2lOY290NElwanN4b2JV/view?usp=sharing" target="_blank">CD Projekt Red</a>
</ul>
</div>
<div class="d-none d-md-inline-block pt-1">
<a class="btn btn-outline-primary" href="https://docs.google.com/document/d/1xP6HASPerXKMJM_x6-PhHVvoYgq-Hym5IRO7g47EX8o/edit?usp=sharing" target="_blank">Resume</a>
<div class="ml-3 btn-group" role="group" aria-label="Basic example">
<a class="btn btn-outline-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyNWw0UDFzT0ZTeVU/view?usp=sharing" target="_blank">Manager</a>
<a class="btn btn-outline-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyaFBhcXBEaGp6YWc/view?usp=sharing" target="_blank">Contractor</a>
<a class="btn btn-outline-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyM0YtTWcxQzk0dEE/view?usp=sharing" target="_blank">Teacher</a>
<a class="btn btn-outline-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyX2owd0xURjh3RlE/view?usp=sharing" target="_blank">Principle</a>
<a class="btn btn-outline-info" href="https://drive.google.com/file/d/0B_iz0vkzXmAyMHdaM1BjZ1MwbWxva2lOY290NElwanN4b2JV/view?usp=sharing" target="_blank">CD Projekt Red</a>
</div>
</div>
</div>
<div class="p-4">
<h3>Projects & Repositories</h3>
<projects></projects>
</div>
<div class="p-4 overflow-hidden">
<h3>Contact</h3>
<contact-form></contact-form>
</div>
<footer class="p-1 bg-dark text-center" style="color: grey">
Copyright © ZaksCode 2022 | All Rights Reserved
<br>
Created by <a href="https://zakscode.com">Zak Timson</a>
</footer>
<div class="d-none d-sm-block skirt"></div>
</div>
</div>

View File

@ -1,25 +0,0 @@
import {AfterViewInit, Component, ViewChild} from '@angular/core';
import {ConsoleComponent} from '../../components/console/console.component';
import {QuoteService} from '../../services/quote.service';
import {sleep} from '../../misc/utils';
@Component({
selector: 'home',
templateUrl: './home.component.html'
})
export class HomeComponent implements AfterViewInit {
@ViewChild(ConsoleComponent) console!: ConsoleComponent;
constructor(private quotes: QuoteService) { }
ngAfterViewInit() { this.animateConsole(); }
animateConsole() {
setTimeout(async () => {
await this.console.exec('bash ./random-thought.sh', () => this.quotes.random());
await sleep(10000);
await this.console.exec('clear', async () => this.console.clear());
this.animateConsole();
}, 1000);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

23
src/components/card.vue Normal file
View File

@ -0,0 +1,23 @@
<script setup lang="ts">
import Icon from '@/components/icon.vue';
defineProps({
color: {type: String, required: true},
icon: {type: String, required: true},
title: {type: String, required: true},
text: {type: String, required: true},
offset: {type: String, default: '0px'}
});
</script>
<template>
<div class="d-flex flex-column align-items-center border mb-3 p-3" style="width: 240px">
<div class="m-3 p-3 rounded-circle text-white d-flex align-items-middle justify-content-center" :style="'height: 50px; width: 50px; background:' + color">
<icon :name="icon" :style="'margin-top:' + offset"/>
</div>
<h4>{{title}}</h4>
<p class="text-center">
{{text}}
</p>
</div>
</template>

117
src/components/contact.vue Normal file
View File

@ -0,0 +1,117 @@
<script setup lang="ts">
import Icon from '@/components/icon.vue';
import {environment} from '@/environments/environment';
import {ref} from 'vue';
const disable = ref(false);
const done = ref(false);
const data = ref({
name: '',
email: '',
subject: '',
message: '',
});
const errors = ref<any>({
banner: false,
name: false,
email: false,
subject: false,
message: false,
});
function reset() {
disable.value = false;
done.value = false;
data.value = {name: '', email: '', subject: '', message: ''};
errors.value = {banner: false, name: false, email: false, subject: false, message: false};
}
function validateEmail(email: string) {
return /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(email);
}
function send(name: string, email: string, subject: string, message: string) {
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() {
disable.value = true;
const d = data.value;
errors.value = {
banner: false,
name: !d.name,
email: !d.email || !validateEmail(d.email),
subject: !d.subject,
message: !d.message
};
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(() => {
done.value = true;
}).catch(err => {
console.error(err);
errors.value.banner = err?.message || err.toString();
disable.value = false;
});
}
</script>
<template>
<!-- Success Banner -->
<div v-if="done" class="alert alert-success">
<span>Success! We will be intouch shortly.</span>
</div>
<!-- Error Banner -->
<div v-if="errors?.banner" class="alert alert-danger">
Error: {{ (errors as any).banner }}
</div>
<!-- Contact Form -->
<form>
<div class="d-flex flex-column flex-md-row flex-wrap justify-content-between">
<!-- Name -->
<div class="mb-2 p-0 pe-md-1 p-md-0" style="flex: 1 0 0;">
<div class="input-group">
<span class="input-group-text"><icon name="user"/></span>
<input class="form-control" type="text" placeholder="Name" v-model="data.name" v-bind:class="{'is-invalid': errors?.name}" :disabled="disable" required>
</div>
</div>
<!-- Email -->
<div class="mb-2 p-0 ps-md-1" style="flex: 1 0 0;">
<div class="input-group">
<span class="input-group-text"><icon name="envelope"/></span>
<input class="form-control" type="email" placeholder="Email" v-model="data.email" v-bind:class="{'is-invalid': errors?.email}" :disabled="disable" required>
</div>
</div>
</div>
<!-- Subject -->
<div class="input-group mb-2">
<span class="input-group-text"><icon name="book"/></span>
<input class="form-control" type="text" placeholder="Subject" v-model="data.subject" v-bind:class="{'is-invalid': errors?.subject}" :disabled="disable" required>
</div>
<!-- Message Body -->
<textarea class="form-control" placeholder="Message" rows="5" v-model="data.message" v-bind:class="{'is-invalid': errors?.message}" :disabled="disable" required></textarea>
<!-- Buttons -->
<div class="text-end pt-3">
<button type="button" class="btn rounded-pill ms-3" @click="reset()">Reset</button>
<button type="button" class="btn btn-primary rounded-pill ms-3" @click="submit" :disabled="disable">Send Message</button>
</div>
</form>
</template>

21
src/components/foot.vue Normal file
View File

@ -0,0 +1,21 @@
<script setup lang="ts">
</script>
<style scoped>
footer {
background: #343a40;
color: #A3A3A3;
a {
color: #75B8FF;
}
}
</style>
<template>
<footer class="p-2 text-center">
Copyright © ZaksCode 2024 | All Rights Reserved
<br>
Created by <a href="https://zakscode.com" target="_blank">Zak Timson</a>
</footer>
</template>

10
src/components/icon.vue Normal file
View File

@ -0,0 +1,10 @@
<script setup lang="ts">
const props = defineProps({
class: {type: String, default: ''},
name: {type: String, required: true},
});
</script>
<template>
<i :class="props.class + ' fa fa-' + props.name"></i>
</template>

266
src/components/konsole.vue Normal file
View File

@ -0,0 +1,266 @@
<script setup>
import {onMounted} from 'vue';
const hostname = 'virtual';
let history = [];
let historyIndex = 0;
let prompt;
let input;
let output;
function focus() {
input.focus();
}
function disable() {
input.disabled = true;
prompt.style.visibility = 'hidden';
}
function enable() {
input.disabled = false;
input.focus();
prompt.style.visibility = 'visible';
}
function banner() {
stdOut(`Konsole 0.2.0 LTS virtual tty1<br><br>${hostname} login: root<br>password:<br><br>`);
}
function process(command) {
(Array.isArray(command) ? command.join(' ') : command).split(';').filter(c => !!c).forEach(c => {
const parts = c.split(' ').filter(c => !!c);
if(window.cli[parts[0]] == undefined || window.cli[parts[0]].run == undefined) {
stdErr(`${parts[0]}: command not found`);
} else {
try {
const out = window.cli[parts[0]].run(parts.slice(1));
if(!!out) stdOut(out);
} catch(err) {
console.error(err)
stdErr(`${parts[0]}: exited with a non-zero status`);
}
}
});
}
function stdErr(text) {
const p = document.createElement('p');
p.classList.add('console-output-line');
p.classList.add('console-output-error');
p.innerText = text;
output.appendChild(p);
}
function stdOut(text, html=true) {
const p = document.createElement('p');
p.classList.add('console-output-line');
p[html ? 'innerHTML' : 'innerText'] = text;
output.appendChild(p);
}
function stdIn(event) {
if(event.key == "Enter") {
disable();
let inputValue = input.value;
input.value = '';
stdOut(`root@localhost:~ # ${inputValue}`, false);
if(!!inputValue) {
history.push(inputValue);
historyIndex = history.length;
process(inputValue)
}
enable();
} else if(event.key == 'Up' || event.key == 'ArrowUp') {
if(historyIndex > 0) historyIndex--;
input.value = historyIndex == history.length ? '' : history[historyIndex];
setTimeout(() => {
const end = input.value.length;
input.setSelectionRange(end, end);
input.focus();
}, 1)
} else if(event.key == 'Down' || event.key == 'ArrowDown') {
if(historyIndex < history.length) historyIndex++;
input.value = historyIndex == history.length ? '' : history[historyIndex];
setTimeout(() => {
const end = input.value.length;
input.setSelectionRange(end, end);
input.focus();
}, 1)
}
}
onMounted(() => {
prompt = document.getElementsByClassName('console-input-prompt')[0];
input = document.getElementsByClassName('console-input-field')[0];
output = document.getElementsByClassName('console-output')[0];
banner();
});
window.cli = {};
window.cli['clear'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Clear console output';
},
run: args => {
output.innerHTML = '';
}
}
window.cli['echo'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Output text to console';
},
run: args => {
return args.join(' ');
}
}
window.cli['exit'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'End session';
},
run: args => {
process('clear');
history = [];
historyIndex = 0;
banner();
}
}
window.cli['help'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Display all commands';
},
run: args => {
return Object.keys(window.cli).map(command => `${command} - ${window.cli[command].help()}`).join('<br>') + '<br><br>';
}
}
window.cli['hostname'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get computer hostname';
},
run: args => {
return 'localhost'
}
}
window.cli['man'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Command manual';
},
run: args => {
return window.cli[args[0]].help();
}
}
window.cli['whoami'] = {
autocomplete: () => {
return [];
},
help: () => {
return 'Get username';
},
run: args => {
return 'root'
}
}
</script>
<style>
.console {
display: flex;
flex-direction: column;
padding: 1rem;
background: #333;
font-family: monospace !important;
overflow-y: auto;
.console-output {
flex-grow: 1;
color: #0f0;
}
.console-output-line {
margin: 0;
padding: 0;
min-height: 1.25rem;
}
.console-input {
display: flex;
margin: 0;
.console-input-prompt {
padding-right: 0.55em;
text-wrap: nowrap;
color: #0f0;
}
.console-input-field {
border: none;
outline: none;
font-size: 1rem;
background-color: rgba(0, 0, 0, 0);
color: #0f0;
flex-grow: 1;
padding: 0;
animation: blink-empty 1s infinite linear;
background-image: linear-gradient(#0f0, #0f0);
background-position: 1px center;
background-repeat: no-repeat;
background-size: 1px 1.1em;
&:focus {
background-image: none;
}
}
}
}
.hidden-label {
border: 0;
padding: 0;
margin: 0;
position: absolute !important;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 - a 0 height clip, off to the bottom right of the visible 1px box */
clip: rect(1px, 1px, 1px, 1px); /*maybe deprecated but we need to support legacy browsers */
clip-path: inset(50%); /*modern browsers, clip-path works inwards from each corner*/
white-space: nowrap; /* added line to stop words getting smushed together (as they go onto seperate lines and some screen readers do not understand line feeds as a space */
}
@keyframes blink-empty {
0% {background-size: 1px 1.1em;}
50% {background-size: 1px 1.1em;}
51% {background-size: 0 1.1em;}
100% {background-size: 0 1.1em;}
}
</style>
<template>
<div class="console">
<div class="console-output"></div>
<div class="console-input" @click=" focus()">
<div class="console-input-prompt">root@{{hostname}}:~ #</div>
<label for="console-input-field" class="hidden-label"><!-- Accessibility -->CLI Input</label>
<input id="console-input-field" class="console-input-field" type="text" @keydown="stdIn($event)">
</div>
</div>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import Icon from '@/components/icon.vue';
</script>
<style scoped>
.card {
border-radius: 4px;
box-shadow: 0 2px 1px -1px #0003;
}
</style>
<template>
<div class="card d-inline-flex flex-column flex-sm-row text-center text-sm-start bg-white p-4">
<div class="mb-4 mb-sm-4 me-sm-4">
<img src="/profile.jpg" width="150px" height="150px" alt="Zakary Timson" class="rounded-circle">
</div>
<div class="me-sm-5">
<h1 class="m-0" style="font-size: 2.5rem">Zakary Timson</h1>
<h2 class="mt-0 text-muted" style="font-size: 1.25rem">
DEVOPS & SOFTWARE ENGINEER
</h2>
<ul class="m-0 p-0 text-start" style="list-style: none">
<li><icon name="map-marker-alt" class="me-1"/> Toronto Ontario, Canada</li>
<li><icon name="envelope" class="me-1"/> <a href="mailto:zaktimson@gmail.com" target="_blank">zaktimson@gmail.com</a></li>
<li><icon name="github" class="fa-brands me-1"/> <a href="https://github.com/ztimson" target="_blank">github.com/ztimson</a></li>
<li><icon name="git-alt" class="fa-brands me-1"/> <a href="https://git.zakscode.com" target="_blank">git.zakscode.com</a></li>
</ul>
</div>
</div>
</template>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
export interface Project {
icon?: string;
name: string;
description: string;
link: string;
source?: string;
}
defineProps({
projects: {type: Array as () => Project[], required: true}
});
</script>
<template>
<div>
<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="me-2">
<img :src="p.icon || '/git.png'" alt="Logo" style="height: 40px; width: 40px;">
</div>
<div class="d-flex flex-column">
<div>
<a :href="p.link" target="_blank" class="me-2">{{p.name}}</a>
<a v-if="p.source" :href="p.source" target="_blank" class="text-small" style="font-size: 0.75em">[source]</a>
</div>
<div class="text-muted">{{p.description}}</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
const resume = 'https://drive.google.com/file/d/1N1L2SYvqY49OJAR97cjjHANj0ModmZSy/view?usp=drive_link';
const refrences = [
// ['Andre Mourinho', ''],
['CD Projekt Red', 'https://files.zakscode.com/share/N61Db3y0'],
['Chris Cartwright', 'https://files.zakscode.com/share/luyY49_N'],
['Garry Whyte', 'https://files.zakscode.com/share/zHjnHReT'],
['Linda Nicodemo', 'https://files.zakscode.com/share/1wKpkQzW'],
['Ray Power', 'https://files.zakscode.com/share/bTR2ab_P'],
]
</script>
<template>
<div class="d-block d-md-none">
<div class="mb-3">
<a class="btn btn-outline-danger w-100" :href="resume" target="_blank">CSV / Resume</a>
</div>
<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>
</div>
</div>
<div class="d-none d-md-block">
<a class="btn btn-outline-danger me-3" :href="resume" target="_blank">CSV / Resume</a>
<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>
</div>
</div>
</template>

View File

@ -1,3 +0,0 @@
export const environment = {
production: true
};

View File

@ -1,16 +1,3 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/plugins/zone-error'; // Included with Angular CLI.
postMailKey: (<any>window)?.env?.APP_POSTMAIL_KEY || import.meta.env.APP_POSTMAIL_ACCESS_TOKEN,
}

View File

@ -1,28 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Zaks Code</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name=”robots” content=”index,nofollow” />
<meta property=”og:type” content=”website” />
<meta property=”og:title” content=”Zakary Timson />
<meta property=”og:description” content=”Cloud Architecture & Software Engineering />
<meta property=”og:image” content=”https://zakscode.com/assets/img/ZaksCode.png” />
<meta property=”og:url” content=”https://zakscode.com” />
<meta property=”og:site_name” content=”ZaksCode” />
<meta name=”twitter:title” content=”Zakary Timson />
<meta name=”twitter:description” content=”Cloud Architecture & Software Engineering />
<meta name=”twitter:image” content=”https://zakscode.com/assets/img/ZaksCode.png” />
<link rel="icon" type="image/png" href="assets/img/logo.png">
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<app-root></app-root>
</body>
</html>

34
src/main.scss Normal file
View File

@ -0,0 +1,34 @@
@import url("https://use.fontawesome.com/releases/v6.1.1/css/all.css");
@import url("https://fonts.googleapis.com/css2?family=Roboto");
@import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
::-webkit-scrollbar { width: 10px; }
::-webkit-scrollbar-track { background: #333; }
::-webkit-scrollbar-thumb { background: #555; border-radius: 5px; }
::-webkit-scrollbar-thumb:hover { background: #aaa; }
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-family: Roboto,sans-serif;
background: #354B72 url('/cloud.gif') no-repeat fixed right 50% top -10vh;
}
a:not(.btn), a:not(.btn):visited {
color: #006FE6;
text-decoration: none;
&:hover {
color: #0062ce;
text-decoration: underline;
}
}
.cap-width {
width: min(100%, 1100px);
margin-left: auto;
margin-right: auto;
}

View File

@ -1,12 +1,9 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import './main.scss'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
const app = createApp(App)
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));
app.use(router)
app.mount('#app')

View File

@ -1,53 +0,0 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes recent versions of Safari, Chrome (including
* Opera), Edge on the desktop, and iOS and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

11
src/router/index.ts Normal file
View File

@ -0,0 +1,11 @@
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{path: '/', name: 'home', component: Home}
]
})
export default router

View File

@ -1,53 +0,0 @@
@use '@angular/material' as mat;
@import url("https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css");
@import url("https://use.fontawesome.com/releases/v6.1.1/css/all.css");
@import url("https://fonts.googleapis.com/icon?family=Material+Icons|Roboto:300,400,500");
@include mat.core();
$zakscode-primary: mat.define-palette(mat.$blue-palette, 900);
$zakscode-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
$zakscode-warn: mat.define-palette(mat.$red-palette);
$zakscode-theme: mat.define-light-theme((
color: (
primary: $zakscode-primary,
accent: $zakscode-accent,
warn: $zakscode-warn,
)
));
@include mat.all-component-themes($zakscode-theme);
::-webkit-scrollbar-track {
background-color: #354B72;
}
::-webkit-scrollbar {
width: 10px;
background-color: #354B72;
}
::-webkit-scrollbar-thumb {
border-radius: 5px;
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.5);
background-color: #354B72;
}
html, body {
height: 100%;
}
body {
margin: 0;
font-family: Roboto, sans-serif;
background: #354B72 url("/assets/img/ZaksCode.gif") no-repeat fixed center;
background-position: right 50% top -10vh;
}
.skirt {
height: 3rem;
width: 100%;
background-color: #354B72;
}

View File

@ -1,26 +0,0 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
<T>(id: string): T;
keys(): string[];
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting(),
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

103
src/views/Home.vue Normal file
View File

@ -0,0 +1,103 @@
<script setup lang="ts">
import Card from '@/components/card.vue';
import Contact from '@/components/contact.vue';
import Konsole from '@/components/konsole.vue';
import Projects from '@/components/projects.vue';
import Refrences from '@/components/refrences.vue';
import {ref} from 'vue';
const services: Projects[] = [
{name: 'Formula Manager', icon: 'https://git.zakscode.com/avatars/7ec6bfd66b2bf9bad5c43c75a33f9cb3f6609b05c33a31f5d1e524a567cd09c1?size=280', link: 'https://screenprintingsuppliescanada.com/formulation-manager', description: 'A web & computer application used by FH&Sons to record chemical formulas & distribute them to clients'},
{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'},
{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'},
];
const openSource: Projects[] = [
{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)'},
{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'},
{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 '},
{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
let remainder = ref(0);
fetch('https://git.zakscode.com/api/v1/repos/search', {
method: 'get',
headers: {"Content-Type": "application/json"}
}).then(async repos => {
const data = (await repos.json())?.data;
remainder.value = data.length - openSource.length;
});
</script>
<template>
<div class="p-3">
<!-- Terminal -->
<konsole class="mb-5" style="max-height: 300px" />
<!-- Steps -->
<div class="mb-5 pt-5">
<h3 class="mb-0 text-center">Plan for Success</h3>
<hr class="mb-4">
<div class="text-center my-5">
<img src="/cycle.svg" alt="Development Cycle" style="width: 100%; max-width: 600px; height: auto;">
</div>
<div class="d-flex flex-wrap justify-content-around">
<card color="#6aa84f" icon="clipboard" offset="1px" title="Plan" text="Working with the client we will identify the goals of the project. This includes things like the target audience, use case, features, style, and delivery."/>
<card color="#6d9eeb" icon="code" offset="2px" title="Code" text="Goals are broken down into tasks and prioritized in our ticketing system. Using CI/CD, tasks are automatically deployed for testing as they are completed."/>
<card color="#e69138" icon="message" offset="3px" title="Feedback" text="Clients are notified with the release notes and can test at their convince. Any critiques can be communicated directly to us or through our ticketing system."/>
<card color="#674ea7" icon="play" offset="2px" title="Release" text="Once all goals are complete we will work with you to deploy the product to any location. Once setup, future updates are automatically deployed to our clients."/>
</div>
</div>
<!-- About Section -->
<div class="mb-5">
<h3 class="mb-0">About</h3>
<hr class="mb-4">
<img alt="Childhood" src="/childhood.jpg" height="150px" width="auto" class="float-end m-3 m-md-0 ml-md-3" style="border-radius: 50%;">
<p>
Zak Timson is a software engineer with over 10 years of professional experience. Zak has had a love for
computers since he was born & taught him self to code at the age of 13. Since then, he has gone to school
for computer science & has worked for both small businesses and large corporations as a developer and team lead.
</p>
<p>
Zak specializes in full-stack web development & server infrastructure, and primarily works on large enterprise
grade "Software as a service" (SaaS) products. As a software architect & team lead he is able to work with
business's to create a road map of their needs, build enterprise grade software solutions that meet those
needs & work with clients to host & deliver automatic updates at scale using continuous integration.
</p>
<div class="mt-4">
<h4 class="mb-3 text-muted">CSV & References</h4>
<refrences />
</div>
</div>
<!-- Projects List -->
<div class="mb-5">
<h3 class="m-0">Projects</h3>
<hr class="mb-4">
<div class="mb-4">
<h4 class="mb-3 text-muted">Products & Services</h4>
<projects :projects="services"/>
</div>
<div>
<h4 class="mb-3 text-muted">Open Source</h4>
<projects :projects="openSource"/>
</div>
<a v-if="remainder" class="float-end m-2" href="https://git.zakscode.com/explore" target="_blank">See {{remainder}} More...</a>
</div>
<!-- Contact Form -->
<div>
<h3 class="m-0">Contact</h3>
<hr class="mb-4">
<div>
<contact />
</div>
</div>
</div>
</template>

View File

@ -1,16 +1,22 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts",
"src/app/misc/utils.ts"
],
"include": [
"src/**/*.d.ts"
]
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue"
],
"exclude": [
"src/**/__tests__/*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"moduleResolution": "Node",
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}

View File

@ -1,32 +1,11 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2017",
"module": "es2020",
"lib": [
"es2020",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

18
tsconfig.node.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"moduleResolution": "Bundler",
"types": [
"node"
]
}
}

View File

@ -1,18 +0,0 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

16
vite.config.ts Normal file
View File

@ -0,0 +1,16 @@
import {defineConfig} from 'vite';
import {fileURLToPath, URL} from 'node:url';
import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
envPrefix: 'APP',
});