Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Terminal Window React Component with Keystroke Sounds – https://blog.webmaestro.fr/terminal-window-react-component-keystroke-sounds-css-svg
@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;
}
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/>| &gt; {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}⸩&nbsp;{loader}&nbsp;{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