diff --git a/app/Http/Controllers/TableController.php b/app/Http/Controllers/TableController.php index 4b67cba..d60af5d 100644 --- a/app/Http/Controllers/TableController.php +++ b/app/Http/Controllers/TableController.php @@ -12,6 +12,20 @@ use MightyPork\Exceptions\NotApplicableException; class TableController extends Controller { + public function view(User $user, string $table) + { + /** @var Table $tableModel */ + $tableModel = $user->tables()->where('name', $table)->first(); + $revision = $tableModel->activeRevision; + + return view('table.view', [ + 'table' => $tableModel, + 'revision' => $revision, + 'columns' => Column::columnsFromJson(json_decode($revision->columns)), + 'rows' => $revision->rows, + ]); + } + /** * SHow a form for creating a new table * @@ -20,8 +34,8 @@ class TableController extends Controller public function create() { $exampleColumns = - "latin,string,Latin Name\n". - "common,string,Common Name\n". + "latin,string,Latin Name\n" . + "common,string,Common Name\n" . "lifespan,int,Lifespan (years)"; $exampleData = @@ -82,11 +96,12 @@ class TableController extends Controller $rowTable = array_map('str_getcsv', explode("\n", $request->get('data'))); + // Preparing data to insert into the Rows table $rowsData = null; try { $rowsData = array_map(function ($row) use ($columns) { if (count($row) != count($columns)) { - throw new NotApplicableException("All rows must have ".count($columns)." fields."); + throw new NotApplicableException("All rows must have " . count($columns) . " fields."); } $parsed = []; foreach ($row as $i => $val) { @@ -98,7 +113,7 @@ class TableController extends Controller 'refs' => 1, ]; }, $rowTable); - }catch (\Exception $e) { + } catch (\Exception $e) { return $this->backWithErrors(['columns' => $e->getMessage()]); } @@ -118,6 +133,11 @@ class TableController extends Controller 'origin' => $request->get('origin'), ]); + // Attach the revision to the table, set as current + $table->revisions()->attach($revision); + $table->activeRevision()->associate($revision); + + // Spawn the rows, linked to the revision $revision->rows()->createMany($rowsData); // Now we create rows, a revision pointing to them, and the table using it. diff --git a/app/Models/Table.php b/app/Models/Table.php index d7821d3..c3c229a 100644 --- a/app/Models/Table.php +++ b/app/Models/Table.php @@ -79,7 +79,7 @@ class Table extends Model /** Active revision */ public function activeRevision() { - return $this->hasOne(Revision::class, 'revision_id'); + return $this->belongsTo(Revision::class, 'revision_id'); } /** Proposals submitted to this table */ diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 95e9b5f..9111c52 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\Models\User; use App\View\WidgetFactory; use Illuminate\Support\ServiceProvider; @@ -17,6 +18,13 @@ class AppServiceProvider extends ServiceProvider if($this->app->environment('production')) { \URL::forceScheme('https'); } + + \Route::bind('user', function ($value) { + $u = User::where('name', $value)->first(); + // it may also be the _id directly + if (!$u) $u = User::find($value); + return $u; + }); } /** diff --git a/app/Utils/Column.php b/app/Utils/Column.php index f3d66b2..6c172dc 100644 --- a/app/Utils/Column.php +++ b/app/Utils/Column.php @@ -25,6 +25,13 @@ class Column implements JsonSerializable private $title; private $type; + public static function columnsFromJson($columns) + { + return array_map(function ($x) { + return new Column($x); + }, $columns); + } + public function __get($name) { if (property_exists($this, $name)) { diff --git a/app/View/Widget.php b/app/View/Widget.php index f983a5d..4f5326c 100644 --- a/app/View/Widget.php +++ b/app/View/Widget.php @@ -53,18 +53,32 @@ class Widget $this->$method = $arg; } else { - $this->attributesArray[$method] = $arg; + $this->attributesArray[kebab_case($method)] = $arg; } return $this; } + /** Explicitly set a CSS rule */ public function css($prop, $val) { $this->styleArray[$prop] = $val; return $this; } + /** Explicitly set an attribute */ + public function attr($attr, $val) + { + $this->attributesArray[$attr] = $val; + return $this; + } + + /** Configure a text field for automatic alias generation */ + public function autoAlias($targetName, $delimiter='_') + { + return $this->attr('data-autoalias', $targetName)->attr('data-aa-delimiter', $delimiter); + } + /** Apply a given layout (bootstrap grid, 12 cols total) */ public function layout($labelCols, $fieldCols) { diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 50e0ad6..03a1357 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -6,6 +6,7 @@ */ require('./bootstrap'); +let url_slug = require('./url-slug') $(function () { $('[data-toggle="tooltip"]').tooltip({ @@ -13,6 +14,23 @@ $(function () { }) }) +$(document).on('input keypress paste keyup', 'input[data-autoalias]', function () { + const $this = $(this) + const target_name = $this.data('autoalias') + const delimiter = $this.data('aa-delimiter') || '_' + + const new_alias = url_slug($this.val(), {'delimiter': delimiter}) + + const $target = $(`input[name="${target_name}"]`) + const lastset = $target.data('aa-last-set-val') + + // 1. pick up, or 2. continue + if (new_alias === $target.val() || lastset === $target.val()) { + $target.val(new_alias) + $target.data('aa-last-set-val', new_alias) + } +}) + // // window.Vue = require('vue'); diff --git a/resources/assets/js/url-slug.js b/resources/assets/js/url-slug.js new file mode 100644 index 0000000..7fc22fe --- /dev/null +++ b/resources/assets/js/url-slug.js @@ -0,0 +1,102 @@ +// Source & more langs: https://gist.github.com/sgmurphy/3095196 + + +const char_map = { + // Latin + 'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Ä': 'A', 'Å': 'A', 'Æ': 'AE', 'Ç': 'C', + 'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E', 'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I', + 'Ð': 'D', 'Ñ': 'N', 'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ö': 'O', 'Ő': 'O', + 'Ø': 'O', 'Ù': 'U', 'Ú': 'U', 'Û': 'U', 'Ü': 'U', 'Ű': 'U', 'Ý': 'Y', 'Þ': 'TH', + 'ß': 'ss', + 'à': 'a', 'á': 'a', 'â': 'a', 'ã': 'a', 'ä': 'a', 'å': 'a', 'æ': 'ae', 'ç': 'c', + 'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e', 'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i', + 'ð': 'd', 'ñ': 'n', 'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ö': 'o', 'ő': 'o', + 'ø': 'o', 'ù': 'u', 'ú': 'u', 'û': 'u', 'ü': 'u', 'ű': 'u', 'ý': 'y', 'þ': 'th', + 'ÿ': 'y', + + // Latin symbols + '©': '(c)', + + // Czech + 'Č': 'C', 'Ď': 'D', 'Ě': 'E', 'Ň': 'N', 'Ř': 'R', 'Š': 'S', 'Ť': 'T', 'Ů': 'U', + 'Ž': 'Z', + 'č': 'c', 'ď': 'd', 'ě': 'e', 'ň': 'n', 'ř': 'r', 'š': 's', 'ť': 't', 'ů': 'u', + 'ž': 'z', + + // Polish + 'Ą': 'A', 'Ć': 'C', 'Ę': 'e', 'Ł': 'L', 'Ń': 'N', 'Ś': 'S', 'Ź': 'Z', + 'Ż': 'Z', + 'ą': 'a', 'ć': 'c', 'ę': 'e', 'ł': 'l', 'ń': 'n', 'ś': 's', 'ź': 'z', + 'ż': 'z', +} + +const alnum = new RegExp('[^a-z0-9]+', 'ig') + +/** + * Create a web friendly URL slug from a string. + * + * Although supported, transliteration is discouraged because + * 1) most web browsers support UTF-8 characters in URLs + * 2) transliteration causes a loss of information + * + * @author Sean Murphy + * @copyright Copyright 2012 Sean Murphy. All rights reserved. + * @license http://creativecommons.org/publicdomain/zero/1.0/ + * + * @return string + * @param {string} s + * @param {object} opt + */ +function url_slug (s, opt) { + let k + + s = String(s) + opt = Object(opt) + + const defaults = { + 'delimiter': '-', + 'limit': undefined, + 'lowercase': true, + 'replacements': {}, + 'transliterate': true + } + + // Merge options + for (k in defaults) { + if (defaults.hasOwnProperty(k) && !opt.hasOwnProperty(k)) { + opt[k] = defaults[k] + } + } + + // Make custom replacements + for (k in opt.replacements) { + if (opt.replacements.hasOwnProperty(k)) { + s = s.replace(new RegExp(k, 'g'), opt.replacements[k]) + } + } + + // Transliterate characters to ASCII + if (opt.transliterate) { + for (k in char_map) { + if (char_map.hasOwnProperty(k)) { + s = s.replace(new RegExp(k, 'g'), char_map[k]) + } + } + } + + // Replace non-alphanumeric characters with our delimiter + s = s.replace(alnum, opt.delimiter) + + // Remove duplicate delimiters + s = s.replace(new RegExp('[' + opt.delimiter + ']{2,}', 'g'), opt.delimiter) + + // Truncate slug to max. characters + s = s.substring(0, opt.limit) + + // Remove delimiter from ends + s = s.replace(new RegExp('(^' + opt.delimiter + '|' + opt.delimiter + '$)', 'g'), '') + + return opt.lowercase ? s.toLowerCase() : s +} + +module.exports = url_slug; diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 44a504f..bf25dab 100644 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -38,7 +38,9 @@ No tables yet. @else @foreach($tables as $table) - {{ $table->title }} + {{ + $table->title + }} @endforeach {{ $tables->links() }} @endif diff --git a/resources/views/table/create.blade.php b/resources/views/table/create.blade.php index d38a594..03ac9c7 100644 --- a/resources/views/table/create.blade.php +++ b/resources/views/table/create.blade.php @@ -9,13 +9,13 @@ @csrf
+ {!! Widget::text('title', 'Title')->autoAlias('name', '-') + ->help('Unique among your tables') !!} + {!! Widget::text('name', 'Name')->value('molluscs-'.uniqid()) ->help('Unique among your tables, and part of the URL; only letters, digits and some symbols are allowed.') !!} - {!! Widget::text('title', 'Title') - ->help('Unique among your tables') !!} - {!! Widget::textarea('description', 'Description')->height('8em') ->help('Description what data is in the table. Please use the dedicated fields for License and data source. URLs in a full format will be clickable.') !!} diff --git a/resources/views/table/view.blade.php b/resources/views/table/view.blade.php new file mode 100644 index 0000000..2e425ee --- /dev/null +++ b/resources/views/table/view.blade.php @@ -0,0 +1,39 @@ +@extends('layouts.app') + +@php +/** @var \App\Models\Table $table */ +@endphp + +@section('content') +
+
+

{{ $table->title }}

+
+
+ +
+ + + + + @foreach($columns as $col) + + @endforeach + + + + @foreach($rows as $row) + + + @php($rdata = json_decode($row['data'], true)) + @foreach($columns as $col) + + @endforeach + + @endforeach + +
ID{{ $col->title }}
#{{ $row->id }}{{ $rdata[$col->name] }}
+
+
+
+@endsection diff --git a/routes/public.php b/routes/public.php index 3dd59a4..38fa3b1 100644 --- a/routes/public.php +++ b/routes/public.php @@ -17,3 +17,6 @@ Route::get('/', function () { } return view('welcome'); }); + +// Table resource +Route::get('{user}/{table}', 'TableController@view')->name('table.view');