Profile polish: both git links, tighter gutter, page-scroll thank section, 6x faster typing, reordered sequence, mobile thank panel

This commit is contained in:
2026-09-20 07:45:15 +08:00
parent de7c44b87d
commit 4c78e8efd4
3 changed files with 224 additions and 118 deletions
+82 -56
View File
@@ -23,13 +23,17 @@
data() {
return {
interval: profile.typingSpeed,
charsPerTick: profile.charsPerTick,
fullStyle: profile.desktopStyles,
fullMarkdown: profile.resumeMarkdown,
thanksMarkdown: profile.thanksMarkdown,
currentStyle: '',
currentMarkdown: '',
currentThankMarkdown: '',
enableHtml: false
enableHtml: false,
htmlShown: false,
thanksShown: false,
styleFull: profile.desktopStyles.join('')
}
},
created() {
@@ -37,79 +41,101 @@
},
methods: {
makeResume: async function () {
// Type every stage of the stylesheet in order. The original code called
// progressivelyShowStyle(1) three times between steps, but that call is
// idempotent once the cumulative-length guard is satisfied, so stages 2
// and 3 were never typed and the whole .thankEditor stylesheet (padding,
// link colours, box layout) was missing from the injected <style> tag.
for (let n = 0; n < this.fullStyle.length; n++) {
await this.progressivelyShowStyle(n)
}
// Type the resume body while it is still plain text, then swap to
// rendered HTML. The swap must happen AFTER both text passes, otherwise
// `enableHtml` flips early and both panels render empty markdown.
await this.progressivelyShowResume()
await this.progressivelyShowThanks()
// Both editors appear (background + borders), then the CSS types in the
// left pane. During this the right pane is NOT blank: the resume editor
// renders its content as it goes, so there is never a long empty wait.
await this.typeStyleUpToMarker('/* Start writing resume */')
// Render the resume right after the marker, before the remaining CSS.
await this.showHtml()
// Type the rest of the stylesheet, then show the bottom panel once it is
// fully styled so it never sits half-drawn for a long time.
await this.typeStyleRemaining()
await this.showThanks()
await this.progressivelyShowThanks()
},
typeStyleUpToMarker(marker) {
return new Promise((resolve) => {
const markerIdx = this.styleFull.indexOf(marker)
const target = markerIdx >= 0 ? markerIdx + marker.length : this.styleFull.length
this.progressivelyShowStylePartial(target, resolve)
})
},
typeStyleRemaining() {
return new Promise((resolve) => {
this.progressivelyShowStyleAll(resolve)
})
},
showHtml() {
return new Promise((resolve) => {
this.enableHtml = true
this.$nextTick(() => {
if (this.$refs.resumeEditor) this.$refs.resumeEditor.goTop()
this.htmlShown = true
resolve()
})
})
},
showThanks() {
return new Promise((resolve) => {
this.thanksShown = true
this.$nextTick(() => {
if (this.$refs.thankEditor) this.$refs.thankEditor.goTop()
resolve()
})
resolve()
})
},
progressivelyShowStyle(n) {
return new Promise((resolve) => {
const interval = this.interval
const showStyle = (() => {
const style = this.fullStyle[n]
if (!style) return
const length = this.fullStyle.filter((_, index) => index <= n).map((item) => item.length).reduce((p, c) => p + c, 0)
const prefixLength = length - style.length
if (this.currentStyle.length < length) {
const l = this.currentStyle.length - prefixLength
this.currentStyle += style.substring(l, l + 1) || ' '
if (style.substring(l - 1, l) === '\n' && this.$refs.styleEditor) {
this.$nextTick(() => this.$refs.styleEditor.goBottom())
}
setTimeout(showStyle, interval)
} else {
resolve()
}
})
showStyle()
})
},
progressivelyShowResume() {
return new Promise((resolve) => {
const length = this.fullMarkdown.length
const interval = this.interval
const showResume = () => {
if (this.currentMarkdown.length < length) {
this.currentMarkdown = this.fullMarkdown.substring(0, this.currentMarkdown.length + 1)
const prevChar = this.currentMarkdown[this.currentMarkdown.length - 2]
if (prevChar === '\n' && this.$refs.resumeEditor) {
this.$nextTick(() => this.$refs.resumeEditor.goBottom())
}
setTimeout(showResume, interval)
} else {
resolve()
}
// Type the entire style array from whatever is already typed, writing
// charsPerTick characters per tick.
progressivelyShowStyleAll(resolve) {
const total = this.styleFull.length
const step = this.increment(total)
const tick = () => {
if (this.currentStyle.length < total) {
step()
setTimeout(tick, this.interval)
} else {
resolve()
}
showResume()
})
}
tick()
},
progressivelyShowStylePartial(target, resolve) {
const step = this.increment(target)
const tick = () => {
if (this.currentStyle.length < target) {
step()
setTimeout(tick, this.interval)
} else {
resolve()
}
}
tick()
},
// Returns a function that writes `charsPerTick` characters per call and
// keeps the code pane scrolled to the newly typed line.
increment(total) {
const cpt = this.charsPerTick
return () => {
const cur = this.currentStyle.length
if (cur >= total) return
const end = Math.min(total, cur + cpt)
const slice = this.styleFull.substring(cur, end)
this.currentStyle += slice
if (slice.includes('\n') && this.$refs.styleEditor) {
this.$nextTick(() => this.$refs.styleEditor.goBottom())
}
}
},
progressivelyShowThanks() {
return new Promise((resolve) => {
const length = this.thanksMarkdown.length
const interval = this.interval
const cpt = this.charsPerTick
const showThanks = () => {
if (this.currentThankMarkdown.length < length) {
this.currentThankMarkdown = this.thanksMarkdown.substring(0, this.currentThankMarkdown.length + 1)
this.currentThankMarkdown = this.thanksMarkdown.substring(0, Math.min(length, this.currentThankMarkdown.length + cpt))
const prevChar = this.currentThankMarkdown[this.currentThankMarkdown.length - 2]
if (prevChar === '\n' && this.$refs.thankEditor) {
this.$nextTick(() => this.$refs.thankEditor.goBottom())
@@ -136,4 +162,4 @@
* {
box-sizing: border-box;
}
</style>
</style>
+77 -41
View File
@@ -2,6 +2,7 @@
<div id="app">
<StyleEditor ref="styleEditor" :code="currentStyle"></StyleEditor>
<ResumeEditor ref="resumeEditor" :markdown="currentMarkdown" :enableHtml="enableHtml"></ResumeEditor>
<ThankEditor ref="thankEditor" :markdown="currentThankMarkdown" :enableHtml="enableHtml"></ThankEditor>
</div>
</template>
@@ -9,19 +10,24 @@
import profile from './config/profile.js'
import StyleEditor from './components/StyleEditor.vue'
import ResumeEditor from './components/ResumeEditor.vue'
import ThankEditor from './components/ThankEditor.vue'
import './assets/reset.css'
export default {
name: 'app',
components: { StyleEditor, ResumeEditor },
components: { StyleEditor, ResumeEditor, ThankEditor },
data() {
return {
interval: profile.typingSpeed,
charsPerTick: profile.charsPerTick,
fullStyle: profile.mobileStyles,
fullMarkdown: profile.resumeMarkdown,
thanksMarkdown: profile.thanksMarkdown,
currentStyle: '',
currentMarkdown: '',
enableHtml: false
currentThankMarkdown: '',
enableHtml: false,
styleFull: profile.mobileStyles.join('')
}
},
created() {
@@ -29,58 +35,88 @@
},
methods: {
makeResume: async function () {
await this.progressivelyShowStyle(0)
await this.progressivelyShowResume()
await this.progressivelyShowStyle(1)
await this.typeStyleUpToMarker('/* Start writing resume */')
await this.showHtml()
await this.progressivelyShowStyle(1)
await this.typeStyleRemaining()
await this.progressivelyShowThanks()
},
typeStyleUpToMarker(marker) {
return new Promise((resolve) => {
const markerIdx = this.styleFull.indexOf(marker)
const target = markerIdx >= 0 ? markerIdx + marker.length : this.styleFull.length
this.progressivelyShowStylePartial(target, resolve)
})
},
typeStyleRemaining() {
return new Promise((resolve) => {
this.progressivelyShowStyleAll(resolve)
})
},
showHtml() {
return new Promise((resolve) => {
this.enableHtml = true
this.$nextTick(() => this.$refs.resumeEditor.goTop())
resolve()
})
},
progressivelyShowStyle(n) {
return new Promise((resolve) => {
const interval = this.interval
const showStyle = (() => {
const style = this.fullStyle[n]
if (!style) return
const length = this.fullStyle.filter((_, index) => index <= n).map((item) => item.length).reduce((p, c) => p + c, 0)
const prefixLength = length - style.length
if (this.currentStyle.length < length) {
const l = this.currentStyle.length - prefixLength
this.currentStyle += style.substring(l, l + 1) || ' '
if (style.substring(l - 1, l) === '\n' && this.$refs.styleEditor) {
this.$nextTick(() => this.$refs.styleEditor.goBottom())
}
setTimeout(showStyle, interval)
} else {
resolve()
}
this.$nextTick(() => {
if (this.$refs.resumeEditor) this.$refs.resumeEditor.goTop()
resolve()
})
showStyle()
})
},
progressivelyShowResume() {
progressivelyShowStyleAll(resolve) {
const total = this.styleFull.length
const step = this.increment(total)
const tick = () => {
if (this.currentStyle.length < total) {
step()
setTimeout(tick, this.interval)
} else {
resolve()
}
}
tick()
},
progressivelyShowStylePartial(target, resolve) {
const step = this.increment(target)
const tick = () => {
if (this.currentStyle.length < target) {
step()
setTimeout(tick, this.interval)
} else {
resolve()
}
}
tick()
},
increment(total) {
const cpt = this.charsPerTick
return () => {
const cur = this.currentStyle.length
if (cur >= total) return
const end = Math.min(total, cur + cpt)
const slice = this.styleFull.substring(cur, end)
this.currentStyle += slice
if (slice.includes('\n') && this.$refs.styleEditor) {
this.$nextTick(() => this.$refs.styleEditor.goBottom())
}
}
},
progressivelyShowThanks() {
return new Promise((resolve) => {
const length = this.fullMarkdown.length
const length = this.thanksMarkdown.length
const interval = this.interval
const showResume = () => {
if (this.currentMarkdown.length < length) {
this.currentMarkdown = this.fullMarkdown.substring(0, this.currentMarkdown.length + 1)
const prevChar = this.currentMarkdown[this.currentMarkdown.length - 2]
if (prevChar === '\n' && this.$refs.resumeEditor) {
this.$nextTick(() => this.$refs.resumeEditor.goBottom())
}
setTimeout(showResume, interval)
const cpt = this.charsPerTick
const showThanks = () => {
if (this.currentThankMarkdown.length < length) {
this.currentThankMarkdown = this.thanksMarkdown.substring(0, Math.min(length, this.currentThankMarkdown.length + cpt))
this.$nextTick(() => {
// Keep the newly revealed thanks section in view as it grows.
window.scrollTo(0, document.body.scrollHeight)
})
setTimeout(showThanks, interval)
} else {
resolve()
}
}
showResume()
showThanks()
})
}
}
@@ -101,4 +137,4 @@
*{
box-sizing: border-box;
}
</style>
</style>
+65 -21
View File
@@ -2,11 +2,14 @@ export default {
// Personal info
name: 'Mr. Hoelee',
title: 'Full Stack Engineer · AI & Automation · DevOps',
githubUrl: 'https://git.hoelee.com/hoelee',
githubUrl: 'https://github.com/hoelee/hoelee-profile',
giteaUrl: 'https://git.hoelee.com/hoelee',
// Animation speed (ms per character typed)
// Lower = faster. Recommended range: 1 (instant) to 20 (dramatic slow)
// Animation speed — per-tick delay (ms) and characters written per tick.
// Effective speed = typingSpeed / charsPerTick. Lower delay = faster;
// higher charsPerTick = fewer setTimeout hops = much faster and smoother.
typingSpeed: 1,
charsPerTick: 5,
// Resume content in Markdown
resumeMarkdown: `Mr. Hoelee (Lee Teong Hoe)
@@ -90,7 +93,8 @@ The best fit is a team that needs one person who can own the whole stack: backen
* WhatsApp → [+6012-7972969](https://wa.me/60127972969)
* Email → [[email protected]](mailto:[email protected])
* GitHub → [git.hoelee.com/hoelee](https://git.hoelee.com/hoelee)
* Gitea → [git.hoelee.com/hoelee](https://git.hoelee.com/hoelee)
* GitHub → [github.com/hoelee](https://github.com/hoelee/)
* Based in Perak, Malaysia — remote-friendly, open to hybrid in Malaysia
Where I can contribute:
@@ -105,6 +109,7 @@ Where I can contribute:
desktopStyles: [
`/*
* Gitea: https://git.hoelee.com/hoelee
* GitHub: https://github.com/hoelee
* Hi, I'm Mr. Hoelee — Full Stack Engineer, AI & Automation, DevOps
* Let me create an animated resume!
*/
@@ -120,11 +125,11 @@ html {
#content{
height:70vh;
margin:0;
overflow: hidden;
}
#foot{
height:30vh;
min-height:30vh;
margin:0;
overflow: hidden;
}
/* Set border */
@@ -208,14 +213,14 @@ html{
* Thanks for everyone's attention.
*/
.styleEditor{
width: calc(49vw - 1em);
width: calc(50vw - 1em);
height:70vh;
margin-left:1vw;
margin-left:.5em;
}
.resumeEditor{
width: calc(49vw - 1em);
width: calc(50vw - 1em);
height:70vh;
margin-right:1vw;
margin-right:.5em;
}
.thankEditor{
@@ -250,9 +255,10 @@ html{
}
.thankEditor {
width: 99vw; height: calc(30vh - 1.5em);
width: 99vw; min-height: calc(30vh - 1.5em);
border: 1px solid #ccc;
font-size: .9em;
overflow: visible;
}
`,`
.thankEditor{
@@ -276,18 +282,19 @@ html{
}
.thankEditor{
width: 95vw;
height: calc(30vh - 1.5em);
width: 95vw;
min-height: calc(30vh - 1.5em);
margin: .5em auto;
padding: 1.25em 1.75em;
box-sizing: border-box;
overflow: visible;
-webkit-transition: none;
transition: none;
-webkit-transform: rotateX(-10deg) translateZ(-200px);
transform: rotateX(-10deg) translateZ(-200px);
}
/* keep the page itself from ever scrolling */
/* keep horizontal scrolling off; allow vertical scroll when short */
body{
overflow-x: hidden;
}
@@ -297,6 +304,7 @@ body{
mobileStyles: [
`/*
* Gitea: https://git.hoelee.com/hoelee
* GitHub: https://github.com/hoelee
* Hi, I'm Mr. Hoelee — Full Stack Engineer, AI & Automation, DevOps
* Let me create an animated resume!
*/
@@ -315,9 +323,10 @@ html {
padding: .5em;
border: 1px solid;
overflow: auto;
width: 96vw;
width: 94vw;
margin: 2.3vh 2vw;
height: 90vh;
box-sizing: border-box;
}
/* Too short */
.styleEditor {
@@ -342,19 +351,20 @@ html{
perspective: 1000px;
}
.styleEditor {
position: fixed; left: 0; top: 0;
position: absolute; left: 0; top: 0;
transform: rotateX(-10deg) translateZ(-50px) ;
}
/* Prepare an editor */
.resumeEditor{
position: fixed;
top: 48%; left: 0;
position: relative;
top: 47vh; left: 0;
padding: .5em; margin: 1.5vh;
width: 94vw; height: 48.5vh;
border: 1px solid;
background: white; color: #222;
overflow: auto;
box-sizing: border-box;
}
/* Start writing resume */
@@ -414,10 +424,44 @@ html{
.resumeEditor a:hover {
text-decoration: underline;
}
/* keep the page itself from ever scrolling */
html, body{
/* Thank-you panel below the resume, full width */
.thankEditor {
position: relative;
width: 94vw;
margin: 2vh auto;
padding: 1.25em 1.25em;
box-sizing: border-box;
background: #E9D9BB;
color: #001C42;
overflow: auto;
border: 1px solid #ccc;
font-size: .9em;
}
.thankEditor h2 {
margin-top: 0;
padding-bottom: .35em;
border-bottom: 2px solid rgba(0,28,66,.35);
}
.thankEditor a {
color: #0b4a6f;
font-weight: 600;
}
.thankEditor ul, .thankEditor ol {
list-style: none;
padding-left: .25em;
}
.thankEditor ul > li::before {
content: '☞';
color: #0b4a6f;
margin-right: .5em;
}
/* allow vertical scroll, never horizontal */
html, body {
overflow-x: hidden;
overflow-y: auto;
max-width: 100%;
}
`]
}
}