Added new web app version.
This commit is contained in:
parent
9075938d43
commit
62bc2857d0
|
@ -0,0 +1,15 @@
|
||||||
|
/* eslint-env node */
|
||||||
|
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
'extends': [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-typescript',
|
||||||
|
'@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
*.tsbuildinfo
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"semi": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
# app
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Type Support for `.vue` Imports in TS
|
||||||
|
|
||||||
|
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type-Check, Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Andrew Lalis</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"build-only": "vite build",
|
||||||
|
"type-check": "vue-tsc --build --force",
|
||||||
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"vue": "^3.4.29",
|
||||||
|
"vue-router": "^4.3.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rushstack/eslint-patch": "^1.8.0",
|
||||||
|
"@tsconfig/node20": "^20.1.4",
|
||||||
|
"@types/node": "^20.14.5",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
|
"@vue/eslint-config-typescript": "^13.0.0",
|
||||||
|
"@vue/tsconfig": "^0.5.1",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-vue": "^9.23.0",
|
||||||
|
"npm-run-all2": "^6.2.0",
|
||||||
|
"prettier": "^3.2.5",
|
||||||
|
"typescript": "~5.4.0",
|
||||||
|
"vite": "^5.3.1",
|
||||||
|
"vue-tsc": "^2.0.21"
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,82 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
import NavLink from './components/NavLink.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="bisection-container">
|
||||||
|
<header class="page-bisection">
|
||||||
|
<h1 class="site-header-text">Andrew Lalis</h1>
|
||||||
|
<nav>
|
||||||
|
<NavLink path="/">Home</NavLink>
|
||||||
|
<NavLink path="/about">About</NavLink>
|
||||||
|
<NavLink path="/software">Software</NavLink>
|
||||||
|
<NavLink path="/gardening">Gardening</NavLink>
|
||||||
|
<NavLink path="/logbook">Logbook</NavLink>
|
||||||
|
<NavLink path="/contact">Contact</NavLink>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<RouterView v-slot="{ Component }">
|
||||||
|
<Transition name="page">
|
||||||
|
<Component :is="Component" class="page-bisection content-page" />
|
||||||
|
</Transition>
|
||||||
|
</RouterView>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bisection-container {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.page-bisection {
|
||||||
|
width: 50ch;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.content-page {
|
||||||
|
text-align: left;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header-text {
|
||||||
|
font-family: Sacramento;
|
||||||
|
font-size: 60px;
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Activates once the left and right half of the page stack due to width constraints.
|
||||||
|
We remove a lot of the vertical spacing to make things fit better.
|
||||||
|
*/
|
||||||
|
@media (width < calc(100ch - 2.5rem)) {
|
||||||
|
.site-header-text {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.content-page {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Once the device width is smaller than one of the halves, sync the page half to the device width.
|
||||||
|
*/
|
||||||
|
@media (width < 50ch) {
|
||||||
|
.page-bisection {
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter-active {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: Sacramento;
|
||||||
|
src: url('@/assets/fonts/Sacramento-Regular.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: BaskervvilleSC;
|
||||||
|
src: url('@/assets/fonts/BaskervvilleSC-Regular.ttf');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: OpenSans;
|
||||||
|
src: url('@/assets/fonts/OpenSans-VariableFont_wdth,wght.ttf');
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: OpenSans;
|
||||||
|
src: url('@/assets/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf');
|
||||||
|
font-style: italic;
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,47 @@
|
||||||
|
@import '@/assets/fonts.css';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--text-color: rgb(224, 224, 224);
|
||||||
|
--background-color: rgb(17, 17, 17);
|
||||||
|
--background-color-2: rgb(34, 34, 34);
|
||||||
|
--code-color: rgb(224, 226, 120);
|
||||||
|
--success-color: rgb(118, 201, 118);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: OpenSans;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-family: BaskervvilleSC;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
color: var(--code-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-local {
|
||||||
|
color: var(--success-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.link-local:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-out {
|
||||||
|
color: var(--code-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.link-out:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Message } from '@/views/LogBookView.vue'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
message: Message
|
||||||
|
}>()
|
||||||
|
const date = computed(() => {
|
||||||
|
const b = props.message.createdAt.split(/\D+/)
|
||||||
|
const offsetMult = props.message.createdAt.indexOf('+') !== -1 ? -1 : 1
|
||||||
|
const hrOffset = offsetMult * (+b[7] || 0)
|
||||||
|
const minOffset = offsetMult * (+b[8] || 0)
|
||||||
|
return new Date(
|
||||||
|
Date.UTC(+b[0], +b[1] - 1, +b[2], +b[3] + hrOffset, +b[4] + minOffset, +b[5], +b[6] || 0)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<div class="logbook-message">
|
||||||
|
<p v-text="message.message"></p>
|
||||||
|
<div>
|
||||||
|
<small>sent by <span v-text="message.name" style="color: var(--code-color)"></span></small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<small><time :datetime="date.toISOString()" v-text="date.toLocaleString()"></time></small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.logbook-message {
|
||||||
|
background-color: var(--background-color-2);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
.logbook-message + .logbook-message {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
.logbook-message > time {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
.logbook-message > p {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
defineProps<{
|
||||||
|
path: string
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="nav-link">
|
||||||
|
<RouterLink :to="path" v-bind:class="route.path === path ? 'nav-link-active' : ''">
|
||||||
|
<slot></slot>
|
||||||
|
</RouterLink>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.nav-link {
|
||||||
|
text-align: right;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link > a {
|
||||||
|
font-family: BaskervvilleSC;
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: color 0.25s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link-active {
|
||||||
|
color: var(--success-color) !important;
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link > a:hover {
|
||||||
|
color: var(--success-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Activates once the left and right half of the page stack due to width constraints.
|
||||||
|
We remove a lot of the vertical spacing to make things fit better.
|
||||||
|
*/
|
||||||
|
@media (width < calc(100ch - 2.5rem)) {
|
||||||
|
.nav-link {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<template>
|
||||||
|
<p class="note">
|
||||||
|
<slot></slot>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.note {
|
||||||
|
font-size: small;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
tags?: string[]
|
||||||
|
}>()
|
||||||
|
const sortedTags = computed(() => (!props.tags ? null : props.tags.slice().sort()))
|
||||||
|
|
||||||
|
const COMMON_TAGS: Record<string, string[]> = {
|
||||||
|
Java: ['white', '#e76f00'],
|
||||||
|
Vue: ['white', '#41b883'],
|
||||||
|
Dlang: ['white', '#b03931'],
|
||||||
|
Typescript: ['white', '#3178c6'],
|
||||||
|
Finance: ['white', '#0e4f0a'],
|
||||||
|
Spring: ['white', '#6db33f']
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTagStyle(tag: string) {
|
||||||
|
if (tag in COMMON_TAGS) {
|
||||||
|
return { color: COMMON_TAGS[tag][0], 'background-color': COMMON_TAGS[tag][1] }
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
color: 'white',
|
||||||
|
'background-color': 'gray'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="software-project-tile">
|
||||||
|
<h3><a :href="url" v-text="name" class="link-out"></a></h3>
|
||||||
|
<div class="software-project-tile-content">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<div v-if="tags">
|
||||||
|
<span
|
||||||
|
v-for="tag in sortedTags"
|
||||||
|
:key="tag"
|
||||||
|
v-text="tag"
|
||||||
|
class="software-project-tile-tag-badge"
|
||||||
|
:style="getTagStyle(tag)"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.software-project-tile {
|
||||||
|
background-color: var(--background-color-2);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.software-project-tile + .software-project-tile {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.software-project-tile > h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: x-large;
|
||||||
|
}
|
||||||
|
|
||||||
|
.software-project-tile-content > p {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.software-project-tile-tag-badge {
|
||||||
|
font-size: 12px;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
padding-top: 0.1rem;
|
||||||
|
padding-bottom: 0.1rem;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,14 @@
|
||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
import AboutView from '@/views/AboutView.vue'
|
||||||
|
import ContactView from '@/views/ContactView.vue'
|
||||||
|
import SoftwareView from '@/views/SoftwareView.vue'
|
||||||
|
import GardeningView from '@/views/GardeningView.vue'
|
||||||
|
import LogBookView from '@/views/LogBookView.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
component: HomeView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/about',
|
||||||
|
component: AboutView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/contact',
|
||||||
|
component: ContactView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/software',
|
||||||
|
component: SoftwareView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/gardening',
|
||||||
|
component: GardeningView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/logbook',
|
||||||
|
component: LogBookView
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<p>
|
||||||
|
This page was designed and programmed by myself, Andrew, to be my personal presence on the
|
||||||
|
world-wide-web. I use it as my professional portfolio, in addition to a repository for
|
||||||
|
anything random that interests me and might be interesting for others too.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Feel free to look around, and
|
||||||
|
<RouterLink to="/contact" class="link-local">contact me</RouterLink> if you find anything
|
||||||
|
broken or have any questions about anything you find. Maybe say hello in my
|
||||||
|
<RouterLink to="/logbook" class="link-local">logbook</RouterLink> while you're at it?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>About This Site, Technically</h3>
|
||||||
|
<p>
|
||||||
|
You can find the source code for this website at
|
||||||
|
<a href="https://git.andrewlalis.com/andrew/homepage" class="link-out">
|
||||||
|
git.andrewlalis.com/andrew/homepage </a
|
||||||
|
>. It's built using <a href="https://vuejs.org/" class="link-out">Vue</a>, and styled
|
||||||
|
completely by hand using plain-old CSS.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The site is hosted on a small DigitalOcean "droplet" (their branding for a Virtual Private
|
||||||
|
Server), and is served through Nginx and various firewalls.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you actually go and view the source code, you'll see a <code>deploy.sh</code> file.
|
||||||
|
Essentially, all I do is build the site locally, then upload the files. Pretty simple, right?
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style></style>
|
|
@ -0,0 +1,35 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const emailText = ref('')
|
||||||
|
const emailCorrect = computed(() => {
|
||||||
|
const testStr = atob('YW5kcmV3bGFsaXNvZmZpY2lhbEBnbWFpbC5jb20=')
|
||||||
|
return emailText.value.trim().toLowerCase() === testStr
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<p>
|
||||||
|
My preferred method of communication is email, which I usually check daily. Send to
|
||||||
|
<code>first-name + last-name + "official" at gmail dot com</code>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
See if you can figure it out:
|
||||||
|
<input class="contactEmailInput" type="text" v-model="emailText" />
|
||||||
|
<span v-if="emailCorrect">
|
||||||
|
<br />
|
||||||
|
<small style="color: var(--success-color)"><em>You got it!</em></small>
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<small><em>I write it like that to avoid getting spammed by bots.</em></small>
|
||||||
|
</p>
|
||||||
|
<p>My Discord username is <code>____andrew____</code></p>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.contactEmailInput {
|
||||||
|
font-family: OpenSans, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
width: calc(min(30ch, 100%));
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<p><em>This page is a work in progress. Please check back later.</em></p>
|
||||||
|
</main>
|
||||||
|
</template>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<p>
|
||||||
|
My name's Andrew, and I'm a software engineer, gardener, runner, home cook, and probably many
|
||||||
|
other things too, depending on who you ask. Welcome to my website!
|
||||||
|
</p>
|
||||||
|
<p>Click one of the links on the left to check out something more interesting.</p>
|
||||||
|
</main>
|
||||||
|
</template>
|
|
@ -0,0 +1,159 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import LogbookMessage from '@/components/LogbookMessage.vue'
|
||||||
|
import NoteText from '@/components/NoteText.vue'
|
||||||
|
import { onMounted, ref, type Ref } from 'vue'
|
||||||
|
|
||||||
|
const LOGBOOK_URL = 'https://logbook.andrewlalis.com'
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
createdAt: string
|
||||||
|
name: string
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages: Ref<Message[]> = ref([])
|
||||||
|
const addingMessage = ref(false)
|
||||||
|
const messageSubmitted = ref(false)
|
||||||
|
const formName = ref('')
|
||||||
|
const formMessage = ref('')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
messages.value = await getMessages()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function getMessages(): Promise<Message[]> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(LOGBOOK_URL)
|
||||||
|
if (response.ok) {
|
||||||
|
return (await response.json()) as Message[]
|
||||||
|
} else {
|
||||||
|
console.warn('Failed to get logbook messages.', response.status)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send logbook request.', error)
|
||||||
|
}
|
||||||
|
return [] // In case of error, always return an empty list.
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitMessage() {
|
||||||
|
const data = {
|
||||||
|
name: formName.value,
|
||||||
|
message: formMessage.value
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(LOGBOOK_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
if (response.ok) {
|
||||||
|
messages.value = await getMessages()
|
||||||
|
formName.value = ''
|
||||||
|
formMessage.value = ''
|
||||||
|
addingMessage.value = false
|
||||||
|
messageSubmitted.value = true
|
||||||
|
} else {
|
||||||
|
console.warn('Log message rejected.', response.status)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send log message.', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<p>
|
||||||
|
If you'd like, you can leave a message in my online logbook so that I and others might see it.
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
v-if="!messageSubmitted"
|
||||||
|
@click.prevent="addingMessage = !addingMessage"
|
||||||
|
class="button-link"
|
||||||
|
>Add your message.</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<form v-if="addingMessage" class="message-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="nameField">Name</label>
|
||||||
|
<input
|
||||||
|
id="nameField"
|
||||||
|
class="message-form-input"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
minlength="2"
|
||||||
|
maxlength="32"
|
||||||
|
required
|
||||||
|
v-model="formName"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="messageField">Message</label>
|
||||||
|
<textarea
|
||||||
|
id="messageField"
|
||||||
|
class="message-form-input"
|
||||||
|
type="text"
|
||||||
|
name="message"
|
||||||
|
minlength="2"
|
||||||
|
maxlength="255"
|
||||||
|
required
|
||||||
|
v-model="formMessage"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<a href="#" @click.prevent="submitMessage()" class="button-link">Submit your message!</a>
|
||||||
|
</div>
|
||||||
|
<NoteText>
|
||||||
|
Note that I do check all logs and some basic information about who sent them (IP address,
|
||||||
|
browser, etc). Inappropriate comments will be removed and their authors banned.
|
||||||
|
</NoteText>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<TransitionGroup name="messages" tag="div">
|
||||||
|
<LogbookMessage v-for="msg in messages" :key="msg.createdAt" :message="msg" />
|
||||||
|
</TransitionGroup>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
.button-link {
|
||||||
|
color: var(--success-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.button-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.message-form {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.form-row {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.form-row > label {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.message-form-input {
|
||||||
|
font-family: OpenSans;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
#messageField {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-enter-active,
|
||||||
|
.messages-leave-active {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages-enter-from,
|
||||||
|
.messages-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(30px);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,98 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SoftwareProjectTile from '@/components/SoftwareProjectTile.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main>
|
||||||
|
<p>
|
||||||
|
Here you'll find a selection of projects I've worked on over the course of my software
|
||||||
|
engineering career.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Beside the projects listed here, you can view my complete portfolio on
|
||||||
|
<a href="https://git.andrewlalis.com" class="link-out">git.andrewlalis.com</a>. I also have a
|
||||||
|
<a href="https://github.com/andrewlalis" class="link-out">GitHub</a> account, but I only use
|
||||||
|
it for collaboration and a projects that rely on GitHub's API integration.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<SoftwareProjectTile
|
||||||
|
name="Perfin"
|
||||||
|
url="https://git.andrewlalis.com/andrew/perfin"
|
||||||
|
:tags="['Java', 'JavaFX', 'Finance']"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
A desktop personal finance application that makes it easy to manage your accounts and
|
||||||
|
transactions in a secure manner. Record transactions, track brokerage assets, and view
|
||||||
|
graphs of your spending, total value, and more.
|
||||||
|
</p>
|
||||||
|
</SoftwareProjectTile>
|
||||||
|
<SoftwareProjectTile
|
||||||
|
name="Guerilla Gardening Map"
|
||||||
|
url="https://guerilla-gardening.andrewlalis.com"
|
||||||
|
:tags="['Dlang', 'Vue', 'Typescript']"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
A website for anonymously tracking and sharing information on
|
||||||
|
<a href="https://en.wikipedia.org/wiki/Guerrilla_gardening" class="link-out"
|
||||||
|
>Guerilla-Gardening</a
|
||||||
|
>
|
||||||
|
locations around the world.
|
||||||
|
</p>
|
||||||
|
</SoftwareProjectTile>
|
||||||
|
<SoftwareProjectTile
|
||||||
|
name="LiteList"
|
||||||
|
url="https://litelist.andrewlalis.com"
|
||||||
|
:tags="['Dlang', 'Vue', 'Typescript']"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
An extremely barebones web application to demonstrate the viability of my own HTTP server.
|
||||||
|
It's just your average ToDo list app, with basic JWT-based authentication.
|
||||||
|
</p>
|
||||||
|
</SoftwareProjectTile>
|
||||||
|
<SoftwareProjectTile
|
||||||
|
name="Handy-Http"
|
||||||
|
url="https://github.com/andrewlalis/handy-httpd"
|
||||||
|
:tags="['Dlang']"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
An lightweight and flexible HTTP server implemented in the D programming language. Supports
|
||||||
|
websockets, file serving, complex URL paths with path variables, and more!
|
||||||
|
</p>
|
||||||
|
</SoftwareProjectTile>
|
||||||
|
<SoftwareProjectTile
|
||||||
|
name="JavaFX Scene Router"
|
||||||
|
url="https://git.andrewlalis.com/andrew/javafx-scene-router"
|
||||||
|
:tags="['Java', 'JavaFX']"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
A <em>router</em> implementation for JavaFX desktop applications. Define a singleton
|
||||||
|
<code>SceneRouter</code>, add some routes to it, and add the router's provided view
|
||||||
|
component to your scene graph. Now you can navigate to different "pages" programmatically or
|
||||||
|
with user-interaction.
|
||||||
|
</p>
|
||||||
|
</SoftwareProjectTile>
|
||||||
|
<SoftwareProjectTile
|
||||||
|
name="Gymboard"
|
||||||
|
url="https://git.andrewlalis.com/andrew/Gymboard"
|
||||||
|
:tags="['Java', 'Spring', 'Vue', 'Typescript', 'Dlang']"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Proof-of-concept for a local gym leaderboard web application, where users post videos of
|
||||||
|
their lifts to climb to the top of various leaderboards. Built using a
|
||||||
|
<em>modular-monolith</em> software architecture.
|
||||||
|
</p>
|
||||||
|
</SoftwareProjectTile>
|
||||||
|
<SoftwareProjectTile
|
||||||
|
name="Streams"
|
||||||
|
url="https://github.com/andrewlalis/streams"
|
||||||
|
:tags="['Dlang']"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
A library that defines useful stream primitive types and implementations for IO, which
|
||||||
|
serves as a convenient wrapper around platform-specific reading and writing operations for
|
||||||
|
files, sockets, and more.
|
||||||
|
</p>
|
||||||
|
</SoftwareProjectTile>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
<style></style>
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||||
|
"exclude": ["src/**/__tests__/*"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"extends": "@tsconfig/node20/tsconfig.json",
|
||||||
|
"include": [
|
||||||
|
"vite.config.*",
|
||||||
|
"vitest.config.*",
|
||||||
|
"cypress.config.*",
|
||||||
|
"nightwatch.conf.*",
|
||||||
|
"playwright.config.*"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"types": ["node"]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { fileURLToPath, URL } from 'node:url'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
Reference in New Issue