New business card page
Build Website / Build NPM Project (push) Failing after 26s
Build Website / Tag Version (push) Skipped
Build Website / Build Container (push) Failing after 28s
Build Website / Deploy Container (push) Skipped

This commit is contained in:
2026-10-14 15:42:26 -04:00
parent 5a5718abe4
commit 5ed028d00c
8 changed files with 774 additions and 27 deletions
+306
View File
@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@ztimson/momentum": "^1.5.4", "@ztimson/momentum": "^1.5.4",
"@ztimson/utils": "^0.30.2", "@ztimson/utils": "^0.30.2",
"qrcode": "^1.5.4",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
@@ -1369,6 +1370,15 @@
"var-persist": "^1.0.1" "var-persist": "^1.0.1"
} }
}, },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": { "node_modules/ansi-styles": {
"version": "6.2.3", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
@@ -1419,6 +1429,15 @@
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
}, },
"node_modules/camelcase": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -1435,6 +1454,35 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/cliui": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^6.2.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/computeds": { "node_modules/computeds": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
@@ -1486,6 +1534,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1497,6 +1554,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dijkstrajs": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
"integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
"license": "MIT"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/entities": { "node_modules/entities": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -1554,6 +1623,19 @@
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/find-up": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
"license": "MIT",
"dependencies": {
"locate-path": "^5.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1569,6 +1651,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/he": { "node_modules/he": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
@@ -1597,6 +1688,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-glob": { "node_modules/is-glob": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -1628,6 +1728,18 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/locate-path": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
"license": "MIT",
"dependencies": {
"p-locate": "^4.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/magic-string": { "node_modules/magic-string": {
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -1732,6 +1844,42 @@
"npm": ">= 8" "npm": ">= 8"
} }
}, },
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"license": "MIT",
"dependencies": {
"p-try": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
"license": "MIT",
"dependencies": {
"p-limit": "^2.2.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-try": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/path-browserify": { "node_modules/path-browserify": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
@@ -1739,6 +1887,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -1782,6 +1939,15 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/pngjs": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
"integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.13", "version": "8.5.13",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz",
@@ -1810,6 +1976,23 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/qrcode": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
"integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
"license": "MIT",
"dependencies": {
"dijkstrajs": "^1.0.1",
"pngjs": "^5.0.0",
"yargs": "^15.3.1"
},
"bin": {
"qrcode": "bin/qrcode"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/read-package-json-fast": { "node_modules/read-package-json-fast": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz",
@@ -1838,6 +2021,21 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"license": "ISC"
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.60.2", "version": "4.60.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
@@ -1917,6 +2115,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
"license": "ISC"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -1962,6 +2166,32 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.3.3", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
@@ -2129,6 +2359,82 @@
"engines": { "engines": {
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
"license": "ISC"
},
"node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "15.4.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
"license": "MIT",
"dependencies": {
"cliui": "^6.0.0",
"decamelize": "^1.2.0",
"find-up": "^4.1.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^4.2.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^18.1.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/yargs-parser": {
"version": "18.1.3",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
"license": "ISC",
"dependencies": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
},
"engines": {
"node": ">=6"
}
} }
} }
} }
+1
View File
@@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"@ztimson/momentum": "^1.5.4", "@ztimson/momentum": "^1.5.4",
"@ztimson/utils": "^0.30.2", "@ztimson/utils": "^0.30.2",
"qrcode": "^1.5.4",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
}, },
+1 -24
View File
@@ -1,31 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import Foot from '@/components/foot.vue';
import Profile from '@/components/profile.vue';
import {momentum} from '@/services/momentum.service'; import {momentum} from '@/services/momentum.service';
momentum.client.inject(); momentum.client.inject();
</script> </script>
<template> <template>
<!-- Spacer --> <router-view />
<div class="w-100" style="height: min(75vh, 500px)"></div>
<!-- Content -->
<div class="cap-width mb-3 bg-white">
<!-- Header -->
<header class="px-4 d-flex justify-content-center justify-content-sm-start" style="background: #732222">
<profile style="transform: translateY(-33%)" />
</header>
<!-- Body -->
<main class="p-3">
<router-view></router-view>
</main>
<!-- Footer -->
<foot />
</div>
<!-- Spacer -->
<div class="d-none d-lg-block w-100" style="height: 40px"></div>
</template> </template>
+1 -1
View File
@@ -17,7 +17,7 @@ import Icon from '@/components/icon.vue';
<div class="me-sm-5"> <div class="me-sm-5">
<h1 class="m-0" style="font-size: 2.5rem">Zakary Timson</h1> <h1 class="m-0" style="font-size: 2.5rem">Zakary Timson</h1>
<h2 class="mt-0 text-muted" style="font-size: 1.25rem"> <h2 class="mt-0 text-muted" style="font-size: 1.25rem">
AI SOFTWARE ENGINEER SOFTWARE ENGINEER
</h2> </h2>
<ul class="m-0 p-0 text-start" style="list-style: none"> <ul class="m-0 p-0 text-start" style="list-style: none">
<li><icon name="map-marker-alt" class="me-1"/> London Ontario, Canada</li> <li><icon name="map-marker-alt" class="me-1"/> London Ontario, Canada</li>
+1 -1
View File
@@ -1,7 +1,7 @@
import './main.scss' import './main.scss'
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './routes'
const app = createApp(App) const app = createApp(App)
+4 -1
View File
@@ -1,10 +1,13 @@
import BusinessCard from '@/views/BusinessCard.vue';
import Layout from '@/views/Layout.vue';
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue' import Home from '@/views/Home.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory((<any>import.meta).env.BASE_URL), history: createWebHistory((<any>import.meta).env.BASE_URL),
routes: [ routes: [
{path: '/', name: 'home', component: Home} {path: '/', component: Layout, children: [{path: '', component: Home}]},
{path: '/card', name: 'home', component: BusinessCard}
] ]
}) })
+432
View File
@@ -0,0 +1,432 @@
<script setup lang="ts">
import Icon from '@/components/icon.vue';
import QRCode from 'qrcode';
import {computed, onMounted, ref} from 'vue';
// ---------- Your info (edit here) ----------
const person = {
first: 'Zakary',
last: 'Timson',
title: 'Software Engineer',
org: 'ZaksCode',
email: 'zaktimson@gmail.com',
phone: '+1 (519) 917-1046', // e.g. '+1 519 555 0123' - leave empty to hide
city: 'London',
region: 'Ontario',
country: 'Canada',
website: 'https://zakscode.com',
github: 'https://github.com/ztimson',
git: 'https://git.zakscode.com',
photo: '/profile.jpg'
};
// ---------- Rows displayed on the card ----------
const rows = computed(() => [
{key: 'email', label: 'Email', value: person.email, href: `mailto:${person.email}`, icon: 'envelope'},
person.phone && {key: 'phone', label: 'Phone', value: person.phone, href: `tel:${person.phone.replace(/[^+\d]/g, '')}`, icon: 'phone'},
{key: 'website', label: 'Website', value: person.website.replace(/^https?:\/\//, ''), copy: person.website, href: person.website, icon: 'globe'},
].filter(Boolean) as any[]);
// ---------- Copy to clipboard ----------
const copied = ref('');
let copyTimer: any;
async function copy(key: string, text: string) {
try {
await navigator.clipboard.writeText(text);
} catch {
const el = document.createElement('textarea');
el.value = text;
el.style.position = 'fixed';
el.style.opacity = '0';
document.body.appendChild(el);
el.select();
document.execCommand('copy');
el.remove();
}
copied.value = key;
clearTimeout(copyTimer);
copyTimer = setTimeout(() => copied.value = '', 1600);
}
// ---------- vCard ----------
const esc = (s: string) => s.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\r?\n/g, '\\n');
const fold = (line: string) => line.match(/.{1,74}/g)?.join('\r\n ') ?? line;
function buildVCard(photoBase64?: string) {
const lines = [
'BEGIN:VCARD',
'VERSION:3.0',
`N:${esc(person.last)};${esc(person.first)};;;`,
`FN:${esc(person.first + ' ' + person.last)}`,
`ORG:${esc(person.org)}`,
`TITLE:${esc(person.title)}`,
`EMAIL;TYPE=INTERNET,WORK:${person.email}`,
person.phone && `TEL;TYPE=CELL:${person.phone}`,
`ADR;TYPE=WORK:;;;${esc(person.city)};${esc(person.region)};;${esc(person.country)}`,
`URL:${person.website}`,
`item1.URL:${person.github}`,
'item1.X-ABLabel:GitHub',
`item2.URL:${person.git}`,
'item2.X-ABLabel:Git',
photoBase64 && `PHOTO;ENCODING=b;TYPE=JPEG:${photoBase64}`,
'END:VCARD'
].filter(Boolean) as string[];
return lines.map(fold).join('\r\n') + '\r\n';
}
// Shrink the profile picture so the .vcf stays small
async function loadPhoto(size = 256): Promise<string | undefined> {
try {
const blob = await (await fetch(person.photo)).blob();
const bmp = await createImageBitmap(blob);
const side = Math.min(bmp.width, bmp.height);
const canvas = document.createElement('canvas');
canvas.width = canvas.height = size;
canvas.getContext('2d')!.drawImage(
bmp, (bmp.width - side) / 2, (bmp.height - side) / 2, side, side, 0, 0, size, size
);
return canvas.toDataURL('image/jpeg', 0.8).split(',')[1];
} catch {
return undefined;
}
}
const saving = ref(false);
async function downloadContact() {
saving.value = true;
try {
const vcf = buildVCard(await loadPhoto());
const url = URL.createObjectURL(new Blob([vcf], {type: 'text/vcard;charset=utf-8'}));
const a = document.createElement('a');
a.href = url;
a.download = `${person.first}-${person.last}.vcf`;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} finally {
saving.value = false;
}
}
// ---------- QR code (no photo, so it stays easy to scan) ----------
const qr = ref('');
onMounted(async () => {
qr.value = await QRCode.toDataURL(buildVCard(), {
errorCorrectionLevel: 'M',
margin: 1,
width: 600,
color: {dark: '#1c1c1e', light: '#ffffff'}
});
});
</script>
<template>
<div class="bc-page">
<article class="bc">
<!-- Banner -->
<div class="bc-banner">
<span class="bc-ring bc-ring--1"></span>
<span class="bc-ring bc-ring--2"></span>
<span class="bc-ring bc-ring--3"></span>
</div>
<!-- Identity -->
<div class="bc-identity">
<img class="bc-avatar" :src="person.photo" :alt="`${person.first} ${person.last}`">
<h1 class="bc-name">{{person.first}} {{person.last}}</h1>
<div class="bc-title">{{person.title}}</div>
</div>
<!-- Details -->
<ul class="bc-list">
<li v-for="r in rows" :key="r.key" class="bc-row">
<span class="bc-row-icon"><icon :name="r.icon" :class="{'fa-brands': r.brand}" /></span>
<div class="bc-row-text">
<span class="bc-row-label">{{r.label}}</span>
<a v-if="r.href" class="bc-row-value" :href="r.href" :target="r.href.startsWith('http') ? '_blank' : undefined" rel="noopener">{{r.value}}</a>
<span v-else class="bc-row-value">{{r.value}}</span>
</div>
<button type="button" class="bc-copy" :class="{'bc-copy--done': copied == r.key}" :aria-label="`Copy ${r.label}`" @click="copy(r.key, r.copy || r.value)">
<icon :name="copied == r.key ? 'check' : 'copy'" />
</button>
</li>
</ul>
<!-- QR -->
<div class="bc-qr">
<div class="bc-qr-box">
<img v-if="qr" :src="qr" alt="Contact QR code">
</div>
<div class="bc-qr-caption">Scan to add me to your contacts</div>
<a v-if="qr" class="bc-qr-link" :href="qr" :download="`${person.first}-${person.last}-QR.png`">Download QR</a>
</div>
<!-- Primary action -->
<div class="bc-section">
<button type="button" class="bc-save" :disabled="saving" @click="downloadContact">
<icon name="user-plus" /> Save to Contacts
</button>
</div>
</article>
<!-- Toast -->
<div class="bc-toast" :class="{'bc-toast--show': copied}" role="status">Copied to clipboard</div>
</div>
</template>
<style scoped>
.bc-page {
--bc-accent: #732222;
--bc-accent-2: #a63a3a;
--bc-ink: #1c1c1e;
--bc-muted: #7a7a82;
--bc-line: #ececf0;
min-height: 100vh;
min-height: 100dvh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
background:
radial-gradient(1200px 600px at 10% -10%, #8f2d2d 0%, transparent 60%),
radial-gradient(900px 500px at 110% 110%, #3b1010 0%, transparent 60%),
#2a0d0d;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: var(--bc-ink);
}
/* Card */
.bc {
width: 100%;
max-width: 400px;
background: #fff;
border-radius: 28px;
overflow: hidden;
box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(255, 255, 255, 0.06);
}
/* Banner */
.bc-banner {
position: relative;
height: 130px;
background: linear-gradient(135deg, var(--bc-accent) 0%, var(--bc-accent-2) 100%);
overflow: hidden;
}
.bc-ring {
position: absolute;
border-radius: 50%;
border: 1.5px solid rgba(255, 255, 255, 0.18);
}
.bc-ring--1 { width: 220px; height: 220px; right: -60px; top: -90px; }
.bc-ring--2 { width: 140px; height: 140px; right: -20px; top: -50px; }
.bc-ring--3 { width: 300px; height: 300px; left: -140px; bottom: -190px; }
.bc-org {
position: absolute;
top: 18px;
left: 22px;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.22em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.85);
}
/* Identity */
.bc-identity {
text-align: center;
padding: 0 24px 4px;
margin-top: -60px;
position: relative;
}
.bc-avatar {
width: 120px;
height: 120px;
object-fit: cover;
border-radius: 50%;
border: 5px solid #fff;
box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.35);
background: #eee;
}
.bc-name {
margin: 14px 0 2px;
font-size: 1.65rem;
line-height: 1.15;
font-weight: 700;
letter-spacing: -0.01em;
}
.bc-title {
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--bc-accent);
}
/* Save button */
.bc-section {
padding: 20px 24px 8px;
}
.bc-save {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 0.6rem;
padding: 0.9rem 1rem;
border: 0;
border-radius: 14px;
background: linear-gradient(135deg, var(--bc-accent) 0%, var(--bc-accent-2) 100%);
color: #fff;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
box-shadow: 0 10px 20px -10px var(--bc-accent);
transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s;
}
.bc-save:hover { transform: translateY(-1px); box-shadow: 0 14px 24px -10px var(--bc-accent); }
.bc-save:active { transform: translateY(0); }
.bc-save:disabled { opacity: 0.6; cursor: progress; }
/* Rows */
.bc-list {
list-style: none;
margin: 0;
padding: 8px 12px 4px;
}
.bc-row {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 12px;
border-radius: 14px;
transition: background 0.15s;
}
.bc-row:hover { background: #f7f7f9; }
.bc-row-icon {
flex: none;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: color-mix(in srgb, var(--bc-accent) 10%, white);
color: var(--bc-accent);
font-size: 0.95rem;
}
.bc-row-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.bc-row-label {
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--bc-muted);
}
.bc-row-value {
font-size: 0.95rem;
font-weight: 500;
color: var(--bc-ink);
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
a.bc-row-value:hover { color: var(--bc-accent); }
.bc-copy {
flex: none;
width: 38px;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
border: 1.5px solid var(--bc-line);
border-radius: 50%;
background: #fff;
color: var(--bc-muted);
cursor: pointer;
transition: all 0.15s;
}
.bc-copy:hover { border-color: var(--bc-accent); color: var(--bc-accent); }
.bc-copy:active { transform: scale(0.92); }
.bc-copy--done { background: #2e9e5b; border-color: #2e9e5b; color: #fff; }
.bc-copy--done:hover { background: #2e9e5b; border-color: #2e9e5b; color: #fff; }
/* QR */
.bc-qr {
padding: 24px 20px 18px;
text-align: center;
border-top: 1px dashed var(--bc-line);
}
.bc-qr-box {
width: 200px;
height: 200px;
margin: 0 auto;
padding: 10px;
border-radius: 20px;
background: #fff;
box-shadow: 0 0 0 1.5px var(--bc-line), 0 12px 24px -12px rgba(0, 0, 0, 0.25);
box-sizing: border-box;
}
.bc-qr-box img { width: 100%; height: 100%; display: block; border-radius: 8px; }
.bc-qr-caption {
margin-top: 14px;
font-size: 0.82rem;
color: var(--bc-muted);
}
.bc-qr-link {
display: inline-block;
margin-top: 4px;
font-size: 0.8rem;
font-weight: 600;
color: var(--bc-accent);
text-decoration: none;
}
.bc-qr-link:hover { text-decoration: underline; }
/* Toast */
.bc-toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translate(-50%, 20px);
padding: 0.65rem 1.1rem;
border-radius: 999px;
background: rgba(28, 28, 30, 0.92);
color: #fff;
font-size: 0.85rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s, transform 0.2s;
}
.bc-toast--show { opacity: 1; transform: translate(-50%, 0); }
@media (max-width: 440px) {
.bc-page { padding: 0; align-items: stretch; }
.bc { max-width: none; border-radius: 0; }
}
</style>
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
import Foot from '@/components/foot.vue';
import Profile from '@/components/profile.vue';
</script>
<template>
<!-- Spacer -->
<div class="w-100" style="height: min(75vh, 500px)"></div>
<!-- Content -->
<div class="cap-width mb-3 bg-white">
<!-- Header -->
<header class="px-4 d-flex justify-content-center justify-content-sm-start" style="background: #732222">
<profile style="transform: translateY(-33%)" />
</header>
<!-- Body -->
<main class="p-3">
<router-view></router-view>
</main>
<!-- Footer -->
<foot />
</div>
<!-- Spacer -->
<div class="d-none d-lg-block w-100" style="height: 40px"></div>
</template>