Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Robin Choffardet
71d1794d56 Behave like a terminal (#20)
*  add history feature

* 📝 merge submit event

* 📝 remove else keyword by making use of early return

* 📝 end file with a newline

*  add limit to history (1000 elements max)

*  behave like a terminal

*  add redirection for "old" sharable url
2017-10-30 18:58:27 +01:00
16 changed files with 219 additions and 105 deletions

View file

@ -13,6 +13,12 @@ class HomeController extends Controller
public function submit($command)
{
if(request()->header('Accept') !== 'application/json')
{
return redirect()->to('/#'.$command);
}
return (new CommandChain())->perform(strtolower($command));
}
}

View file

@ -15,9 +15,7 @@ class CommandChain
protected $commands = [
Manual::class,
Localhost::class,
Clear::class,
Ip::class,
Doom::class,
DnsLookup::class,
];

View file

@ -1,19 +0,0 @@
<?php
namespace App\Services\Commands\Commands;
use App\Services\Commands\Command;
use Symfony\Component\HttpFoundation\Response;
class Clear implements Command
{
public function canPerform(string $command): bool
{
return $command === 'clear';
}
public function perform(string $command): Response
{
return redirect('/');
}
}

View file

@ -24,11 +24,15 @@ class DnsLookup implements Command
$errorText = __('errors.noDnsRecordsFound', compact('domain'));
flash()->error($errorText);
return redirect('/');
return response([
'message' => $errorText,
'type' => 'danger',
]);
}
return response()->view('home.index', ['output' => htmlentities($dnsRecords)]);
return response([
'message' => htmlentities($dnsRecords),
'type' => 'default',
]);
}
}

View file

@ -1,19 +0,0 @@
<?php
namespace App\Services\Commands\Commands;
use App\Services\Commands\Command;
use Symfony\Component\HttpFoundation\Response;
class Doom implements Command
{
public function canPerform(string $command): bool
{
return $command === 'doom';
}
public function perform(string $command): Response
{
return redirect('https://js-dos.com/games/doom.exe.html');
}
}

View file

@ -14,8 +14,9 @@ class Ip implements Command
public function perform(string $command): Response
{
$output = 'Your ip address is ' . request()->ip() . '.';
return response()->view('home.index', ['output'=> $output]);
return response([
'message' => 'Your ip address is ' . request()->ip() . '.',
'type' => 'default',
]);
}
}

View file

@ -14,8 +14,9 @@ class Localhost implements Command
public function perform(string $command): Response
{
flash()->error("Please try someone else's domain.");
return back();
return response([
'message' => 'Please try someone else\'s domain.',
'type' => 'danger',
]);
}
}

View file

@ -21,8 +21,9 @@ class Manual implements Command
"Enter 'doom' to play Doom.",
])->implode('<br>');
flash()->message($manualText, 'info');
return redirect('/');
return response([
'message' => $manualText,
'type' => 'info'
]);
}
}

View file

@ -1,5 +1,6 @@
h1 {
font-size: 1rem;
padding-top: 2rem;
}
.h1__prefix {

49
resources/assets/js/History.js vendored Normal file
View file

@ -0,0 +1,49 @@
class History {
constructor() {
this.importFromLocalStorage();
this.index = 0;
}
add(value) {
if (this.items.length > 1000) {
this.items.pop();
}
this.items.unshift(value);
this.save();
}
importFromLocalStorage() {
const storedHistory = JSON.parse(localStorage.getItem('history'));
this.items = storedHistory || [];
}
save() {
localStorage.setItem('history', JSON.stringify(this.items));
}
getPrevious() {
if (this.index < this.items.length - 1) {
return this.items[this.index++];
}
return this.items[this.items.length - 1];
}
getNext() {
if (this.index > 0) {
return this.items[--this.index];
}
return '';
}
clear() {
this.items = [];
this.save();
}
}
export default History;

View file

@ -1,14 +1,15 @@
const form = document.getElementById('form');
const input = document.getElementById('url');
require('./bootstrap');
const Vue = require('vue');
form.addEventListener('submit', event => {
event.preventDefault();
Vue.component('terminal', require('./components/terminal.vue'));
form.action = input.value.toLowerCase();
form.submit();
const app = new Vue({
el: '#main',
});
const input = document.getElementById('url');
window.addEventListener('click', event => {
event.stopPropagation();

13
resources/assets/js/bootstrap.js vendored Normal file
View file

@ -0,0 +1,13 @@
/** Configuration of Axios for Laravel */
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
const token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
window.axios.defaults.headers.common['Accept'] = 'application/json';
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}

View file

@ -0,0 +1,119 @@
<template>
<div>
<h1
v-if="results.length == 0"
class="title">
<span class="carret">~</span>
dnsrecords.io
</h1>
<div
v-for="result in results">
<h1
v-if="result.type == 'command'"
class="title">
<span class="carret">~</span>
dnsrecords.io $ {{ result.message }}
</h1>
<pre
v-else-if="result.type == 'default'"
class="main__results"
id="results"
v-html="result.message"
></pre>
<p
v-else-if="result.type == 'danger'"
class="alert alert--danger"
v-html="result.message"
></p>
<div
v-else=""
:class="'alert alert--' + result.type"
role="alert"
v-html="result.message"
></div>
</div>
<span class="carret -green">&rarr;</span>
<input
id="url"
name="command"
placeholder="Enter a domain"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
autofocus="autofocus"
spellcheck="false"
v-model="input"
v-on:keyup.enter="execute()"
v-on:keydown.up="previous()"
v-on:keydown.down="next()"
/>
</div>
</template>
<script>
import History from '../History.js';
export default {
updated() {
window.scrollTo(0, document.body.scrollHeight);
},
mounted() {
let hash = window.location.hash;
if ('' !== hash) {
this.input = hash.substr(1);
this.execute();
}
},
data() {
return {
results: [],
input: '',
history: new History()
}
},
methods : {
send(command) {
axios.get('/' + command).then(response => {
this.results.push(response.data);
})
},
previous() {
this.input = this.history.getPrevious();
},
next() {
this.input = this.history.getNext();
},
execute() {
let input = this.input;
this.input = '';
this.history.add(input);
this.results.push({
'message' : input,
'type': 'command',
});
window.location.hash = '#'+input;
switch(input)
{
case 'doom':
window.location.href = "https://js-dos.com/games/doom.exe.html";
return;
case 'clear':
return this.results = [];
case 'history -c':
return this.history.clear();
default:
this.send(input);
}
}
}
}
</script>

View file

@ -1,41 +1,8 @@
@extends('layout.master')
@section('content')
<header class="header">
<h1 class="title">
<span class="carret">~</span>
dnsrecords.io
</h1>
</header>
<main class="main">
@if(isset($output))
<pre class="main__results" id="results">{{ $output }}</pre>
@endif
@if($errors->has('input'))
<p class="alert alert--danger">
{{ $errors->first('input') }}
</p>
@endif
@include('layout._partials.flash')
<form id="form" method="post" action="/">
{{ csrf_field() }}
<span class="carret -green">&rarr;</span>
<input
id="url"
name="command"
placeholder="Enter a domain"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
autofocus="autofocus"
spellcheck="false"
/>
</form>
<main id="main" class="main">
<terminal></terminal>
</main>
<footer class="footer">

View file

@ -1,10 +0,0 @@
@foreach (session('flash_notification', collect())->toArray() as $message)
<div class="alert alert--{{ $message['level'] }}
{{ $message['important'] ? 'alert--important' : '' }}"
role="alert"
>
{!! $message['message'] !!}
</div>
@endforeach
{{ session()->forget('flash_notification') }}

View file

@ -20,6 +20,7 @@
<link rel="manifest" href="/manifest.json">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#151d21">
<meta name="theme-color" content="#ffffff">
<meta name="csrf-token" content="{{ csrf_token() }}">
</head>
<body class="layout">