Profile polish: both git links, tighter gutter, page-scroll thank section, 6x faster typing, reordered sequence, mobile thank panel
This commit is contained in:
+82
-56
@@ -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
@@ -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
@@ -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%;
|
||||
}
|
||||
`]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user