Last active
September 10, 2021 15:38
-
-
Save WebMaestroFr/4f41541602a15c278b39f1af4e1661e0 to your computer and use it in GitHub Desktop.
Terminal Window React Component with Keystroke Sounds
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@import url('https://cdn.rawgit.com/tonsky/FiraCode/1.204/distr/fira_code.css'); | |
body { | |
background-color: #212121; | |
font-family: HelveticaNeue, 'Helvetica Neue', 'Lucida Grande', Arial, sans-serif; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
.App { | |
height: 100vh; | |
position: relative; | |
} | |
.DesktopWindow { | |
border-radius: 6px; | |
bottom: 32px; | |
left: 32px; | |
overflow: hidden; | |
position: absolute; | |
right: 32px; | |
top: 32px; | |
} | |
.DesktopWindow-header { | |
background-color: #e0e0e0; | |
border-radius: 6px 6px 0 0; | |
height: 24px; | |
position: absolute; | |
top: 0; | |
width: 100%; | |
} | |
.DesktopWindow-body { | |
bottom: 0; | |
position: absolute; | |
top: 24px; | |
width: 100%; | |
} | |
.DesktopWindow-header-buttons { | |
float: left; | |
} | |
.DesktopWindow-header-buttons:hover circle { | |
cursor: not-allowed; | |
fill: #f3f1f3; | |
stroke: #b1aeb1; | |
} | |
.DesktopWindow-header-title { | |
color: #4d494d; | |
cursor: default; | |
font-size: 12px; | |
font-weight: normal; | |
line-height: 24px; | |
margin: 0; | |
text-align: center; | |
user-select: none; | |
} | |
.TerminalLine { | |
margin: 1em 0; | |
} | |
.DesktopWindow-body > .TerminalShell { | |
background-color: #2e3440; | |
box-sizing: border-box; | |
color: #eceff4; | |
font-family: 'Fira Code', monospace; | |
font-size: 16px; | |
height: 100%; | |
line-height: 24px; | |
overflow-y: auto; | |
padding: 0 1em; | |
width: 100%; | |
} | |
.TerminalShell .TerminalShell { | |
margin: 1em 0; | |
padding: 0; | |
} | |
.TerminalShell .TerminalShell .TerminalLine { | |
margin: 0; | |
} | |
.TerminalProgress .TerminalLine { | |
display: inline; | |
} | |
.TerminalShell .blue { | |
color: #81a1c1; | |
} | |
.TerminalShell .green { | |
color: #a3be8c; | |
} | |
.TerminalShell .yellow { | |
color: #ebcb8b; | |
} | |
.TerminalShell .orange { | |
color: #d08770; | |
} | |
.TerminalShell .red { | |
color: #bf616a; | |
} | |
.TerminalShell .purple { | |
color: #b48ead; | |
} | |
.Terminal .reverse { | |
background-color: #eceff4; | |
color: #2e3440; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {Component} from 'react'; | |
const sounds = { | |
delete: [ | |
new Audio('sounds/DELETE-0.wav'), new Audio('sounds/DELETE-1.wav'), new Audio('sounds/DELETE-2.wav'), new Audio('sounds/DELETE-3.wav'), new Audio('sounds/DELETE-4.wav') | |
], | |
enter: [ | |
new Audio('sounds/ENTER-0.wav'), new Audio('sounds/ENTER-1.wav') | |
], | |
letter: [ | |
new Audio('sounds/LETTER-0.wav'), | |
new Audio('sounds/LETTER-1.wav'), | |
new Audio('sounds/LETTER-2.wav'), | |
new Audio('sounds/LETTER-3.wav'), | |
new Audio('sounds/LETTER-4.wav'), | |
new Audio('sounds/LETTER-5.wav'), | |
new Audio('sounds/LETTER-6.wav'), | |
new Audio('sounds/LETTER-7.wav'), | |
new Audio('sounds/LETTER-8.wav'), | |
new Audio('sounds/LETTER-9.wav'), | |
new Audio('sounds/LETTER-10.wav'), | |
new Audio('sounds/LETTER-11.wav'), | |
new Audio('sounds/LETTER-12.wav'), | |
new Audio('sounds/LETTER-13.wav'), | |
new Audio('sounds/LETTER-14.wav'), | |
new Audio('sounds/LETTER-15.wav'), | |
new Audio('sounds/LETTER-16.wav'), | |
new Audio('sounds/LETTER-17.wav') | |
], | |
shift: [new Audio('sounds/SHIFT-0.wav')], | |
space: [new Audio('sounds/SPACE-0.wav'), new Audio('sounds/SPACE-1.wav'), new Audio('sounds/SPACE-2.wav')] | |
}; | |
function playSound(name) { | |
const s = Math.round(Math.random() * (sounds[name].length - 1)); | |
sounds[name][s].play(); | |
} | |
function getDuration(max) { | |
return Math.random() * 4 * max / 5 + max / 5; | |
} | |
export class TerminalLine extends Component { | |
componentDidMount() { | |
const {duration, onComplete} = this.props, | |
d = getDuration(duration || 600); | |
setTimeout(onComplete, d); | |
} | |
render() { | |
return <div className="TerminalLine">{this.props.children}</div>; | |
} | |
} | |
export class TerminalCommand extends Component { | |
constructor(props) { | |
super(props); | |
const {children, duration, onComplete} = props; | |
this.state = { | |
characters: children.split(''), | |
output: '' | |
}; | |
this.nextStep = () => { | |
const {characters, output} = this.state, | |
character = characters.shift(), | |
d = getDuration(duration || 300); | |
if (character === undefined) { | |
playSound('enter'); | |
return setTimeout(onComplete, d); | |
} | |
playSound( | |
character === ' ' | |
? 'space' | |
: 'letter' | |
); | |
setTimeout(this.nextStep, d); | |
return this.setState({ | |
characters, | |
output: output + character | |
}); | |
}; | |
} | |
render() { | |
return <TerminalLine duration={this.props.duration || 300} onComplete={this.nextStep}> | |
[<span className="red">{this.props.dir}</span>]<br/>| > {this.state.output} | |
</TerminalLine>; | |
} | |
} | |
export class TerminalProgress extends Component { | |
constructor(props) { | |
super(props); | |
const {children, duration, onComplete} = props; | |
this.nextStep = () => { | |
const {step} = this.state, | |
d = getDuration(duration || 600); | |
if (step === children.length) { | |
return setTimeout(onComplete, d); | |
} | |
setTimeout(this.nextStep, d); | |
return this.setState({ | |
step: step + 1 | |
}); | |
}; | |
this.state = { | |
step: 0 | |
}; | |
} | |
render() { | |
const {children, duration} = this.props, {step} = this.state, | |
progress = step / children.length, | |
progressValue = Math.round(progress * 18), | |
bar = '█'.repeat(progressValue) + '░'.repeat(18 - progressValue), | |
loader = [ | |
'⠇', | |
'⠋', | |
'⠙', | |
'⠸', | |
'⠴', | |
'⠦' | |
][step % 6]; | |
return step === children.length | |
? null | |
: <TerminalLine duration={duration || 600} onComplete={this.nextStep}>⸨{bar}⸩ {loader} {children[step]}</TerminalLine>; | |
} | |
} | |
export class TerminalShell extends Component { | |
constructor(props) { | |
super(props); | |
const {duration, onComplete} = props, | |
nextStep = () => { | |
const {children, step} = this.state; | |
if (step === children.length) { | |
const d = getDuration(duration || 1200); | |
return onComplete | |
? setTimeout(onComplete, d) | |
: null; | |
} | |
return this.setState({ | |
step: step + 1 | |
}); | |
}; | |
this.state = { | |
children: props | |
.children | |
.map((child, key) => React.cloneElement(child, {key, onComplete: nextStep})), | |
step: 1 | |
}; | |
} | |
componentDidMount() { | |
this.refs.shell.scrollTop = this.refs.shell.scrollHeight; | |
} | |
componentDidUpdate() { | |
this.refs.shell.scrollTop = this.refs.shell.scrollHeight; | |
} | |
render() { | |
const {children, step} = this.state; | |
return <div className="TerminalShell" ref="shell">{children.slice(0, step)}</div>; | |
} | |
} | |
export class DesktopWindow extends Component { | |
render() { | |
const {title, children} = this.props; | |
return <div className="DesktopWindow"> | |
<div className="DesktopWindow-header"> | |
<svg | |
className="DesktopWindow-header-buttons" | |
viewBox="0 0 80 24" | |
xmlns="http://www.w3.org/2000/svg" | |
width="80" | |
height="24"> | |
<circle cx="16" cy="12" r="6" fill="#ff5c5c" stroke="#e33e41" strokeWidth="1"/> | |
<circle cx="40" cy="12" r="6" fill="#ffbd4c" stroke="#e09e3e" strokeWidth="1"/> | |
<circle cx="64" cy="12" r="6" fill="#00ca56" stroke="#14ae46" strokeWidth="1"/> | |
</svg> | |
<h1 className="DesktopWindow-header-title">{title}</h1> | |
</div> | |
<div className="DesktopWindow-body">{children}</div> | |
</div>; | |
} | |
} | |
export default DesktopWindow; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment