Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
14c3460c11 | |||
ab16219f3b | |||
7ffc4f518b | |||
1f00bfa07b | |||
93050928d8 | |||
4668473293 | |||
0ad815d516 | |||
abf4575151 | |||
383148a3ac | |||
9241171e1b | |||
294956f8e7 | |||
cc848bf06f | |||
8fa85ed6ac | |||
8c7cec7e7c | |||
664fe17610 | |||
b76100ad06 | |||
db794ff582 | |||
002e1aa234 | |||
922f2b4e0e | |||
9ee8e25090 | |||
78e980df41 | |||
a1dfdfb8c5 | |||
94f114bb5c |
28
.github/workflows/build.yml
vendored
Normal file
28
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
name: Build Site
|
||||
run-name: ${{ github.actor }} is building Personal Site
|
||||
on: [push]
|
||||
jobs:
|
||||
Build-Site:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
steps:
|
||||
- run: echo "Starting build job"
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- name: copy file via ssh password
|
||||
uses: appleboy/scp-action@v0.1.4
|
||||
with:
|
||||
host: ${{ secrets.HOST }}
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.PASSWORD }}
|
||||
port: ${{ secrets.PORT }}
|
||||
source: "dist/*"
|
||||
target: /home/thomas/testwww
|
||||
strip_components: 1
|
32
.gitignore
vendored
32
.gitignore
vendored
@ -1,24 +1,22 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
# build output
|
||||
dist/
|
||||
.output/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
package-lock.json
|
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@ -1,3 +1,4 @@
|
||||
{
|
||||
"recommendations": ["svelte.svelte-vscode"]
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
||||
|
11
.vscode/launch.json
vendored
Normal file
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Thomas Cole
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
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.
|
47
README.md
47
README.md
@ -1,47 +0,0 @@
|
||||
# Svelte + TS + Vite
|
||||
|
||||
This template should help get you started developing with Svelte and TypeScript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).
|
||||
|
||||
## Need an official Svelte framework?
|
||||
|
||||
Check out [SvelteKit](https://github.com/sveltejs/kit#readme), which is also powered by Vite. Deploy anywhere with its serverless-first approach and adapt to various platforms, with out of the box support for TypeScript, SCSS, and Less, and easily-added support for mdsvex, GraphQL, PostCSS, Tailwind CSS, and more.
|
||||
|
||||
## Technical considerations
|
||||
|
||||
**Why use this over SvelteKit?**
|
||||
|
||||
- It brings its own routing solution which might not be preferable for some users.
|
||||
- It is first and foremost a framework that just happens to use Vite under the hood, not a Vite app.
|
||||
|
||||
This template contains as little as possible to get started with Vite + TypeScript + Svelte, while taking into account the developer experience with regards to HMR and intellisense. It demonstrates capabilities on par with the other `create-vite` templates and is a good starting point for beginners dipping their toes into a Vite + Svelte project.
|
||||
|
||||
Should you later need the extended capabilities and extensibility provided by SvelteKit, the template has been structured similarly to SvelteKit so that it is easy to migrate.
|
||||
|
||||
**Why `global.d.ts` instead of `compilerOptions.types` inside `jsconfig.json` or `tsconfig.json`?**
|
||||
|
||||
Setting `compilerOptions.types` shuts out all other types not explicitly listed in the configuration. Using triple-slash references keeps the default TypeScript setting of accepting type information from the entire workspace, while also adding `svelte` and `vite/client` type information.
|
||||
|
||||
**Why include `.vscode/extensions.json`?**
|
||||
|
||||
Other templates indirectly recommend extensions via the README, but this file allows VS Code to prompt the user to install the recommended extension upon opening the project.
|
||||
|
||||
**Why enable `allowJs` in the TS template?**
|
||||
|
||||
While `allowJs: false` would indeed prevent the use of `.js` files in the project, it does not prevent the use of JavaScript syntax in `.svelte` files. In addition, it would force `checkJs: false`, bringing the worst of both worlds: not being able to guarantee the entire codebase is TypeScript, and also having worse typechecking for the existing JavaScript. In addition, there are valid use cases in which a mixed codebase may be relevant.
|
||||
|
||||
**Why is HMR not preserving my local component state?**
|
||||
|
||||
HMR state preservation comes with a number of gotchas! It has been disabled by default in both `svelte-hmr` and `@sveltejs/vite-plugin-svelte` due to its often surprising behavior. You can read the details [here](https://github.com/rixo/svelte-hmr#svelte-hmr).
|
||||
|
||||
If you have state that's important to retain within a component, consider creating an external store which would not be replaced by HMR.
|
||||
|
||||
```ts
|
||||
// store.ts
|
||||
// An extremely simple external store
|
||||
import { writable } from 'svelte/store'
|
||||
export default writable(0)
|
||||
```
|
11
astro.config.mjs
Normal file
11
astro.config.mjs
Normal file
@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import svelte from "@astrojs/svelte";
|
||||
import mdx from "@astrojs/mdx";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: 'https://thomaspcole.com',
|
||||
integrations: [tailwind(), svelte(), mdx(), sitemap()]
|
||||
});
|
24
index.html
24
index.html
@ -1,24 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>thomaspcole.com</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
|
||||
rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
3410
package-lock.json
generated
3410
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@ -1,31 +1,29 @@
|
||||
{
|
||||
"name": "temppersonal",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"name": "@thomaspcole/website",
|
||||
"type": "module",
|
||||
"version": "0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^2.0.3",
|
||||
"@tsconfig/svelte": "^3.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.21",
|
||||
"svelte": "^3.55.1",
|
||||
"svelte-check": "^2.10.3",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"tslib": "^2.5.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.2.0"
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": "^18.15.7",
|
||||
"@types/three": "^0.149.0",
|
||||
"@astrojs/mdx": "^2.0.1",
|
||||
"@astrojs/sitemap": "^3.0.3",
|
||||
"@astrojs/svelte": "^5.0.1",
|
||||
"@astrojs/tailwind": "^5.0.3",
|
||||
"astro": "^4.0.4",
|
||||
"cytoscape": "^3.23.0",
|
||||
"daisyui": "^4.4.19",
|
||||
"p5-svelte": "^3.1.2",
|
||||
"svelte-cubed": "^0.2.1",
|
||||
"three": "^0.150.1"
|
||||
"svelte": "^4.2.8",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"theme-change": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
13
public/favicon.svg
Normal file
13
public/favicon.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
|
||||
<path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
|
||||
<path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
@media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
|
||||
</style>
|
||||
</svg>
|
After Width: | Height: | Size: 873 B |
BIN
public/icon-512-maskable.png
Normal file
BIN
public/icon-512-maskable.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 104 KiB |
BIN
public/icon-512.png
Normal file
BIN
public/icon-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 607 KiB After Width: | Height: | Size: 607 KiB |
9
public/particles.min.js
vendored
Normal file
9
public/particles.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
102
public/particlesjs-config-dark.json
Normal file
102
public/particlesjs-config-dark.json
Normal file
@ -0,0 +1,102 @@
|
||||
{
|
||||
"particles": {
|
||||
"number": {
|
||||
"value": 100,
|
||||
"density": {
|
||||
"enable": true,
|
||||
"value_area": 800
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"value": "#ffffff"
|
||||
},
|
||||
"shape": {
|
||||
"type": "circle",
|
||||
"stroke": {
|
||||
"width": 0,
|
||||
"color": "#000000"
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"value": 0.5,
|
||||
"random": false,
|
||||
"anim": {
|
||||
"enable": false,
|
||||
"speed": 1,
|
||||
"opacity_min": 0.1,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"value": 3,
|
||||
"random": true,
|
||||
"anim": {
|
||||
"enable": false,
|
||||
"speed": 40,
|
||||
"size_min": 0.1,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"line_linked": {
|
||||
"enable": true,
|
||||
"distance": 150,
|
||||
"color": "#ffffff",
|
||||
"opacity": 0.4,
|
||||
"width": 1
|
||||
},
|
||||
"move": {
|
||||
"enable": true,
|
||||
"speed": 1,
|
||||
"direction": "none",
|
||||
"random": false,
|
||||
"straight": false,
|
||||
"out_mode": "out",
|
||||
"bounce": false,
|
||||
"attract": {
|
||||
"enable": false,
|
||||
"rotateX": 600,
|
||||
"rotateY": 1200
|
||||
}
|
||||
}
|
||||
},
|
||||
"interactivity": {
|
||||
"detect_on": "canvas",
|
||||
"events": {
|
||||
"onhover": {
|
||||
"enable": false,
|
||||
"mode": "repulse"
|
||||
},
|
||||
"onclick": {
|
||||
"enable": false,
|
||||
"mode": "push"
|
||||
},
|
||||
"resize": true
|
||||
},
|
||||
"modes": {
|
||||
"grab": {
|
||||
"distance": 400,
|
||||
"line_linked": {
|
||||
"opacity": 1
|
||||
}
|
||||
},
|
||||
"bubble": {
|
||||
"distance": 400,
|
||||
"size": 40,
|
||||
"duration": 2,
|
||||
"opacity": 8,
|
||||
"speed": 3
|
||||
},
|
||||
"repulse": {
|
||||
"distance": 200,
|
||||
"duration": 0.4
|
||||
},
|
||||
"push": {
|
||||
"particles_nb": 4
|
||||
},
|
||||
"remove": {
|
||||
"particles_nb": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"retina_detect": true
|
||||
}
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,43 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import Background from "./components/Background.svelte";
|
||||
import Bootup from "./components/Bootup.svelte";
|
||||
import Footer from "./components/Footer.svelte";
|
||||
import NameSplash from "./components/NameSplash.svelte";
|
||||
import About from "./components/About.svelte";
|
||||
import Projects from "./components/Projects.svelte";
|
||||
|
||||
let bootDone = false;
|
||||
let mainContent;
|
||||
|
||||
async function finishBoot() {
|
||||
bootDone = true;
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
mainContent.classList.add("opacity-100");
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ("skip_boot" in localStorage) {
|
||||
console.log("Skipping boot sequence");
|
||||
finishBoot();
|
||||
}
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<Background />
|
||||
<section class="h-screen relative p-4" class:hidden={bootDone}>
|
||||
<Bootup on:boot_done={finishBoot} />
|
||||
</section>
|
||||
<div
|
||||
bind:this={mainContent}
|
||||
class="trasnition-all ease-in duration-300 opacity-0"
|
||||
class:hidden={!bootDone}
|
||||
>
|
||||
<NameSplash />
|
||||
<div class="py-6" />
|
||||
<About />
|
||||
<div class="py-6" />
|
||||
<Projects />
|
||||
<Footer />
|
||||
</div>
|
14
src/app.css
14
src/app.css
@ -1,14 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
background-color: black;
|
||||
user-select: none;
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="projects"
|
||||
class="w-5/6 lg:xl:w-4/5 mx-auto font-mono text-neutral-50 rounded-xl drop-shadow-screen p-4 border-2 border-solid border-green-700"
|
||||
>
|
||||
<p class="font-bold text-2xl pb-6 ">> About Me</p>
|
||||
<p>
|
||||
Detail oriented IT professional with 5+ years in systems and network
|
||||
administration. Excellent problem-solving skills and ability to perform
|
||||
well in a team. Responsible for operation and maintenance of a
|
||||
multicampus enterprise network with 500+ average daily users.
|
||||
Demonstrated experience in reducing operating expenses by implementing
|
||||
open-source solutions and services.
|
||||
</p>
|
||||
</section>
|
@ -1,46 +0,0 @@
|
||||
<div
|
||||
class="fixed w-full h-screen top-0 left-0 bg-green-700/20 rounded-3xl border-solid border-2 border-green-300/50 -z-10 "
|
||||
>
|
||||
<div class="absolute screendim" />
|
||||
<div class="absolute scanlines" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.scanlines {
|
||||
background: linear-gradient(
|
||||
rgb(134, 239, 172, 0.15),
|
||||
rgb(134, 239, 172, 0.15) 3px,
|
||||
transparent 3px,
|
||||
transparent 9px
|
||||
);
|
||||
background-size: 100% 9px;
|
||||
border-radius: 1.5rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
animation: scan 120s infinite linear;
|
||||
}
|
||||
|
||||
.screendim {
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0) 70%,
|
||||
rgba(0, 0, 0, 1) 100%
|
||||
);
|
||||
border-radius: 1.5rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
from {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
to {
|
||||
background-position: 0% -100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,82 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import Progress from "../utils/Progress.svelte";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let bootStage1;
|
||||
let bootStage2;
|
||||
let hexDumpContainer;
|
||||
let percentage = 0;
|
||||
|
||||
onMount(async ()=>{
|
||||
await bootPart1();
|
||||
await bootPart2();
|
||||
})
|
||||
|
||||
function makeConsoleString(text:string){
|
||||
const e = document.createElement('p');
|
||||
e.className = baseTextClass;
|
||||
e.innerText = text;
|
||||
return e;
|
||||
}
|
||||
|
||||
function generateHexString(){
|
||||
return "0x" + (Math.random()*4096).toString(16).replace('.','').slice(0,8).toUpperCase();
|
||||
}
|
||||
|
||||
async function bootPart1() {
|
||||
bootStage1.insertBefore(makeConsoleString("> System Boot Start..."), hexDumpContainer);
|
||||
await sleep(100);
|
||||
for (let i = 0; i <4 ; i++) {
|
||||
var hexCol = document.createElement('div');
|
||||
hexCol.className = "flex flex-col mr-4"
|
||||
hexDumpContainer.appendChild(hexCol);
|
||||
for (let j = 0; j<16; j++){
|
||||
hexCol.appendChild(makeConsoleString(generateHexString()));
|
||||
await sleep(50);
|
||||
}
|
||||
}
|
||||
await sleep(500);
|
||||
bootStage1.appendChild(makeConsoleString("> Boot ROM Initialized"));
|
||||
await sleep(750);
|
||||
bootStage1.appendChild(makeConsoleString("> Checking Integrity"));
|
||||
await sleep(750);
|
||||
bootStage1.appendChild(makeConsoleString("> Checksums OK"));
|
||||
await sleep(750);
|
||||
bootStage1.appendChild(makeConsoleString("> Starting DisplayOS"));
|
||||
await sleep(2000);
|
||||
bootStage1.classList.add("hidden");
|
||||
}
|
||||
|
||||
async function bootPart2(){
|
||||
bootStage2.classList.remove("hidden");
|
||||
await sleep(500)
|
||||
while(percentage < 100){
|
||||
percentage += 10;
|
||||
await sleep(Math.random()*500);
|
||||
}
|
||||
bootStage2.classList.add("opacity-0");
|
||||
await sleep(300);
|
||||
dispatch("boot_done");
|
||||
}
|
||||
|
||||
async function sleep(delay:number){
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
}
|
||||
|
||||
const baseTextClass = "text-neutral-50 drop-shadow-screen text-xl font-mono";
|
||||
</script>
|
||||
|
||||
<div bind:this={bootStage1}>
|
||||
<div bind:this={hexDumpContainer} class="flex flex-row"/>
|
||||
</div>
|
||||
|
||||
<div bind:this={bootStage2} class="hidden w-full h-full transition-all duration-300 ease-out">
|
||||
<div class="relative top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center">
|
||||
<img class="w-1/5 mx-auto" src="/vite.svg" alt="" draggable="false">
|
||||
<div class="h-2 w-4/5 mx-auto mt-8 relative drop-shadow-screen bg-green-700 rounded-md">
|
||||
<Progress color="#fafafa" percentage={percentage}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
11
src/components/Footer.astro
Normal file
11
src/components/Footer.astro
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
import SocialLinks from "./nav/SocialLinks.astro";
|
||||
|
||||
---
|
||||
|
||||
<footer class="z-10 text-gray-600 dark:text-gray-400 body-font">
|
||||
<div class="container flex flex-col items-center px-5 pt-8 pb-2 mx-auto place-content-center sm:flex-row">
|
||||
<p class="mt-4 text-sm text-gray-500 align-middle dark:text-gray-400">© 2024
|
||||
Thomas Cole</p>
|
||||
</div>
|
||||
</footer>
|
@ -1,37 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let showAnimationCheck;
|
||||
|
||||
function updateBootPrefrence(){
|
||||
console.log(showAnimationCheck)
|
||||
if(!showAnimationCheck){
|
||||
localStorage.clear();
|
||||
} else {
|
||||
localStorage.setItem("skip_boot", "");
|
||||
}
|
||||
}
|
||||
|
||||
onMount(()=>{
|
||||
if ("skip_boot" in localStorage) {
|
||||
showAnimationCheck = false;
|
||||
} else {
|
||||
showAnimationCheck = true;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<footer
|
||||
class="w-5/6 xl:lg:w-4/5 mx-auto text-neutral-50 drop-shadow-screen font-mono font-light m-2 p-4"
|
||||
>
|
||||
<div class="flex gap-4 place-content-center underline">
|
||||
<a href="mailto:thomas.patrick.cole@gmail.com">Contact</a>
|
||||
<a href="https://github.com/thomaspcole">Github</a>
|
||||
<a href="https://git.thomaspcole.com/thomascole">Gitea</a>
|
||||
<a href="https://www.linkedin.com/in/thomaspcole/">Linkedin</a>
|
||||
<span>'Boot' animation:</span>
|
||||
<input type="checkbox" bind:checked={showAnimationCheck} on:click={updateBootPrefrence}>
|
||||
</div>
|
||||
<br />
|
||||
<p class="text-sm text-center font-sans">© 2023 Thomas Cole</p>
|
||||
</footer>
|
@ -1,33 +0,0 @@
|
||||
<script>
|
||||
import GlitchText from "../utils/GlitchText.svelte";
|
||||
</script>
|
||||
|
||||
<section id="top" class="h-screen">
|
||||
<div class="relative top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<img
|
||||
class="w-1/2 lg:xl:w-1/4 mx-auto rounded-full aspect-square object-cover mb-6"
|
||||
src="/profile.jpg"
|
||||
alt=""
|
||||
/>
|
||||
<h1
|
||||
class="text-center font-mono text-4xl lg:xl:text-6xl text-neutral-50 drop-shadow-screen"
|
||||
>
|
||||
Thomas Cole
|
||||
</h1>
|
||||
<GlitchText
|
||||
strings={[
|
||||
"system administrator",
|
||||
"linux enthusiast",
|
||||
"web developer",
|
||||
"network engineer",
|
||||
"electronics hobbyist",
|
||||
]}
|
||||
classVars="text-neutral-50 drop-shadow-screen text-center text-xl lg:xl:text-2xl font-mono w-fit mx-auto capitalize"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
class="material-symbols-outlined absolute bottom-[10px] left-1/2 -translate-x-1/2 text-neutral-50 drop-shadow-screen text-4xl lg:xl:text-6xl animate-[pulse_4s_cubic-bezier(0.4,0,0.6,1)_infinite] transition-all duration-500"
|
||||
>
|
||||
expand_more
|
||||
</span>
|
||||
</section>
|
@ -1,10 +0,0 @@
|
||||
<script lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<section class="h-[10vh] w-5/6 lg:xl:w-4/5 mx-auto text-neutral-50 drop-shadow-screen font-mono">
|
||||
<div class="flex flex-col text-2xl">
|
||||
<p>> <a href="#projects" class="underline">Latest Projects</a></p>
|
||||
<p>> <a href="#about" class="underline">About Me</a></p>
|
||||
</div>
|
||||
</section>
|
24
src/components/Particles.astro
Normal file
24
src/components/Particles.astro
Normal file
@ -0,0 +1,24 @@
|
||||
<style lang="scss">
|
||||
#pjs {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
z-index: 0;
|
||||
animation: fadein .5s;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script is:inline src="/particles.min.js" />
|
||||
<div id="pjs"></div>
|
||||
<script>
|
||||
particlesJS.load('pjs', '/particlesjs-config-dark.json');
|
||||
</script>
|
@ -1,44 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ProjectCard from "../utils/ProjectCard.svelte";
|
||||
import LocalDemo from "../utils/LocalDemo.svelte";
|
||||
import Quicksort from "../demos/quicksort.svelte";
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="projects"
|
||||
class="w-5/6 lg:xl:w-4/5 mx-auto font-mono text-neutral-50 rounded-xl drop-shadow-screen p-4 border-2 border-solid border-green-700"
|
||||
>
|
||||
<p class="font-bold text-2xl pb-6 ">> Projects and Demos</p>
|
||||
<ProjectCard
|
||||
title="Pedal-pi"
|
||||
subtitle="A custom effects processor using MODEP"
|
||||
image="https://images.unsplash.com/photo-1511203438670-49f8ea8441c6?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80"
|
||||
link="#"
|
||||
callback={undefined}
|
||||
/>
|
||||
<LocalDemo
|
||||
title="Quicksort Demo"
|
||||
subtitle="A simple visualization of the quicksort algorithm using p5.js"
|
||||
image="https://images.unsplash.com/photo-1669399213378-2853e748f217?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1632&q=80"
|
||||
>
|
||||
<p class="text-xl underline">Quicksort</p>
|
||||
<p>
|
||||
This demo was originally written as part of a job application to
|
||||
work for the IT department at my university. It was the first
|
||||
project I wrote with the aid of the p5.js library. I used this demo
|
||||
as a way to learn how to visualize the quicksort algorithm. I took
|
||||
inspiration from the YouTube video: <a
|
||||
class="underline"
|
||||
href="https://www.youtube.com/watch?v=kPRA0W1kECg"
|
||||
>15 Sorting Algorithms in 6 Minutes</a
|
||||
>. The source code for those interested can be found on my
|
||||
<a
|
||||
class="underline"
|
||||
href="https://github.com/thomaspcole/p5QuicksortVisualization"
|
||||
>Github</a
|
||||
>.
|
||||
</p>
|
||||
<br />
|
||||
<Quicksort />
|
||||
</LocalDemo>
|
||||
</section>
|
@ -1,119 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ResumeItem from "../utils/ResumeItem.svelte";
|
||||
</script>
|
||||
|
||||
<section
|
||||
id="about"
|
||||
class="w-5/6 lg:xl:w-4/5 mx-auto font-mono text-neutral-50 rounded-xl drop-shadow-screen p-4 border-2 border-solid border-green-700"
|
||||
>
|
||||
<a href="#top" class="text-sm font-thin underline">Back to Top</a>
|
||||
<!-- <a href="#top" class="text-sm font-thin underline">Download as PDF</a> -->
|
||||
|
||||
<p class="font-bold text-2xl py-6 ">> Work Experience</p>
|
||||
<ResumeItem
|
||||
title="Director Of Information Technology"
|
||||
subtitle="Christ Lutheran Church | December 2020 - Present"
|
||||
>
|
||||
<ul class="mt-2 mb-4">
|
||||
<li>
|
||||
> Maintain and upgrade critical network infrastructure for a
|
||||
multi-campus environment.
|
||||
</li>
|
||||
<li>> Facilitate backups and ensure their integrity.</li>
|
||||
<li>
|
||||
> Integrate with action teams to develop technology plans and
|
||||
solutions.
|
||||
</li>
|
||||
<li>
|
||||
> Implement new software and hardware solutions with
|
||||
increased functionality while reducing costs by $50,000/yr.
|
||||
</li>
|
||||
</ul>
|
||||
</ResumeItem>
|
||||
<ResumeItem
|
||||
title="Tech Associate"
|
||||
subtitle="Christ Lutheran Church | August 2019 - December 2020 (Part Time)"
|
||||
>
|
||||
<ul class="mt-2 mb-4">
|
||||
<li>
|
||||
> Develop and implement new strategies in collaboration with
|
||||
the Christ Providence Tech team to improve live stream services
|
||||
and reach a broader audience.
|
||||
</li>
|
||||
<li>
|
||||
> Facilitated building and installation of new computer
|
||||
systems to improve recording and streaming capabilities of
|
||||
worship services.
|
||||
</li>
|
||||
<li>
|
||||
> Run graphics for in house worship and live stream services.
|
||||
</li>
|
||||
</ul>
|
||||
</ResumeItem>
|
||||
<ResumeItem
|
||||
title="Tier II Managed Services Technician"
|
||||
subtitle="ScanOnline | June 2019 - December 2020"
|
||||
>
|
||||
<ul class="mt-2 mb-4">
|
||||
<li>
|
||||
> Administer Office 365 and Windows Active Directory
|
||||
infrastructure.
|
||||
</li>
|
||||
<li>
|
||||
> Manage company VOIP phone system and extension listings.
|
||||
</li>
|
||||
<li>
|
||||
> Deploy and configure virtual machines to align with
|
||||
business needs.
|
||||
</li>
|
||||
<li>
|
||||
> Develop new Android applications to suit the business needs
|
||||
of customers operating in the logistics industry.
|
||||
</li>
|
||||
<li>
|
||||
> Maintain legacy Windows Mobile applications for existing
|
||||
customers.
|
||||
</li>
|
||||
<li>
|
||||
> Preform configuration and maintenance of customer hardware
|
||||
and software.
|
||||
</li>
|
||||
</ul>
|
||||
</ResumeItem>
|
||||
<ResumeItem
|
||||
title="Student Network Analyst"
|
||||
subtitle="University of North Carolina at Greensboro | February 2018 - May 2019"
|
||||
>
|
||||
<ul class="mt-2 mb-4">
|
||||
<li>
|
||||
> Assisted in maintenance and troubleshooting of enterprise
|
||||
network systems.
|
||||
</li>
|
||||
<li>
|
||||
> Preformed on-boarding of new network devices at the
|
||||
physical level.
|
||||
</li>
|
||||
</ul>
|
||||
</ResumeItem>
|
||||
|
||||
<p class="font-bold text-2xl py-6 ">> Education</p>
|
||||
<ResumeItem
|
||||
title="The University of North Carolina at Greensboro"
|
||||
subtitle=""
|
||||
>
|
||||
<ul class="mt-2 mb-4">
|
||||
<li>
|
||||
> Bachelor Of Science Information Systems and Supply Chain
|
||||
Management
|
||||
</li>
|
||||
<li>> Minor in Computer Science</li>
|
||||
</ul>
|
||||
</ResumeItem>
|
||||
|
||||
<p class="font-bold text-2xl py-6 ">> Professional Certifications</p>
|
||||
<ResumeItem title="Dante Level 3" subtitle="">
|
||||
<ul class="mt-2 mb-4">
|
||||
<li>> Audinate | Certificate earned March 2021</li>
|
||||
</ul>
|
||||
</ResumeItem>
|
||||
</section>
|
33
src/components/misc/BlogProjectCard.astro
Normal file
33
src/components/misc/BlogProjectCard.astro
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
image: string;
|
||||
link: string;
|
||||
category: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
const { title, subtitle, image, link, category, date } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="p-4 md:w-1/3">
|
||||
<div class="w-full h-full overflow-hidden bg-gray-500 rounded-lg sm:pt-0 rounded-xl bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-10">
|
||||
<img class="object-cover object-center w-full lg:h-48 md:h-36" src={image} alt="blog" draggable="false">
|
||||
<div class="p-6">
|
||||
<h2 class="mb-1 text-xs font-medium tracking-widest text-gray-300">{category}</h2>
|
||||
<h1 class="mb-3 text-lg font-medium text-white title-font">{title}</h1>
|
||||
<p class="mb-3 leading-relaxed">{subtitle}</p>
|
||||
<div class="flex flex-wrap items-center ">
|
||||
<a href={link} class="inline-flex items-center text-indigo-200 md:mb-2 lg:mb-0">Read
|
||||
<svg class="w-4 h-4 ml-2" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12h14"></path>
|
||||
<path d="M12 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<span class="text-sm leading-none text-right text-gray-300 grow">{date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
13
src/components/misc/Button.astro
Normal file
13
src/components/misc/Button.astro
Normal file
@ -0,0 +1,13 @@
|
||||
---
|
||||
export interface Props {
|
||||
text: string;
|
||||
link: string;
|
||||
width?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const { text, link, width, icon } = Astro.props;
|
||||
let baseClass = "flex items-center p-2 mx-auto font-bold text-center text-white transition-all border-2 border-white rounded-md place-content-center hover:text-black hover:bg-white " + width;
|
||||
---
|
||||
|
||||
<a class={baseClass} href={link}>{text}{icon && <span class="ml-2 material-symbols-outlined">{icon}</span>}</a>
|
1
src/components/misc/Divider.astro
Normal file
1
src/components/misc/Divider.astro
Normal file
@ -0,0 +1 @@
|
||||
<div class="w-full h-0.5 rounded divider"></div>
|
29
src/components/misc/ThemeToggle.astro
Normal file
29
src/components/misc/ThemeToggle.astro
Normal file
@ -0,0 +1,29 @@
|
||||
<label class="transition ease-in-out swap swap-rotate hover:scale-125">
|
||||
<!-- this hidden checkbox controls the state -->
|
||||
<input id="themetoggle" type="checkbox" onchange="updateTheme()" />
|
||||
<span class="swap-off material-symbols-outlined">
|
||||
dark_mode
|
||||
</span>
|
||||
<span class="swap-on material-symbols-outlined">
|
||||
light_mode
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<script is:inline>
|
||||
let toggle = document.getElementById("themetoggle");
|
||||
|
||||
if (localStorage.theme === 'light') {
|
||||
toggle.checked = true;
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
if (toggle.checked) {
|
||||
document.documentElement.classList.remove('dark')
|
||||
localStorage.setItem("theme", "light");
|
||||
} else {
|
||||
document.documentElement.classList.add('dark')
|
||||
localStorage.setItem("theme", "dark");
|
||||
};
|
||||
window.dispatchEvent(new Event("themeupdate"));
|
||||
}
|
||||
</script>
|
22
src/components/nav/Nav.astro
Normal file
22
src/components/nav/Nav.astro
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
export interface Props {
|
||||
pageName?: string;
|
||||
}
|
||||
|
||||
const { pageName } = Astro.props;
|
||||
let displayedName = "";
|
||||
if (pageName) {
|
||||
displayedName = pageName.replace("|", "").trim();
|
||||
}
|
||||
---
|
||||
|
||||
<header class="z-10 text-white">
|
||||
<div class="container flex flex-col flex-wrap items-center p-5 mx-auto md:flex-row">
|
||||
<nav class="flex flex-wrap items-center justify-center text-base md:ml-auto">
|
||||
<a href="/" class="mr-5">Home</a>
|
||||
<a href="/projects/1" class="mr-5">Projects</a>
|
||||
<a href="/about" class="mr-5">About</a>
|
||||
<a href="/contact" class="mr-5">Contact</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
33
src/components/nav/SocialLinks.astro
Normal file
33
src/components/nav/SocialLinks.astro
Normal file
@ -0,0 +1,33 @@
|
||||
<span class="flex justify-center gap-4 mt-2">
|
||||
<a class="text-white" href="https://git.thomaspcole.com/thomascole">
|
||||
<svg class="w-5 h-5" id="main_outline" viewBox="0 0 640 550" xml:space="preserve" stroke="currentColor">
|
||||
<g>
|
||||
<path id="teabag" class="fill-none"
|
||||
d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z" />
|
||||
<g>
|
||||
<g>
|
||||
<path class="fill-current"
|
||||
d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z" />
|
||||
<path class="fill-current"
|
||||
d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
<a class="text-white" href="https://github.com/thomaspcole">
|
||||
<svg class="w-5 h-5" viewBox="0 0 98 96" fill="currentColor" stroke="currentColor" stroke-linecap="round"
|
||||
stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a class="text-white" href="https://www.linkedin.com/in/thomaspcole">
|
||||
<svg fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="0"
|
||||
class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path stroke="none"
|
||||
d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-2-2 2 2 0 00-2 2v7h-4v-7a6 6 0 016-6zM2 9h4v12H2z"></path>
|
||||
<circle cx="4" cy="4" r="2" stroke="none"></circle>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
20
src/components/resume/Timeline.astro
Normal file
20
src/components/resume/Timeline.astro
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
export interface Props {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
const { title, subtitle } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="flex">
|
||||
<div>
|
||||
<span class="block w-4 h-4 mt-2 bg-white rounded-full"></span>
|
||||
<span class="bg-white block h-full w-[2px] translate-x-[7px]"></span>
|
||||
</div>
|
||||
<div class="px-5">
|
||||
<p class="text-xl font-semibold">{title}</p>
|
||||
<span class="text-sm font-light">{subtitle}</span>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
@ -1,199 +0,0 @@
|
||||
<script lang="ts">
|
||||
import P5, { type Sketch } from "p5-svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
// Demo modified from a very old p5js sketch written in college.
|
||||
// The original was part of a the application process to work for the university IT department
|
||||
// https://github.com/thomaspcole/p5QuicksortVisualization
|
||||
|
||||
let data = [];
|
||||
let animArray = [];
|
||||
let valuesToGenerate = 200;
|
||||
let frame = 0;
|
||||
|
||||
let width;
|
||||
let sketch:Sketch;
|
||||
|
||||
//Added controls for play/pause and stepping frame by frame.
|
||||
let playpause = false;
|
||||
|
||||
onMount(()=>{
|
||||
if(width < 1000){
|
||||
valuesToGenerate = 75;
|
||||
}
|
||||
const sketchWidth = Math.floor(width*.8);
|
||||
const barWidth = (sketchWidth-20)/valuesToGenerate;
|
||||
sketch = (p5) => {
|
||||
p5.setup = () => {
|
||||
for (let i = 0; i < valuesToGenerate; i++) {
|
||||
data[i]=(i%700);
|
||||
}
|
||||
|
||||
p5.shuffle(data, true)
|
||||
quicksort(data, 0, data.length-1);
|
||||
animArray.push(new arrayFrame(data.toString(), null, null, null));
|
||||
|
||||
//setup the canvas
|
||||
p5.createCanvas(sketchWidth,valuesToGenerate*2);
|
||||
p5.frameRate(30);
|
||||
}
|
||||
|
||||
p5.draw = () => {
|
||||
if(playpause){
|
||||
if(frame == animArray.length-1){
|
||||
playpause = !playpause;
|
||||
} else {
|
||||
drawFrame(frame);
|
||||
frame++;
|
||||
}
|
||||
|
||||
} else {
|
||||
drawFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
function drawFrame(index){
|
||||
let dataFrame = animArray[index];
|
||||
p5.background(28);
|
||||
for (var i = 0; i < valuesToGenerate; i++){
|
||||
p5.fill(255)
|
||||
if(dataFrame.midIndex == i){
|
||||
p5.fill(255,0,0);
|
||||
}
|
||||
if(dataFrame.lowVal == i || dataFrame.highVal == i){
|
||||
p5.fill(0,255,0);
|
||||
}
|
||||
p5.rect(barWidth*i+10,valuesToGenerate*2-10, barWidth,-dataFrame.getArray()[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Copied directly from original code
|
||||
* @param array the array of data being worked on
|
||||
* @param low the low values
|
||||
* @param high the high value
|
||||
*/
|
||||
function quicksort(array, low, high){
|
||||
animArray.push(new arrayFrame(array.toString(), 0, array.length, 0));
|
||||
//Do we need to sort?
|
||||
if(low >= high){
|
||||
return;
|
||||
}
|
||||
|
||||
//Pick a pivot point in the middle of the passed array.
|
||||
let mid = Math.floor(low + ((high-low) / 2));
|
||||
let pivot = array[mid];
|
||||
|
||||
let l = low;
|
||||
let h = high;
|
||||
|
||||
while(l <= h){
|
||||
while(array[l] < pivot){
|
||||
l++;
|
||||
}
|
||||
|
||||
while(array[h] > pivot){
|
||||
h--;
|
||||
}
|
||||
|
||||
if(l <= h){
|
||||
//console.log(array, l, h, mid);
|
||||
//console.log("Switch: " + array[l] + ":" + array[h]);
|
||||
animArray.push(new arrayFrame(array.toString(), l, h, mid));
|
||||
|
||||
let tmp = array[l];
|
||||
array[l] = array[h];
|
||||
array[h] = tmp;
|
||||
l++;
|
||||
h--;
|
||||
}
|
||||
}
|
||||
|
||||
if(low < h){
|
||||
quicksort(array, low, h);
|
||||
}
|
||||
|
||||
if(high > l){
|
||||
quicksort(array, l, high);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a arrayFrame object
|
||||
* @param arrayState the array of data at a current point in time
|
||||
* @param lowVal the low index
|
||||
* @param highVal the high midIndex
|
||||
* @param midIndex the pivot index
|
||||
* @param getArray Returns the array at the current time
|
||||
*/
|
||||
function arrayFrame(arrayState, lowVal, highVal, midIndex){
|
||||
this.arrayState = arrayState;
|
||||
this.lowVal = lowVal;
|
||||
this.highVal = highVal;
|
||||
this.midIndex = midIndex;
|
||||
|
||||
this.getArray = function(){
|
||||
return this.arrayState.split(",");
|
||||
}
|
||||
}
|
||||
|
||||
function prevFrame(){
|
||||
if(frame == 0){
|
||||
return;
|
||||
}
|
||||
frame--;
|
||||
}
|
||||
|
||||
function nextFrame(){
|
||||
if(frame == animArray.length-1){
|
||||
return;
|
||||
}
|
||||
frame++;
|
||||
}
|
||||
|
||||
function reset(){
|
||||
frame=0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={width}/>
|
||||
<div class="w-fit mx-auto">
|
||||
<P5 {sketch}/>
|
||||
</div>
|
||||
<div class="flex justify-evenly mt-6 mb-4">
|
||||
<button on:click={prevFrame}>
|
||||
<span class="material-symbols-outlined">
|
||||
skip_previous
|
||||
</span>
|
||||
</button>
|
||||
<button on:click={()=>{
|
||||
if(frame==animArray.length-1){
|
||||
reset();
|
||||
} else {
|
||||
playpause = !playpause;
|
||||
}
|
||||
}}>
|
||||
{#if playpause}
|
||||
<span class="material-symbols-outlined">
|
||||
pause
|
||||
</span>
|
||||
{:else if frame==animArray.length-1}
|
||||
<span class="material-symbols-outlined">
|
||||
restart_alt
|
||||
</span>
|
||||
{:else}
|
||||
<span class="material-symbols-outlined">
|
||||
play_arrow
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
<button on:click={nextFrame}>
|
||||
<span class="material-symbols-outlined">
|
||||
skip_next
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="astro/client" />
|
36
src/layouts/InteractiveDemoLayout.astro
Normal file
36
src/layouts/InteractiveDemoLayout.astro
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
import Divider from "../components/misc/Divider.astro";
|
||||
import Layout from "./Layout.astro";
|
||||
import Nav from "../components/nav/Nav.astro";
|
||||
const { frontmatter } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Nav/>
|
||||
<main class="z-10 flex flex-col justify-center w-5/6 mx-auto mt-4 mb-6 sm:w-5/6 md:w-4/5 lg:w-4/5 xl:w-4/5 2xl:w-1/2 overflow-hidden bg-gray-500 rounded-lg sm:pt-0 rounded-xl bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-10">
|
||||
<div class="relative mb-4">
|
||||
<img class="object-cover aspect-video rounded-xl max-h-[50vh] w-full" src={frontmatter.image} alt=""
|
||||
draggable="false" />
|
||||
<a href={frontmatter.imageattr} class="absolute bottom-0 right-0 h-[24px]">
|
||||
<span class="material-symbols-outlined opacity-25 mr-[4px]">link</span>
|
||||
</a>
|
||||
</div>
|
||||
<p class="my-2 text-5xl px-2 font-bold text-white">{frontmatter.title}</p>
|
||||
<p class="text-gray-200 px-2">{frontmatter.date}</p>
|
||||
<Divider />
|
||||
<!-- Render our markdown with the normal header styling and some extra padding. -->
|
||||
<style is:inline>
|
||||
.astro-code {
|
||||
margin-bottom: 1rem;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.prose p>img{
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
<article id="article" class="max-w-full prose text-white px-2 pb-2">
|
||||
<slot />
|
||||
</article>
|
||||
</main>
|
||||
</Layout>
|
59
src/layouts/Layout.astro
Normal file
59
src/layouts/Layout.astro
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
import Nav from '../components/nav/Nav.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import Particles from '../components/Particles.astro';
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const { title } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="light">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/icon-512.png" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>thomaspcole.com {title}</title>
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap"
|
||||
rel="stylesheet">
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
||||
<style>
|
||||
.material-symbols-outlined {
|
||||
font-variation-settings:
|
||||
'FILL' 0,
|
||||
'wght' 400,
|
||||
'GRAD' 0,
|
||||
'opsz' 48
|
||||
}
|
||||
|
||||
.bg-gradient {
|
||||
background: hsla(205, 46%, 10%, 1);
|
||||
background: linear-gradient(90deg, hsla(205, 46%, 10%, 1) 0%, hsla(191, 28%, 23%, 1) 50%, hsla(207, 41%, 27%, 1) 100%);
|
||||
background: -moz-linear-gradient(90deg, hsla(205, 46%, 10%, 1) 0%, hsla(191, 28%, 23%, 1) 50%, hsla(207, 41%, 27%, 1) 100%);
|
||||
background: -webkit-linear-gradient(90deg, hsla(205, 46%, 10%, 1) 0%, hsla(191, 28%, 23%, 1) 50%, hsla(207, 41%, 27%, 1) 100%);
|
||||
filter: progid: DXImageTransform.Microsoft.gradient(startColorstr="#0e1c26", endColorstr="#2a454b", GradientType=1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col w-full h-full min-h-screen">
|
||||
<div class="fixed top-0 left-0 w-full h-full min-h-screen bg-gradient"/>
|
||||
<Particles />
|
||||
<!-- <Nav pageName={title} /> -->
|
||||
<slot />
|
||||
<div class="grow"></div>
|
||||
<Footer />
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,8 +0,0 @@
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = new App({
|
||||
target: document.getElementById('app'),
|
||||
})
|
||||
|
||||
export default app
|
12
src/pages/404.astro
Normal file
12
src/pages/404.astro
Normal file
@ -0,0 +1,12 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
---
|
||||
|
||||
<Layout title=" | 404">
|
||||
<main class="z-10 flex flex-col w-full text-center text-white mt-36 place-content-center">
|
||||
<h1 class="text-5xl">Uh Oh!</h1>
|
||||
<p class="text-2xl">Sorry about that I can't seem to find what you are looking for.</p>
|
||||
<br>
|
||||
<a href="/" class="underline">Go Home</a>
|
||||
</main>
|
||||
</Layout>
|
81
src/pages/about.astro
Normal file
81
src/pages/about.astro
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Timeline from "../components/resume/Timeline.astro";
|
||||
import Nav from '../components/nav/Nav.astro';
|
||||
import Button from "../components/misc/Button.astro";
|
||||
|
||||
|
||||
const resumeRaw = await fetch('https://git.thomaspcole.com/thomascole/Resume/raw/branch/master/resume.json');
|
||||
const resume = await resumeRaw.json();
|
||||
|
||||
function parseDate(date: string) {
|
||||
const retVal = new Date(date).toLocaleString('default', { month: 'long', year: "numeric" })
|
||||
if (retVal === "Invalid Date") {
|
||||
return "Present";
|
||||
}
|
||||
|
||||
return retVal;
|
||||
}
|
||||
---
|
||||
|
||||
<Layout title={" | Resume"}>
|
||||
<Nav/>
|
||||
|
||||
<style is:inline>
|
||||
.time-line-container>div:last-child>div:nth-child(1)>span:nth-child(2) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<main class="z-10 w-full mx-auto text-white sm:w-5/6 md:w-4/5 lg:w-4/5 xl:w-4/5 2xl:w-1/2">
|
||||
<section class="w-full h-full px-4 pt-2 bg-gray-500 sm:pt-0 rounded-xl bg-clip-padding backdrop-filter backdrop-blur-md bg-opacity-10">
|
||||
<div class="container pb-6 mx-auto sm:pb-0">
|
||||
<div class="flex flex-col-reverse items-start justify-between mx-auto sm:flex-row sm:items-center">
|
||||
<p class="py-0 mr-auto text-2xl font-bold sm:py-6">About Me</p>
|
||||
<div class="hidden sm:block">
|
||||
<Button link="https://git.thomaspcole.com/attachments/9027185b-9841-4687-addd-0e5ba5eb0d1f" text="Download as PDF" icon="download"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p>{resume.basics.summary}</p>
|
||||
|
||||
<div class="divider" />
|
||||
<p class="pb-6 text-2xl font-bold">Work Experience</p>
|
||||
<div class="time-line-container">
|
||||
{resume.work.map((item:any)=>
|
||||
<Timeline title={item.position} subtitle={item.name + " | " + parseDate(item.startDate) + " - " +
|
||||
parseDate(item.endDate)}>
|
||||
<ul class="text-sm ml-3.5 list-disc mt-2 mb-4">
|
||||
{item.highlights.map((highlight:any)=>
|
||||
<li>{highlight}</li>
|
||||
)}
|
||||
</ul>
|
||||
</Timeline>
|
||||
)}
|
||||
</div>
|
||||
<p class="py-6 text-2xl font-bold">Education</p>
|
||||
<div class="time-line-container">
|
||||
{resume.education.map((item:any)=>
|
||||
<Timeline title={item.institution} subtitle={item.area + " | " + parseDate(item.endDate)} />
|
||||
)}
|
||||
</div>
|
||||
<p class="py-6 text-2xl font-bold">Professional Certificates</p>
|
||||
<div class="time-line-container">
|
||||
{resume.certificates.map((item:any)=>
|
||||
<Timeline title={item.name} subtitle={item.issuer + " | " + parseDate(item.date)} />
|
||||
)}
|
||||
</div>
|
||||
<p class="py-6 text-2xl font-bold">Skills</p>
|
||||
{resume.skills.map((item:any)=>
|
||||
<div class="divider">{item.name}</div>
|
||||
<div class="flex flex-wrap gap-2 pb-4 mx-auto my-2">
|
||||
{item.keywords.map((skill:any)=>
|
||||
<div
|
||||
class="text-black bg-white border-gray-200 badge badge-lg">
|
||||
{skill}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</Layout>
|
44
src/pages/contact.astro
Normal file
44
src/pages/contact.astro
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Button from "../components/misc/Button.astro";
|
||||
import Nav from "../components/nav/Nav.astro";
|
||||
---
|
||||
|
||||
<Layout title={" | Contact"}>
|
||||
<Nav/>
|
||||
<section class="relative z-10 text-white">
|
||||
<div class="container w-full h-full px-5 py-12 pt-0 mx-auto sm:pt-24 rounded-xl">
|
||||
<div class="flex flex-col w-full text-center">
|
||||
<h1 class="mb-4 text-2xl font-medium sm:text-3xl">Contact Me</h1>
|
||||
</div>
|
||||
<div class="mx-auto lg:w-1/2 md:w-2/3">
|
||||
<div class="flex flex-wrap -m-2">
|
||||
<div class="w-1/2 p-2">
|
||||
<div class="relative">
|
||||
<label for="name" class="text-sm leading-7">Name</label>
|
||||
<input type="text" id="name" name="name"
|
||||
class="w-full px-3 py-1 text-base leading-6 text-white transition-colors duration-200 ease-in-out bg-gray-500 border border-white rounded outline-none resize-none backdrop-blur-md bg-opacity-10 focus:text-black focus:bg-white">
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 p-2">
|
||||
<div class="relative">
|
||||
<label for="email" class="text-sm leading-7">Email</label>
|
||||
<input type="email" id="email" name="email"
|
||||
class="w-full px-3 py-1 text-base leading-6 text-white transition-colors duration-200 ease-in-out bg-gray-500 border border-white rounded outline-none resize-none backdrop-blur-md bg-opacity-10 focus:text-black focus:bg-white">
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full p-2">
|
||||
<div class="relative">
|
||||
<label for="message" class="text-sm leading-7">Message</label>
|
||||
<textarea id="message" name="message"
|
||||
class="w-full h-32 px-3 py-1 text-base leading-6 text-white transition-colors duration-200 ease-in-out bg-gray-500 border border-white rounded outline-none resize-none backdrop-blur-md bg-opacity-10 focus:text-black focus:bg-white"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full p-2">
|
||||
<Button text="Submit">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
22
src/pages/index.astro
Normal file
22
src/pages/index.astro
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
import Layout from "../layouts/Layout.astro";
|
||||
import Button from "../components/misc/Button.astro";
|
||||
import SocialLinks from "../components/nav/SocialLinks.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<main class="z-10">
|
||||
<div class="flex flex-col w-[90%] mx-auto mt-24 text-center text-white lg:w-1/4">
|
||||
<img class="object-cover w-40 mx-auto mb-2 rounded-full aspect-square drop-shadow-md" src="/img/profile.jpg" alt="profile_img">
|
||||
<h1 class="mb-2 text-3xl font-bold drop-shadow-md title-font sm:text-4xl">Thomas Cole</h1>
|
||||
<p class="mb-8 font-thin drop-shadow-md">Web Developer, System Administrator,<br/> and Network Engineer</p>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<Button text="Projects" link="/projects/1" width="w-1/2"/>
|
||||
<Button text="About" link="/about" width="w-1/2"/>
|
||||
<Button text="Contact" link="/contact" width="w-1/2"/>
|
||||
<SocialLinks/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
66
src/pages/projects/[page].astro
Normal file
66
src/pages/projects/[page].astro
Normal file
@ -0,0 +1,66 @@
|
||||
---
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import BlogProjectCard from "../../components/misc/BlogProjectCard.astro";
|
||||
import Nav from "../../components/nav/Nav.astro";
|
||||
|
||||
export async function getStaticPaths({ paginate }: { paginate: any }) {
|
||||
const posts = (await Astro.glob("./**/*.{md,mdx}")).sort(
|
||||
(a, b) =>
|
||||
new Date(b.frontmatter.date).valueOf() -
|
||||
new Date(a.frontmatter.date).valueOf()
|
||||
).filter((record)=> record.frontmatter.active == true);
|
||||
|
||||
return paginate(posts, { pageSize: 6 });
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title=" | Projects">
|
||||
<Nav/>
|
||||
<main class="z-10 w-full mx-auto mt-4 md:w-5/6 lg:w-4/5 xl:w-4/5">
|
||||
<section class="text-white">
|
||||
<p class="px-5 text-2xl font-bold drop-shadow-md sm:text-2xl md:text-2xl lg:text-4xl xl:text-4xl 2xl:text-4xl">Latest
|
||||
Projects</p>
|
||||
<div class="container px-5 py-6 mx-auto">
|
||||
<div class="flex flex-wrap -m-4">
|
||||
{
|
||||
page.data.map((post: any) => (
|
||||
<BlogProjectCard title={post.frontmatter.title} subtitle={post.frontmatter.tagline}
|
||||
image={post.frontmatter.image} link={post.url} category={post.frontmatter.category} date={post.frontmatter.date}/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex justify-between w-1/2 mx-auto">
|
||||
{
|
||||
page.url.prev ? (
|
||||
<a class="flex w-40" href={page.url.prev}>
|
||||
<span class="material-symbols-outlined">
|
||||
chevron_left
|
||||
</span>
|
||||
<p>Previous Page</p>
|
||||
</a>
|
||||
) : (
|
||||
<div class="w-40" />
|
||||
)
|
||||
}
|
||||
|
||||
<p class="text-center grow whitespace-nowrap">Page {page.currentPage}</p>
|
||||
{
|
||||
page.url.next ? (
|
||||
<a class="flex w-40" href={page.url.next}>
|
||||
<p class="text-right grow">Next Page</p>
|
||||
<span class="material-symbols-outlined">
|
||||
chevron_right
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<div class="w-40" />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
38
src/pages/projects/digital-signage.mdx
Normal file
38
src/pages/projects/digital-signage.mdx
Normal file
@ -0,0 +1,38 @@
|
||||
---
|
||||
layout: "../../layouts/InteractiveDemoLayout.astro"
|
||||
title: "Adventures in Digital Signage"
|
||||
tagline: "Using slideshow.digital for cheap reliable signs"
|
||||
date: "April 17, 2023"
|
||||
category: "Software"
|
||||
image: "https://images.unsplash.com/photo-1496442226666-8d4d0e62e6e9?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80"
|
||||
imageattr: "https://unsplash.com/photos/TaCk3NspYe0"
|
||||
active: false
|
||||
---
|
||||
|
||||
### Overview
|
||||
|
||||
A recent project at work gave me the opportunity to explore the fascinating world of digital signage. I was tasked with making use of a few tvs that we had laying around to display the current events and advertisements for our organization.
|
||||
|
||||
After a quick google search I found no shortage of off the shelf solutions. However, I couldn't find a product that really suited our needs and that was reasonably priced. So, being a tinkerer and not having any form of budget to purchase an off the shelf solution I decided to take a stab at creating my own signage solution.
|
||||
|
||||
### What didn't work
|
||||
|
||||
My first attempt to tackle this was to create a web first approach. I thought if the web server did all the heavy lifting that it would be trivial to attach any old device that could display a web page to a tv and be done. For the most part this worked but it had some drawbacks, the biggest among them being the signage could not operate without an internet connection. I also had issues with the Raspberry pi 4 and gpu acceleration, so displaying a large collection of pictures was a bit choppy at times (*To be fair that might be my fault*).
|
||||
|
||||
The second attempt was to have each sign rsync with a master folder of images and display them locally using a python script. This was functional but not visually appealing. We can do better.
|
||||
|
||||
### Enter Slideshow
|
||||
|
||||
I don't remember how I managed to stumble across Slideshow but I sure am glad that I found it.
|
||||
|
||||
[Slideshow](https://slideshow.digital/) bills itself as a free digital signage app for android devices. AND. IT. DELIVERS.
|
||||
|
||||
The app is configured through a web interface that runs locally on device. It has a simple yet powerful screen builder and allows for content to be synced from remote sources.
|
||||
|
||||
The initial setup is a bit cumbersome but after getting everything configured on a test device the built in backup and restore feature allows for easy export of a configuration file that can be dropped onto a new device. Thanks to the built in file sync server, it is able to drop into our existing workflow for marketing content. Our marketing team does not have to do any additional work to make sure the advertisements make it to the signage.
|
||||
|
||||
I chose to pair slideshow with a new (but extremely cheap) [Chromecast with Google TV](https://store.google.com/us/product/chromecast_google_tv?hl=en-US) and it works flawlessly. The worst part of the setup is the rather forceful insertion of ads onto the Chromecast home screen. Putting the device in app only mode mostly resolved that issue though and makes it simple enough to restart slideshow if the chromecast reboots for any reason.
|
||||
|
||||
The app does offer the ability to restart itself after a device reboot however I personally have not gotten that working yet. I don't think it is the apps fault; I have a feeling the Chromecast does not like having its launcher overridden.
|
||||
|
||||
Slideshow has way more features than we are using in our setup but it scratches that itch that other solutions could not and the end product is a very clean and professional looking sign.
|
14
src/pages/projects/pedalpi/audio.svelte
Normal file
14
src/pages/projects/pedalpi/audio.svelte
Normal file
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import silence from "./silence.mp3";
|
||||
export let title = "Title me";
|
||||
export let file = silence;
|
||||
</script>
|
||||
|
||||
<div class="w-full flex my-4">
|
||||
<p class="my-auto mr-4 w-1/6 text-center font-bold">{title}</p>
|
||||
<audio controls preload="metadata" style="width: 100% !important">
|
||||
<source src={file} type="audio/mpeg" />
|
||||
Audio not supported
|
||||
</audio>
|
||||
<div class="w-1/6"/>
|
||||
</div>
|
47
src/pages/projects/pedalpi/pedal-pi.mdx
Normal file
47
src/pages/projects/pedalpi/pedal-pi.mdx
Normal file
@ -0,0 +1,47 @@
|
||||
---
|
||||
layout: "../../../layouts/InteractiveDemoLayout.astro"
|
||||
title: "Pedal-pi"
|
||||
tagline: "A bass guitar custom effects processor using MODEP"
|
||||
date: "May 15, 2023"
|
||||
category: "Hardware"
|
||||
image: "https://images.unsplash.com/photo-1511203438670-49f8ea8441c6?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1470&q=80"
|
||||
imageattr: "https://unsplash.com/photos/lEwyj4FiHHU"
|
||||
active: false
|
||||
---
|
||||
|
||||
import Audio from './audio.svelte';
|
||||
|
||||
### Overview
|
||||
|
||||
For once I have a post about something other than a project that was put on my desk at work. I have the opportunity to play bass guitar with a local rock band. Often when I am playing with them I can't help but notice the dizzying array of pedals the guitar players have at their disposal and how drastically they can change the sound.
|
||||
|
||||
While bass guitar pedals do exist, they are often hundreds of dollars a piece and while I enjoy playing, I would rather spend that kind of money elsewhere. So I wondered, is there a way I can have the functionality of a massive pedalboard without actually breaking the bank? Enter MODEP.
|
||||
|
||||
### What is MODEP
|
||||
|
||||
[MODEP](https://blokas.io/modep/) is a virtual guitar effects pedalboard powered by the Raspberry Pi. It's user interface is built on top of the work done by the team over at [MOD](https://mod.audio/) and is powered by RaspberryPi OS and Jack audio under the hood.
|
||||
|
||||
### Building a pedalboard
|
||||
|
||||
[Blokas](https://blokas.io/), the team behind MODEP, have a official hat for the Raspberry Pi called the [PiSound](https://blokas.io/pisound/) if you are looking for a compact all in one solution to run MODEP, but I decided to build my own. Why? Because it's more fun that way.
|
||||
|
||||
I already had a pedalboard and a spare Raspberry Pi so the biggest piece I needed was a audio interface. The parts list is below.
|
||||
|
||||
- Pedaltrain Nano+
|
||||
- Korg Pitchblack Advance
|
||||
- Raspberry Pi 4 4gb model
|
||||
- Behringer U-Phoria UM2
|
||||
- Amazon Basics 1/4in patch cable.
|
||||
|
||||
If you are ok using 2 power bricks you can stop with the list above. One 5v brick to power the Pi and a 9v brick for any pedals. I wanted a more all in one solution so I also got a beefy 12v power brick and used a few voltage step down converters to power everything from one plug. However, that is totally optional.
|
||||
### Pictures
|
||||
|
||||
### Demo
|
||||
|
||||
{/* Import audio file before passing it to element */}
|
||||
<Audio title="Test" client:idle/>
|
||||
<Audio title="Test 2" client:idle/>
|
||||
|
||||
### Conclusion
|
||||
|
||||
Overall I'm very pleased with how this project came together. It doesn't make me a better player but at least I can now make cool sounds. 🙃
|
BIN
src/pages/projects/pedalpi/silence.mp3
Normal file
BIN
src/pages/projects/pedalpi/silence.mp3
Normal file
Binary file not shown.
21
src/pages/projects/quicksort/quicksort.mdx
Normal file
21
src/pages/projects/quicksort/quicksort.mdx
Normal file
@ -0,0 +1,21 @@
|
||||
---
|
||||
layout: "../../../layouts/InteractiveDemoLayout.astro"
|
||||
title: "Quicksort Demo"
|
||||
tagline: "A simple visualization of the quicksort algorithm using p5.js"
|
||||
date: "December 23, 2022"
|
||||
category: "Demo"
|
||||
image: "https://images.unsplash.com/photo-1669399213378-2853e748f217?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1632&q=80"
|
||||
imageattr: "https://unsplash.com/photos/5dgXQJ7ezuU"
|
||||
active: true
|
||||
---
|
||||
import Quicksort from './quicksort.svelte';
|
||||
|
||||
<Quicksort client:load/>
|
||||
---
|
||||
This demo was originally written as part of a job application to work for the IT department at my university. It was the first project I wrote with the aid of the p5.js library.
|
||||
I used this demo as a way to learn how to visualize the quicksort algorithm.\
|
||||
\
|
||||
I took inspiration from the YouTube video: *[15 Sorting Algorithms in 6 Minutes](https://www.youtube.com/watch?v=kPRA0W1kECg)*. The source code for those interested can be found on my *[Github](https://github.com/thomaspcole/p5QuicksortVisualization)*.\
|
||||
\
|
||||
The demo works by generating an array of 200 values then randomly shuffling the array. We then call the ```quicksort``` function to sort the array. At each iteration of the function, the code takes a 'snapshot' of the data array at that iteration.
|
||||
By iterating through the 'snapshot' array we get our final animation. It also allows for stepping forward and backwards to see each step of the animation.
|
186
src/pages/projects/quicksort/quicksort.svelte
Normal file
186
src/pages/projects/quicksort/quicksort.svelte
Normal file
@ -0,0 +1,186 @@
|
||||
<script>
|
||||
import P5 from "p5-svelte/P5.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
// Demo modified from a very old p5js sketch written in college.
|
||||
// The original was part of a the application process to work for the university IT department
|
||||
// https://github.com/thomaspcole/p5QuicksortVisualization
|
||||
|
||||
let data = [];
|
||||
let animArray = [];
|
||||
let valuesToGenerate = 200;
|
||||
let frame = 0;
|
||||
|
||||
let parentContainer;
|
||||
let sketch;
|
||||
|
||||
//Added controls for play/pause and stepping frame by frame.
|
||||
let playpause = false;
|
||||
|
||||
onMount(() => {
|
||||
const sketchWidth = parentContainer.offsetWidth;
|
||||
const barWidth = (sketchWidth - 20) / valuesToGenerate;
|
||||
sketch = (p5) => {
|
||||
p5.setup = () => {
|
||||
for (let i = 0; i < valuesToGenerate; i++) {
|
||||
data[i] = i % 700;
|
||||
}
|
||||
|
||||
p5.shuffle(data, true);
|
||||
quicksort(data, 0, data.length - 1);
|
||||
animArray.push(new arrayFrame(data.toString(), null, null, null));
|
||||
|
||||
//setup the canvas
|
||||
p5.createCanvas(sketchWidth, 250);
|
||||
p5.frameRate(30);
|
||||
};
|
||||
|
||||
p5.draw = () => {
|
||||
if (playpause) {
|
||||
if (frame == animArray.length - 1) {
|
||||
playpause = !playpause;
|
||||
} else {
|
||||
drawFrame(frame);
|
||||
frame++;
|
||||
}
|
||||
} else {
|
||||
drawFrame(frame);
|
||||
}
|
||||
};
|
||||
|
||||
function drawFrame(index) {
|
||||
let dataFrame = animArray[index];
|
||||
p5.clear();
|
||||
for (var i = 0; i < valuesToGenerate; i++) {
|
||||
p5.fill(255);
|
||||
if (dataFrame.midIndex == i) {
|
||||
p5.fill(255, 0, 0);
|
||||
}
|
||||
if (dataFrame.lowVal == i || dataFrame.highVal == i) {
|
||||
p5.fill(0, 255, 0);
|
||||
}
|
||||
p5.rect(barWidth * i + 10, 230, barWidth, -dataFrame.getArray()[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Copied directly from original code
|
||||
* @param array the array of data being worked on
|
||||
* @param low the low values
|
||||
* @param high the high value
|
||||
*/
|
||||
function quicksort(array, low, high) {
|
||||
animArray.push(new arrayFrame(array.toString(), 0, array.length, 0));
|
||||
//Do we need to sort?
|
||||
if (low >= high) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Pick a pivot point in the middle of the passed array.
|
||||
let mid = Math.floor(low + (high - low) / 2);
|
||||
let pivot = array[mid];
|
||||
|
||||
let l = low;
|
||||
let h = high;
|
||||
|
||||
while (l <= h) {
|
||||
while (array[l] < pivot) {
|
||||
l++;
|
||||
}
|
||||
|
||||
while (array[h] > pivot) {
|
||||
h--;
|
||||
}
|
||||
|
||||
if (l <= h) {
|
||||
//console.log(array, l, h, mid);
|
||||
//console.log("Switch: " + array[l] + ":" + array[h]);
|
||||
animArray.push(new arrayFrame(array.toString(), l, h, mid));
|
||||
|
||||
let tmp = array[l];
|
||||
array[l] = array[h];
|
||||
array[h] = tmp;
|
||||
l++;
|
||||
h--;
|
||||
}
|
||||
}
|
||||
|
||||
if (low < h) {
|
||||
quicksort(array, low, h);
|
||||
}
|
||||
|
||||
if (high > l) {
|
||||
quicksort(array, l, high);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a arrayFrame object
|
||||
* @param arrayState the array of data at a current point in time
|
||||
* @param lowVal the low index
|
||||
* @param highVal the high midIndex
|
||||
* @param midIndex the pivot index
|
||||
* @param getArray Returns the array at the current time
|
||||
*/
|
||||
function arrayFrame(arrayState, lowVal, highVal, midIndex) {
|
||||
this.arrayState = arrayState;
|
||||
this.lowVal = lowVal;
|
||||
this.highVal = highVal;
|
||||
this.midIndex = midIndex;
|
||||
|
||||
this.getArray = function () {
|
||||
return this.arrayState.split(",");
|
||||
};
|
||||
}
|
||||
|
||||
function prevFrame() {
|
||||
if (frame == 0) {
|
||||
return;
|
||||
}
|
||||
frame--;
|
||||
}
|
||||
|
||||
function nextFrame() {
|
||||
if (frame == animArray.length - 1) {
|
||||
return;
|
||||
}
|
||||
frame++;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
frame = 0;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div bind:this={parentContainer} class="w-[90%] mx-auto rounded-xl border border-white">
|
||||
<P5 {sketch} />
|
||||
|
||||
<div class="flex items-center justify-center gap-8 rounded-xl pb-4">
|
||||
<button on:click={prevFrame} class="h-[24px]">
|
||||
<span class="material-symbols-outlined">skip_previous</span>
|
||||
</button>
|
||||
<button
|
||||
on:click={() => {
|
||||
if (frame == animArray.length - 1) {
|
||||
reset();
|
||||
} else {
|
||||
playpause = !playpause;
|
||||
}
|
||||
}}
|
||||
class="h-[24px]"
|
||||
>
|
||||
{#if playpause}
|
||||
<span class="material-symbols-outlined">pause</span>
|
||||
{:else if frame == animArray.length - 1}
|
||||
<span class="material-symbols-outlined">restart_alt</span>
|
||||
{:else}
|
||||
<span class="material-symbols-outlined">play_arrow</span>
|
||||
{/if}
|
||||
</button>
|
||||
<button on:click={nextFrame} class="h-[24px]">
|
||||
<span class="material-symbols-outlined">skip_next</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
10
src/pages/projects/resume-json.mdx
Normal file
10
src/pages/projects/resume-json.mdx
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
layout: "../../layouts/InteractiveDemoLayout.astro"
|
||||
title: "Building a Better Resume"
|
||||
tagline: "Using JSON Resume to make a resume"
|
||||
date: "August 06, 2023"
|
||||
category: "Software"
|
||||
image: "https://images.unsplash.com/photo-1602407294553-6ac9170b3ed0?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1548&q=80"
|
||||
imageattr: "https://unsplash.com/photos/4YzrcDNcRVg"
|
||||
active: false
|
||||
---
|
88
src/pages/projects/sitemap/sitemapgraph.mdx
Normal file
88
src/pages/projects/sitemap/sitemapgraph.mdx
Normal file
@ -0,0 +1,88 @@
|
||||
---
|
||||
layout: "../../../layouts/InteractiveDemoLayout.astro"
|
||||
title: "Visual Sitemap"
|
||||
tagline: "Using Cytoscape.js to visualize all the pages of a website"
|
||||
date: "January 6, 2023"
|
||||
category: "Demo"
|
||||
image: "https://images.unsplash.com/photo-1639322537228-f710d846310a?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1632&q=80"
|
||||
imageattr: "https://unsplash.com/photos/T9rKvI3N0NM"
|
||||
active: true
|
||||
---
|
||||
import Sitemapgraph from './sitemapgraph.svelte'
|
||||
|
||||
### Background
|
||||
|
||||
I was given a project at work to help improve the SEO of our organization's website. We quickly realized that we did not have a great overview of everything on the website. This began the search for a tool to help visualize all the pages on the website. While there are lots of expensive SEO tools we struggled to find something that would just give us a hierarchical view of pages. So if the tool doesn't exist, we need to make the tool.
|
||||
|
||||
### Designing the tool
|
||||
|
||||
![xkcd](https://imgs.xkcd.com/comics/manuals.png)
|
||||
|
||||
I set out to make a generic site mapper that could take a url and crawl all other pages on the site. I wanted it to have a simple command line interface, with the ability to save results for later viewing, and be able to spin up an integrated web server for the visualization. I also took the opportunity to write it all in Typescript, and I mean really use it not just write Javascript in a .ts file.
|
||||
|
||||
The command line interface and saving a file to disk are straight forward enough. The node fs module lets us read and write to the host filesystem and [Yargs](http://yargs.js.org/) can handle the command line stuff for us. Even crawling a website wasn't too difficult. The [sitemap-generator](https://www.npmjs.com/package/sitemap-generator) project handles all the heavy lifting. The generator creates a crawler object that keeps track of every page it visits in a queue. By looking at that queue after the crawler finishes we can get json objects of every page visited and importantly for us the page that linked to it. Finally, [Express](https://expressjs.com/) framework will handle the web server bits for the visualization.
|
||||
|
||||
### Putting it all together
|
||||
|
||||
So we have a great big array of json objects that have a url and a referrer among other things. How do we take this data and turn it into something usable?
|
||||
|
||||
***Graphs***
|
||||
|
||||
My CS200 professor can rest easy, I was paying attention and did learn something. Side note: for the sake of this app, we made the decision to not care about back links. While this doesn't change the implementation of our graph it does simplify displaying the graph as we don't have to worry about loops. On to the code.
|
||||
|
||||
```typescript
|
||||
class Vertex {
|
||||
name: string;
|
||||
ajd: Array<Edge>;
|
||||
|
||||
constructor(name: string) {
|
||||
this.name = name;
|
||||
this.ajd = new Array<Edge>();
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
class Edge {
|
||||
destination: Vertex;
|
||||
|
||||
constructor(vertex: Vertex) {
|
||||
this.destination = vertex;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each Vertex in the graph has a name and an array of edges. Each edge has a destination vertex. Easy enough. Now for the graph. We keep a map of vertex names and its corresponding vertex. If the vertex does not exist in the map we add it to the map and return the vertex. Adding an edge then becomes as simple as passing the name of the two vertices we want to connect. If both vertices don't exist they are created in the graph and we push the new edge to the source vertex adjacency array. Thats really it, there is some code omitted from the example below for printing the graph as well serializing the graph object so it can be saved to disk.
|
||||
|
||||
```typescript
|
||||
class Graph {
|
||||
vertices: Map<string, Vertex>;
|
||||
|
||||
constructor() {
|
||||
this.vertices = new Map<string, Vertex>();
|
||||
}
|
||||
|
||||
getVertex(name: string) {
|
||||
let v = this.vertices.get(name);
|
||||
if (v === undefined) {
|
||||
v = new Vertex(name);
|
||||
this.vertices.set(name, v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
addEdge(source: string, dest: string) {
|
||||
const v = this.getVertex(source);
|
||||
const w = this.getVertex(dest);
|
||||
v.ajd.push(new Edge(w));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now that we have a graph representation of our site we just need to convert it something a bit more visually appealing. [Cytoscape.js](https://js.cytoscape.org/) will handle that for us. I was tempted to use something like p5.js or D3 to handle the visualization but Cytoscape seemed better built for graph and network data structures. The translation from our graph data structure to Cytoscape is super easy, just pass in an array of vertices and edges.
|
||||
|
||||
Finally after all that crawling and parsing we are left with the desired result. A interactive hierarchical view of every page on a website. Is the tool perfect, no, but it was good enough for what we needed.
|
||||
|
||||
<Sitemapgraph client:load/>
|
109
src/pages/projects/sitemap/sitemapgraph.svelte
Normal file
109
src/pages/projects/sitemap/sitemapgraph.svelte
Normal file
@ -0,0 +1,109 @@
|
||||
<script>
|
||||
import cytoscape from 'cytoscape';
|
||||
import { onMount } from 'svelte';
|
||||
let div;
|
||||
let data = {"map":[{"name":"root","adj":[{"destination":{"name":"http://localhost:8000/","ajd":[{"destination":{"name":"http://localhost:8000/about.html","ajd":[]}},{"destination":{"name":"http://localhost:8000/blog/demo1.html","ajd":[]}},{"destination":{"name":"http://localhost:8000/blog/demo2.html","ajd":[]}}]}}]},{"name":"http://localhost:8000/","adj":[{"destination":{"name":"http://localhost:8000/about.html","ajd":[]}},{"destination":{"name":"http://localhost:8000/blog/demo1.html","ajd":[]}},{"destination":{"name":"http://localhost:8000/blog/demo2.html","ajd":[]}}]},{"name":"http://localhost:8000/about.html","adj":[]},{"name":"http://localhost:8000/blog/demo1.html","adj":[]},{"name":"http://localhost:8000/blog/demo2.html","adj":[]}]};
|
||||
let cyElements = [];
|
||||
|
||||
onMount(()=>{
|
||||
//load all vertexes into cytoscape
|
||||
data.map.forEach((element) => {
|
||||
let shortURL = element.name.replace(/^(?:\/\/|[^/]+)*\//, "/");
|
||||
cyElements.push({
|
||||
data: {
|
||||
id: element.name,
|
||||
name: element.name,
|
||||
short: shortURL,
|
||||
href: element.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
data.map.forEach((element) => {
|
||||
if (element.adj.length > 0) {
|
||||
for (let i = 0; i < element.adj.length; i++) {
|
||||
const e = element.adj[i];
|
||||
let cyid = element.name + "," + element.adj[i].destination.name;
|
||||
let cysrc = element.name;
|
||||
let cytgt = element.adj[i].destination.name;
|
||||
cyElements.push({
|
||||
data: {
|
||||
id: cyid,
|
||||
source: cysrc,
|
||||
target: cytgt,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cytoscape({
|
||||
container: div,
|
||||
elements: cyElements,
|
||||
style: [
|
||||
{
|
||||
selector: "node",
|
||||
style: {
|
||||
'content': "data(name)",
|
||||
'text-valign' : 'center',
|
||||
'shape': 'round-rectangle',
|
||||
'width': '500px',
|
||||
'height': '100px',
|
||||
'background-color' : '#F8F9FA',
|
||||
'border-color': '#000',
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "edge",
|
||||
style: {
|
||||
'line-color': "#fff"
|
||||
}
|
||||
}
|
||||
],
|
||||
userZoomingEnabled: false,
|
||||
userPanningEnabled: false,
|
||||
layout: {
|
||||
name: "breadthfirst",
|
||||
fit: true, // whether to fit the viewport to the graph
|
||||
directed: true, // whether the tree is directed downwards (or edges can point in any direction if false)
|
||||
padding: 5, // padding on fit
|
||||
circle: false, // put depths in concentric circles if true, put depths top down if false
|
||||
grid: false, // whether to create an even grid into which the DAG is placed (circle:false only)
|
||||
spacingFactor: 1, // positive spacing factor, larger => more space between nodes (N.B. n/a if causes overlap)
|
||||
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
|
||||
avoidOverlap: true, // prevents node overlap, may overflow boundingBox if not enough space
|
||||
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
|
||||
roots: cyElements[0], // the roots of the trees
|
||||
maximal: false, // whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)
|
||||
depthSort: undefined, // a sorting function to order nodes at equal depth. e.g. function(a, b){ return a.data('weight') - b.data('weight') }
|
||||
animate: false, // whether to transition the node positions
|
||||
animationDuration: 500, // duration of animation in ms if enabled
|
||||
animationEasing: undefined, // easing of animation if enabled,
|
||||
animateFilter: function (node, i) {
|
||||
return true;
|
||||
}, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
|
||||
ready: undefined, // callback on layoutready
|
||||
stop: undefined, // callback on layoutstop
|
||||
transform: function (node, position) {
|
||||
return position;
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
#cy {
|
||||
width: 100%;
|
||||
height: 75vh;
|
||||
display: block;
|
||||
background-color: #343A40;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div bind:this={div} id="cy">
|
||||
|
||||
</div>
|
7
src/pages/submitContact.js
Normal file
7
src/pages/submitContact.js
Normal file
@ -0,0 +1,7 @@
|
||||
export async function GET({ params, request }) {
|
||||
const response = await fetch("https://docs.astro.build/assets/full-logo-light.png");
|
||||
const buffer = Buffer.from(await response.arrayBuffer());
|
||||
return new Response(buffer, {
|
||||
headers: { "Content-Type": "image/png" },
|
||||
});
|
||||
}
|
@ -1 +0,0 @@
|
||||
<div class="my-6 h-0.5 mx-auto bg-neutral-50 drop-shadow-screen rounded"/>
|
@ -1,54 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
|
||||
let text:string;
|
||||
export let strings:string[];
|
||||
export let classVars:string;
|
||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+[]{},.<>/?";
|
||||
let stringIndex = 0;
|
||||
|
||||
function pickNextWord(){
|
||||
shuffleLetters(strings[stringIndex]);
|
||||
|
||||
if(stringIndex == strings.length-1){
|
||||
stringIndex = 0;
|
||||
} else {
|
||||
stringIndex ++;
|
||||
}
|
||||
}
|
||||
|
||||
//https://www.youtube.com/watch?v=W5oawMJaXbU
|
||||
//Hpyerplexed FTW
|
||||
function shuffleLetters(targetWord:string){
|
||||
let itterations = 0;
|
||||
const interval = setInterval(()=>{
|
||||
text = targetWord.split("").map((letter, index) => {
|
||||
if(index < itterations){
|
||||
return targetWord[index];
|
||||
}
|
||||
|
||||
return letters[Math.floor(Math.random()*letters.length)];
|
||||
}).join("");
|
||||
|
||||
if(itterations >= targetWord.length){
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
itterations ++;
|
||||
}, 40);
|
||||
}
|
||||
|
||||
onMount(()=>{
|
||||
stringIndex = Math.floor(Math.random()*strings.length)
|
||||
pickNextWord();
|
||||
|
||||
setInterval(pickNextWord,10000);
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<!-- <p class={classVars} on:mouseover={pickNextWord}> -->
|
||||
<p class={classVars}>
|
||||
{text}
|
||||
</p>
|
@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ProjectCard from "./ProjectCard.svelte";
|
||||
import Modal from "./Modal.svelte";
|
||||
|
||||
export let image:string;
|
||||
export let title:string;
|
||||
export let subtitle:string;
|
||||
let showModal = false;
|
||||
|
||||
function callback(){
|
||||
showModal = true;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<ProjectCard title={title} subtitle={subtitle} image={image} link={undefined} callback={callback}/>
|
||||
<Modal bind:showModal>
|
||||
<slot/>
|
||||
</Modal>
|
@ -1,55 +0,0 @@
|
||||
<script>
|
||||
//Modal from example on svelte website. https://svelte.dev/examples/modal
|
||||
export let showModal;
|
||||
let dialog;
|
||||
$: if (dialog && showModal) dialog.showModal();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<dialog
|
||||
bind:this={dialog}
|
||||
on:close={() => (showModal = false)}
|
||||
on:click|self={() => dialog.close()}
|
||||
autofocus={false}
|
||||
class="w-full lg:xl:w-5/6 rounded-lg bg-green-700/90 border-2 border-solid border-green-300/50 text-white"
|
||||
>
|
||||
<div on:click|stopPropagation>
|
||||
<slot />
|
||||
<hr />
|
||||
<div class="flex">
|
||||
<div class="grow"></div>
|
||||
<button class="pt-4" on:click={() => dialog.close()}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
dialog::backdrop {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
dialog[open] {
|
||||
animation: zoom 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
@keyframes zoom {
|
||||
from {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
dialog[open]::backdrop {
|
||||
animation: fade 0.2s ease-out;
|
||||
}
|
||||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let percentage: number | 0;
|
||||
export let color: string | "#FFFFFF";
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="progress left rounded-md"
|
||||
style="background-color: {color}; clip-path: polygon(0% 0%, 0% 100%, {percentage}% 100%, {percentage}% 0%);"
|
||||
/>
|
||||
|
||||
<style>
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transition: 0.4s clip-path;
|
||||
clip-path: polygon(0% 0%, 0% 100%, 0% 100%, 0% 0%);
|
||||
}
|
||||
</style>
|
@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let link:string;
|
||||
export let image:string;
|
||||
export let title:string;
|
||||
export let subtitle:string;
|
||||
export let callback;
|
||||
|
||||
let url;
|
||||
if(typeof link === undefined){
|
||||
url = "javascript:void(0)"
|
||||
} else {
|
||||
url = link;
|
||||
}
|
||||
|
||||
if(typeof callback === undefined){
|
||||
callback = ()=>{};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-lg transition ease-in-out hover:scale-[101%]">
|
||||
<a href={url} on:click={callback}>
|
||||
<div class="ml-2 my-2 flex flex-col sm:flex-col md:flex-col lg:flex-row xl:flex-row 2xl:flex-row w-11/12 mx-auto">
|
||||
<img class="h-28 my-2 aspect-video object-cover rounded-lg" src={image} alt="">
|
||||
<div class="lg:ml-4 xl:ml-4 2xl:ml-4 grow">
|
||||
<p class="mt-2 font-semibold text-2xl py-2 sm:text-2xl md:text-2xl lg:text-3xl xl:text-3xl 2xl:text-3xl">{title}</p>
|
||||
<p>{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let title: string;
|
||||
export let subtitle: string;
|
||||
</script>
|
||||
|
||||
<div class="px-5">
|
||||
<p class="text-xl font-bold">{title}</p>
|
||||
<p class="">{subtitle}</p>
|
||||
<hr />
|
||||
<slot />
|
||||
</div>
|
2
src/vite-env.d.ts
vendored
2
src/vite-env.d.ts
vendored
@ -1,2 +0,0 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
@ -1,7 +1,5 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
import { vitePreprocess } from '@astrojs/svelte';
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
||||
preprocess: vitePreprocess(),
|
||||
};
|
||||
|
@ -1,20 +1,42 @@
|
||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx,svelte,html}"
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily:{
|
||||
sans: ['Montserrat', defaultTheme.fontFamily.sans],
|
||||
mono: ['Share Tech Mono', defaultTheme.fontFamily.mono]
|
||||
}
|
||||
},
|
||||
dropShadow: {
|
||||
'screen':'0 0 2px rgb(134,239,172)'
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#294861',
|
||||
},
|
||||
fontFamily: {
|
||||
'sans': ['Open Sans', defaultTheme.fontFamily.sans],
|
||||
},
|
||||
keyframes: {
|
||||
wiggle: {
|
||||
'0%, 100%': { transform: 'rotate(-5deg)' },
|
||||
'50%': { transform: 'rotate(5deg)' },
|
||||
},
|
||||
text: {
|
||||
'0%, 100%': {
|
||||
'background-size': '200% 200%',
|
||||
'background-position': 'left center',
|
||||
},
|
||||
'50%': {
|
||||
'background-size': '200% 200%',
|
||||
'background-position': 'right center',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'wiggle-infinite': 'wiggle 1s ease-in-out infinite',
|
||||
wiggle: 'wiggle 1s ease-in-out',
|
||||
'text': 'text 15s ease infinite'
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("daisyui"),
|
||||
require('@tailwindcss/typography')
|
||||
],
|
||||
}
|
||||
|
@ -1,20 +1,3 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": ["./**/*.d.ts", "./**/*.ts", "./**/*.js", "./**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node"
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
})
|
Loading…
Reference in New Issue
Block a user