From 001210c3bd3b60736cabcd02b983d6259b7543f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrej=20Rama=C5=A1euski?= <andrej@x2.cz> Date: Fri, 6 Jan 2023 01:33:04 +0100 Subject: [PATCH] Kompletni refaktoring --- TODO | 2 +- VERSION | 2 +- lib/PZ.pm | 6 +- lib/PZ/Controller/Log.pm | 17 +- lib/PZ/Controller/Shortcut.pm | 144 ++++-------- lib/PZ/Controller/Stat.pm | 10 +- lib/PZ/Helpers/Core.pm | 25 ++- lib/PZ/Schema/Result/Log.pm | 9 + lib/PZ/Schema/Result/Shortcut.pm | 1 + openapi.yaml | 7 +- sql/migrations.sql | 3 + templates/index.html.ep | 10 +- templates/layouts/default.html.ep | 29 +-- templates/log.csv.html.ep | 2 +- templates/shortcut.html.ep | 128 +++-------- templates/shortcut/created.html.ep | 14 -- templates/shortcut/form.html.ep | 30 --- templates/shortcut/invalid.html.ep | 4 - templates/shortcuts-vue.html.ep | 53 ----- templates/shortcuts.html.ep | 348 +++++++++++++++++++++-------- 20 files changed, 400 insertions(+), 444 deletions(-) delete mode 100644 templates/shortcut/created.html.ep delete mode 100644 templates/shortcut/form.html.ep delete mode 100644 templates/shortcut/invalid.html.ep delete mode 100644 templates/shortcuts-vue.html.ep diff --git a/TODO b/TODO index 30ecaf9..a4bc869 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,3 @@ -- statistika - loop detect - 302 nebo informacni stranka +- https://apexcharts.com/ diff --git a/VERSION b/VERSION index f0bb29e..227cea2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.0 +2.0.0 diff --git a/lib/PZ.pm b/lib/PZ.pm index cb5f488..3fa671a 100644 --- a/lib/PZ.pm +++ b/lib/PZ.pm @@ -102,10 +102,8 @@ sub startup { $r->get('/logout')->to('OIDC#do_logout'); $r->get('/')->to(cb => sub { shift->render('index'); }); - $r->post('/')->to('Shortcut#create'); - $r->get('/shortcuts')->to(cb => sub { shift->render('shortcuts-vue'); }); - $r->get('/shortcuts/:id')->to('Shortcut#get1'); - $r->get('/shortcuts/:id/log.csv')->to('Log#csv'); + $r->get('/shortcut/:id')->to(cb => sub { shift->render('shortcut'); }); + $r->get('/shortcut/:id/log.csv')->to('Log#csv'); $r->get('/:shortcut')->to('Shortcut#redirect'); $r->get('/:shortcut/qr.png')->to('Shortcut#qr'); diff --git a/lib/PZ/Controller/Log.pm b/lib/PZ/Controller/Log.pm index 3a66a82..0799f66 100644 --- a/lib/PZ/Controller/Log.pm +++ b/lib/PZ/Controller/Log.pm @@ -4,11 +4,7 @@ use Mojo::Base 'Mojolicious::Controller', -signatures; sub main ($c) { $c->openapi->valid_input or return; - my $shortcut = $c->stash->{user}->shortcuts({ - id => $c->stash->{id} - })->first; - - return $c->error(404, 'NOT_FOUND') if ! $shortcut; + my $shortcut = $c->shortcut() || return; my ($log, @result); @@ -22,9 +18,10 @@ sub main ($c) { ITEM: while ( my $item = $log->next ) { + push @result, { time => $item->time, - ip => $item->ip, + ip => $item->anonymized_ip, ua => $item->ua, referrer => $item->referrer, }; @@ -35,11 +32,7 @@ sub main ($c) { sub csv ($c) { - my $shortcut = $c->current_user->shortcuts({ - id => $c->stash->{id} - })->first; - - return $c->error(404, 'NOT_FOUND') if ! $shortcut; + my $shortcut = $c->shortcut() || return; my ($log, @result); @@ -58,5 +51,5 @@ sub csv ($c) { log => $log, ); } -1; +1; diff --git a/lib/PZ/Controller/Shortcut.pm b/lib/PZ/Controller/Shortcut.pm index e7e4941..82133dc 100644 --- a/lib/PZ/Controller/Shortcut.pm +++ b/lib/PZ/Controller/Shortcut.pm @@ -4,6 +4,8 @@ use Mojo::Base 'Mojolicious::Controller', -signatures; use Data::Validate::URI qw(is_uri); use Image::PNG::QRCode 'qrpng'; +use constant SHORTCUT => qr/^[a-z\d\-]{1,8}$/; + sub redirect ($c) { my $shortcut = $c->schema->resultset('Shortcut')->search({ shortcut => $c->stash->{shortcut}, @@ -16,6 +18,10 @@ sub redirect ($c) { return; } + $shortcut->update({ + counter => $shortcut->counter + 1 + }); + $shortcut->add_to_log_items({ ip => ($c->forwarded_for || $c->tx->remote_address), ua => $c->req->headers->user_agent, @@ -27,65 +33,62 @@ sub redirect ($c) { } sub create ($c) { - my $url = $c->param('url'); + $c->openapi->valid_input or return; + my $args = $c->req->json; - if( ! is_uri($url) ) { - $c->render('shortcut/invalid', error => 'Chybná URL adresa!'); - return; - } + my $url = $args->{url}; + return $c->error(400, 'Chybná URL adresa!') if ! is_uri($url); - my $custom = lc($c->param('shortcut')); - - if ( ! $custom =~ /^[a..z0..9]{1,8}$/i ) { - $c->render('shortcut/invalid', error => 'Neplatná zkratka'); - return; - } + my $custom = lc($args->{shortcut}); if ( $custom ) { + return $c->error(400, 'Neplatná zkratka') if $custom !~ SHORTCUT; + my $exists = $c->schema->resultset('Shortcut')->search({ - is_active => 1, deleted => undef, shortcut => $custom, })->count; - if( $exists ) { - $c->render('shortcut/invalid', error => "Zkratka $custom uĹľ je pouĹľitá"); - return; - } + return $c->error(400, "Zkratka $custom uĹľ je pouĹľitá") if $exists; } - my %data = ( - deleted => undef, - url => $url, - ); - - my $shortcut = $c->current_user->shortcuts(\%data)->first; - - if ( ! $shortcut ) { - $data{shortcut} = $custom || $c->schema->resultset('Shortcut')->generate(); - $shortcut = $c->current_user->add_to_shortcuts(\%data); - } + my $shortcut = $c->user->add_to_shortcuts({ + url => $c->sanitize_url($url), + shortcut => ($custom || $c->schema->resultset('Shortcut')->generate()) + }); - $c->render('shortcut/created', - url => 'https://' . $c->config->{domain} . '/' . $shortcut->shortcut, - shortcut => $shortcut - ); + $c->render( + status => 201, + openapi => { id => $shortcut->id }, + ); } +sub get ($c) { + my $shortcut = $c->shortcut() || return; + $c->render(json => { $shortcut->get_columns } ); +} -sub qr ($c) { - my $url = 'https://' . $c->config->{domain} . '/' . $c->stash->{shortcut}; - my $png = qrpng (text => $url, level => 4); - $c->res->headers->content_type('image/png'); - $c->render( data => $png ); +sub update ($c) { + my $shortcut = $c->shortcut() || return; + return $c->error(400, 'Chybná URL adresa!') if ! is_uri($c->req->json->{url}); + $shortcut->update({ + code => $c->req->json->{code}, + url => $c->req->json->{url}, + }); + $c->render(status => 204, text => '' ); } +sub delete ($c) { + my $shortcut = $c->shortcut() || return; + $shortcut->update({ deleted => \'now()' }); + $c->render(status => 204, text => '' ); +} sub list ($c) { my @shortcuts = (); SHORTCUT: - foreach my $shortcut ( $c->stash->{user}->shortcuts( + foreach my $shortcut ( $c->user->shortcuts( { deleted => undef }, { order_by => 'shortcut' }, ) ) { @@ -97,69 +100,12 @@ sub list ($c) { $c->render(json => \@shortcuts, ); } -sub get ($c) { - - my $shortcut = $c->stash->{user}->shortcuts({ - id => $c->stash->{id} - })->first; - - if ( ! $shortcut ) { - $c->render( status => 404, text => 'not found' ); - return; - } - $c->render(json => { $shortcut->get_columns } ); -} - -sub get1 ($c) { - - my $shortcut = $c->current_user->shortcuts({ - id => $c->stash->{id} - })->first; - - if ( ! $shortcut ) { - $c->render( status => 404, text => 'not found' ); - return; - } - - $c->stash->{shortcut} = $shortcut; - $c->stash->{template} = 'shortcut'; -} -sub update ($c) { - - my $shortcut = $c->stash->{user}->shortcuts({ - id => $c->stash->{id} - })->first; - - if ( ! $shortcut ) { - $c->render( status => 404, text => 'not found' ); - return; - } - - $shortcut->update({ - code => $c->req->json->{code}, - url => $c->req->json->{url}, - }); - - $c->render(status => 204, text => '' ); -} - -sub delete ($c) { - - my $shortcut = $c->stash->{user}->shortcuts({ - id => $c->stash->{id} - })->first; - - if ( ! $shortcut ) { - $c->render( status => 404, text => 'not found' ); - return; - } - - $shortcut->update({ - deleted => \'now()' - }); - - $c->render(status => 204, text => '' ); +sub qr ($c) { + my $url = 'https://' . $c->config->{domain} . '/' . $c->stash->{shortcut}; + my $png = qrpng (text => $url, level => 4, scale => 5); + $c->res->headers->content_type('image/png'); + $c->render( data => $png ); } 1; diff --git a/lib/PZ/Controller/Stat.pm b/lib/PZ/Controller/Stat.pm index 504a980..0e03ad2 100644 --- a/lib/PZ/Controller/Stat.pm +++ b/lib/PZ/Controller/Stat.pm @@ -4,11 +4,7 @@ use Mojo::Base 'Mojolicious::Controller', -signatures; use DateTime; sub main ($c) { - my $shortcut = $c->stash->{user}->shortcuts({ - id => $c->stash->{id} - })->first; - - return $c->error(404, 'NOT_FOUND') if ! $shortcut; + my $shortcut = $c->shortcut() || return; my %tz = (time_zone => 'Europe/Prague'); my $begin = DateTime->from_epoch(%tz, epoch => $c->param('begin') / 1000); @@ -16,9 +12,9 @@ sub main ($c) { my ($stat, @result); - $stat = $shortcut->stat_daily_items(period => { + $stat = $shortcut->stat_daily_items({period => { between => [$begin, $end] - }); + }}); ITEM: while ( my $item = $stat->next ) { diff --git a/lib/PZ/Helpers/Core.pm b/lib/PZ/Helpers/Core.pm index f566d9a..3e69092 100644 --- a/lib/PZ/Helpers/Core.pm +++ b/lib/PZ/Helpers/Core.pm @@ -3,6 +3,7 @@ package PZ::Helpers::Core; use base 'Mojolicious::Plugin'; use YAML; +use Mojo::URL; sub register { my ($class, $self ) = @_; @@ -12,8 +13,6 @@ sub register { my $status = shift; my $errors = []; - $c->cirpack_ws->rollback(); - if ( scalar @_ == 2 ) { $errors = [{ code => shift, message => shift }]; } @@ -80,6 +79,28 @@ sub register { return $data; }); + $self->helper( shortcut => sub { + my $c = shift; + my $id = shift // $c->stash->{id}; + my $shortcut = $c->schema->resultset('Shortcut')->find({ id => $id }); + return $c->error(404, 'NOT_FOUND') if ! $shortcut; + return $c->error(403, 'ACCESS_DENIED') if $shortcut->user_id != $c->stash->{user}->id; + return $shortcut; + }); + + $self->helper( sanitize_url => sub { + my $c = shift; + + my $url = Mojo::URL->new(shift); + $url->query({ fbclid => undef }); + + return $url->to_string; + }); + + $self->helper( user => sub { + my $c = shift; + return $c->stash->{user}; + }); } 1; diff --git a/lib/PZ/Schema/Result/Log.pm b/lib/PZ/Schema/Result/Log.pm index 57686e5..27a169d 100644 --- a/lib/PZ/Schema/Result/Log.pm +++ b/lib/PZ/Schema/Result/Log.pm @@ -25,6 +25,15 @@ __PACKAGE__->add_columns( ), ); +sub anonymized_ip { + my $self = shift; + + my $ip = $self->ip; + $ip =~ s/\d+\.\d+$/X.X/; + + return $ip; +} + __PACKAGE__->set_primary_key('id'); 1; diff --git a/lib/PZ/Schema/Result/Shortcut.pm b/lib/PZ/Schema/Result/Shortcut.pm index e5219d1..54d39d6 100644 --- a/lib/PZ/Schema/Result/Shortcut.pm +++ b/lib/PZ/Schema/Result/Shortcut.pm @@ -28,6 +28,7 @@ __PACKAGE__->add_columns( code title description + counter ), ); diff --git a/openapi.yaml b/openapi.yaml index c620092..1dac0c8 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3,7 +3,7 @@ openapi: 3.0.3 info: title: PiratskĂ˝ zkracovaÄŤ description: PiratskĂ˝ zkracovaÄŤ API - version: 1.0.0 + version: 1.4.0 license: name: Artistic License 2.0 url: https://www.perlfoundation.org/artistic-license-20.html @@ -38,6 +38,9 @@ components: type: integer description: KĂłd pĹ™esmÄ›rovánĂ enum: [301, 302] + counter: + type: integer + readOnly: true StatisticItem: type: object properties: @@ -78,7 +81,7 @@ paths: - Token: [] summary: "Pridat zkratku" operationId: ShortcutCreate - x-mojo-to: shortcut#create1 + x-mojo-to: shortcut#create requestBody: required: true content: diff --git a/sql/migrations.sql b/sql/migrations.sql index 0a31e8d..6b1bb5c 100644 --- a/sql/migrations.sql +++ b/sql/migrations.sql @@ -71,3 +71,6 @@ from "log" group by "shortcut_id", date_trunc('day', "time") order by date_trunc('day', "time") ; + +-- 6 up +alter table "shortcuts" add column "counter" integer not null default 0; diff --git a/templates/index.html.ep b/templates/index.html.ep index e968304..0e41678 100644 --- a/templates/index.html.ep +++ b/templates/index.html.ep @@ -8,14 +8,12 @@ kterĂ˝ pĹ™ipojĂ ke svĂ© vstupnĂ URL, takĹľe vĂ˝sledná adresa je mnohem kratš pouĹľije (napĹ™. na ni pĹ™ejde prohlĂĹľeÄŤem), zjistĂ nejprve zkracovaÄŤ podle pouĹľitĂ©ho kĂłdu z databáze pĹŻvodnĂ odkaz a následnÄ› návštÄ›vnĂka na pĹŻvodnĂ adresu pĹ™esmÄ›ruje, uĹľivatel tak tĂ©měř nepozná rozdĂl oproti uĹľitĂ celĂ© pĹŻvodnĂ adresy. -% if ( ! $c->is_user_authenticated ) { -Pro pouĹľiti PirátskĂ©ho zkracovaÄŤe musĂte se pĹ™ihliásit <strong><a href="<%= $c->oidc->authorize %>">Pirátskou identitou</a></strong> +% if ( ! is_user_authenticated ) { +Pro pouĹľiti PirátskĂ©ho zkracovaÄŤe musĂte se pĹ™ihliásit <strong><a href="<%= oidc->authorize %>">Pirátskou identitou</a></strong> % } </p> </div> -% if ( $c->is_user_authenticated ) { -<div class="content-block"> -%= include 'shortcut/form'; -</div> +% if ( is_user_authenticated ) { +%= include 'shortcuts'; % } diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep index 6a96274..44d69cd 100644 --- a/templates/layouts/default.html.ep +++ b/templates/layouts/default.html.ep @@ -22,14 +22,14 @@ <title><%= config->{name} %></title> <link rel="manifest" href="/manifest.json"/> <link rel="stylesheet" href="<%= config->{styleguide} %>css/styles.css"/> + <script src="https://cdn-unpkg.pirati.cz/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> - <script src="https://cdn-unpkg.pirati.cz/htmx.org@1.7.0" integrity="sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" crossorigin="anonymous"></script> + <link type="text/css" rel="stylesheet" href="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid.min.css" /> + <link type="text/css" rel="stylesheet" href="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid-theme.min.css" /> + <script type="text/javascript" src="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid.min.js"></script> - <link type="text/css" rel="stylesheet" href="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid.min.css" /> - <link type="text/css" rel="stylesheet" href="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid-theme.min.css" /> - <script type="text/javascript" src="https://cdn-cdnjs-cloudflare.pirati.cz/ajax/libs/jsgrid/1.5.3/jsgrid.min.js"></script> - <script src="https://cdn-unpkg.pirati.cz/vue@2.7.8/dist/vue.min.js"></script> - <script src="<%= config->{styleguide} %>js/main.bundle.js"></script> + <script src="https://cdn-unpkg.pirati.cz/vue@2.7.8/dist/vue.min.js"></script> + <script src="<%= config->{styleguide} %>js/main.bundle.js"></script> <link rel="stylesheet" href="/custom.css"/> </head> @@ -41,10 +41,8 @@ <div> <div class="container container--wide navbar__content" :class="{'navbar__content--initialized': true}"> <div class="navbar__brand my-4 flex items-center lg:pr-8 lg:my-0"> - <a href="#"> - <img src="<%= config->{styleguide} %>/images/logo-round-white.svg" class="w-8" /> - </a> - <span class="pl-4 font-bold text-xl lg:pr-8"><%= config->{name} %></span> + <a href="/"><img src="<%= config->{styleguide} %>/images/logo-round-white.svg" class="w-8" /></a> + <span class="pl-4 font-bold text-xl lg:pr-8"><a href="/"><%= config->{name} %></a></span> </div> <div class="navbar__menutoggle my-4 flex justify-end lg:hidden"> <a href="#" @click="show = !show" class="no-underline hover:no-underline"> @@ -54,17 +52,6 @@ <div v-if="show || isLgScreenSize" class="navbar__main navbar__section navbar__section--expandable container-padding--zero lg:container-padding--auto flex items-center"> <div class="flex-grow"> -% if ( is_user_authenticated ) { - <ul class="navbar-menu text-white"> - <li class="navbar-menu__item"> - <a href="/" class="navbar-menu__link">Nová zkratka</a> - </li> - <li class="navbar-menu__item"> - <a href="/shortcuts" class="navbar-menu__link">Moje zkratky</a> - </li> - </ul> -% } - </div> <div class="flex items-center space-x-4"> % if ( is_user_authenticated ) { diff --git a/templates/log.csv.html.ep b/templates/log.csv.html.ep index 588624b..9050f20 100644 --- a/templates/log.csv.html.ep +++ b/templates/log.csv.html.ep @@ -1,2 +1,2 @@ -"datum a ÄŤas","ip adresa","browser","referrer"<%= "\x0D\x0A" =%><% while (my $entry = $log->next) { =%>"<%= $entry->time =%>","<%= $entry->ip =%>","<%= $entry->ua =%>","<%= $entry->referrer =%>"<%= "\x0D\x0A" =%><% } =%> +"datum a ÄŤas","ip adresa","browser","referrer"<%= "\x0D\x0A" =%><% while (my $entry = $log->next) { =%>"<%= $entry->time =%>","<%= $entry->anonymized_ip =%>","<%= $entry->ua =%>","<%= $entry->referrer =%>"<%= "\x0D\x0A" =%><% } =%> diff --git a/templates/shortcut.html.ep b/templates/shortcut.html.ep index 2081706..49f2f26 100644 --- a/templates/shortcut.html.ep +++ b/templates/shortcut.html.ep @@ -1,67 +1,11 @@ % layout 'default'; +<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> +<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script> -<h1 class="head-alt-md md:head-alt-lg max-w-5xl mb-8">Zkratka "<%= $shortcut->shortcut %>"</h1> -% if (0) { -<h2 class="head-alt-sm my-4">Editace zkratky</h2> - -<form id="Edit" class="mb-8"> - -<div class="form-field form-field--error form-field--required mb-4"> - <label class="form-field__label" for="url">URL</label> - <div class="form-field__wrapper form-field__wrapper--shadowed"> - <input type="text" class="text-input form-field__control form-field--required" value="" v-model="shortcut.url" id="url" /> - </div> - <div class="form-field__error" v-if="shortcut.url === ''"></div> -</div> - -<div class="form-field form-field--error form-field--required mb-4"> - <label class="form-field__label" for="code">Typ pĹ™esmÄ›rovánĂ</label> - <div class="form-field__wrapper form-field__wrapper--shadowed select"> - <select v-model="shortcut.code" class="select__control form-field__control "> - <option value="301">301 trvalĂ©</option> - <option value="302">302 doÄŤasnĂ©</option> - </select> - </div> - <div class="form-field__error" v-if="shortcut.url === ''"></div> +<div id="App"> +<h1 class="head-base mb-8">Zkratka "{{ shortcut.shortcut }} ({{ shortcut.url }})"</h1> </div> -<button class="btn Xbtn--blue-300 btn--hoveractive" @click.prevent="updateShortcut"> - <div class="btn__body">UloĹľit zmÄ›ny</div> -</button> -<button class="btn Xbtn--red-600 btn--hoveractive" @click.prevent="delete_confirm_visible=true"> - <div class="btn__body">Smazat zkratku</div> -</button> - -<div class="modal__overlay toggle-modal-sample-1" id="modal-sample-1" v-if="delete_confirm_visible" v-cloak> - <div class="modal__content" role="dialog"> - <div class="modal__container w-full max-w-2xl" role="dialog"> - <div class="modal__container-body elevation-10"> - <button class="modal__close" title="ZavĹ™Ăt" @click.prevent="delete_confirm_visible = false"><i class="ico--cross"></i></button> - <div class="card "> - <div class="card__body "> - <h1 class="card-headline mb-2">Smazat zratku</h1> - <p class="card-body-text my-8"> - Opravdu chcete smazat zkratku? - </p> - - <p class="card-body-text"> - <button class="btn btn--green-300 btn--hoveractive text-sm" @click.prevent="deleteMeet()"> - <div class="btn__body"><i class="btn__inline-icon ico--bin"></i>ANO</div> - </button> - <button class="btn btn--orange-400 btn--hoveractive text-sm" @click.prevent="delete_confirm_visible = false"> - <div class="btn__body"><i class="btn__inline-icon ico--cross"></i>NE</div> - </button> - </p> - - </div> - </div> - - </div> - </div> - </div> -</div> -</form> -% } <h2 class="head-alt-sm my-4">Statistika navštÄ›v</h2> <div class="mb-8" style="width:100%; height: 600px;"><canvas id="stat"></canvas></div> @@ -70,16 +14,20 @@ <h2 class="head-alt-sm">Log navštÄ›v</h2> <div> <button class="btn btn--orange-400 btn--hoveractive text-sm"> - <div class="btn__body"><i class="btn__inline-icon ico--download"></i><a href="/shortcuts/<%= $shortcut->id %>/log.csv">Stahnout CSV</a></div> + <div class="btn__body"><i class="btn__inline-icon ico--download"></i><a href="/shortcuts/<%= stash->{id} %>/log.csv">Stahnout CSV</a></div> </button> </div> </div> <div id="log"></div> -<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> -<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns/dist/chartjs-adapter-date-fns.bundle.min.js"></script> - <script> + const BASE_URL = "/api/shortcuts/<%= stash->{id} %>"; + const API_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", + "X-Auth-Token": "<%= current_user->token %>", + }; + const end = Date.now(); const begin = Date.now() - 31 * 24 * 3600 * 1000; @@ -125,17 +73,7 @@ } }); - fetch('/api/shortcuts/<%= $shortcut->id %>/stat/?begin=' + begin + '&end=' + end , { - headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' }, - }) - .then((response) => response.json()) - .then((data) => { - chart.data.datasets[0].data = data; - chart.update(); - } - ); - - $("#log").jsGrid({ + const log = $("#log").jsGrid({ width: "100%", height: "auto", @@ -150,9 +88,9 @@ loadData: function(filter) { return $.ajax({ - url: "/api/shortcuts/<%= $shortcut->id %>/log/", + url: BASE_URL + "/log/", + headers: API_HEADERS, dataType: "json", - headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' }, data: filter, }); }, @@ -165,28 +103,16 @@ ] }); -// form -% if (0) { - - - const BASE_URL = "/api/shortcuts"; - const API_HEADERS = { - "Content-Type": "application/json", - "Accept": "application/json", - "X-Auth-Token": "<%= current_user->token %>", - }; - - var form = new Vue({ - el: '#Edit', + var app = new Vue({ + el: '#App', data: { - shortcut: [], - delete_confirm_visible: false, + shortcut: {} }, methods: { fetchData: function() { - fetch(BASE_URL + '/<%= $shortcut->id %>', { + fetch(BASE_URL, { headers: API_HEADERS, }) .then((res) => res.json()) @@ -194,13 +120,21 @@ this.shortcut = res; }) }, + + } }); - form.fetchData(); -% } -</script> - + app.fetchData(); + fetch(BASE_URL + '/stat/?begin=' + begin + '&end=' + end , { + headers: API_HEADERS, + }) + .then((response) => response.json()) + .then((data) => { + chart.data.datasets[0].data = data; + chart.update(); + } + ); </script> diff --git a/templates/shortcut/created.html.ep b/templates/shortcut/created.html.ep deleted file mode 100644 index 2eea1a6..0000000 --- a/templates/shortcut/created.html.ep +++ /dev/null @@ -1,14 +0,0 @@ -<p> -<div class="card elevation-4"> - <div class="card__body"> -<p> -ZkrácenĂ˝ URL: <strong><a href="<%= $url %>"><%= $url %></a></strong> -</p> - -<p> -<img src="/<%= $shortcut->shortcut %>/qr.png"> -</p> - - </div> -</div> -</p> diff --git a/templates/shortcut/form.html.ep b/templates/shortcut/form.html.ep deleted file mode 100644 index 1c84a69..0000000 --- a/templates/shortcut/form.html.ep +++ /dev/null @@ -1,30 +0,0 @@ -<form hx-post="/" hx-target="#response" hx-params="*"> - - <div class="card elevation-4 space-y-4 mt-2"> - <div class="card__body"> - <div class="grid grid-cols-12 gap-4 row-gap-6"> - -<div class="form-field col-span-9"> - <label class="form-field__label" for="url">URL</label> - <div class="form-field__wrapper form-field__wrapper--shadowed"> - <input type="text" name="url" class="text-input form-field__control" value="" placeholder="https://www.pirati.cz/program/dlouhodoby/psychotropni-latky/" required="required" /> - </div> -</div> - -<div class="form-field col-span-3"> - <label class="form-field__label" for="shortcut">Zkratka</label> - <div class="form-field__wrapper form-field__wrapper--shadowed"> - <input type="text" name="shortcut" class="text-input form-field__control w-40" size="8" maxlength="8" value="" placeholder="thc" /> - <button class="btn btn--grey-125 btn--hoveractive ml-4"> - <div class="btn__body">Zkrátit</div> - </button> - </div> -</div> - - </div> - </div> -</div> - -</form> - -<div id="response"></div> diff --git a/templates/shortcut/invalid.html.ep b/templates/shortcut/invalid.html.ep deleted file mode 100644 index d21bf2b..0000000 --- a/templates/shortcut/invalid.html.ep +++ /dev/null @@ -1,4 +0,0 @@ -<span class="alert alert--red-600 alert--faded"> - <span><%= $error %></span> -</span> - diff --git a/templates/shortcuts-vue.html.ep b/templates/shortcuts-vue.html.ep deleted file mode 100644 index e94e200..0000000 --- a/templates/shortcuts-vue.html.ep +++ /dev/null @@ -1,53 +0,0 @@ -% layout 'default'; - -<h1 class="head-alt-md md:head-alt-lg max-w-5xl mb-8">Moje zkratky</h1> - -<table id="Shortcuts" class="table table-fixed table--striped" v-cloak> - <thead> - <tr> - <th>Zkratka</th> - <th>URL</th> - <th>PĹ™esmÄ›rovánĂ</th> - </tr> - </thead> - <tbody> - <tr v-for="shortcut in shortcuts" class="cursor-pointer" @click="window.location.href ='/shortcuts/' + shortcut.id"> - <td class="head-xs">{{shortcut.shortcut}}</td> - <td class="w-96">{{ shortcut.url }}</td> - <td class="">{{ shortcut.code == 301 ? '301 trvalĂ©' : '302 doÄŤasnĂ©'}}</td> - </tr> - </tbody> - -</table> - -<script type="module"> - const BASE_URL = "/api/shortcuts"; - const API_HEADERS = { - "Content-Type": "application/json", - "Accept": "application/json", - "X-Auth-Token": "<%= current_user->token %>", - }; - - var list = new Vue({ - el: '#Shortcuts', - - data: { - shortcuts: [], - }, - - methods: { - fetchData: function() { - fetch(BASE_URL, { - headers: API_HEADERS, - }) - .then((res) => res.json()) - .then(res => { - this.shortcuts = res; - }) - }, - } - }); - - list.fetchData(); - -</script> diff --git a/templates/shortcuts.html.ep b/templates/shortcuts.html.ep index 0ddf381..53c0b2a 100644 --- a/templates/shortcuts.html.ep +++ b/templates/shortcuts.html.ep @@ -1,100 +1,268 @@ -% layout 'default'; +<div id="App"> +<form @submit.prevent="addShortcut" v-cloak> -<h1 class="head-alt-md md:head-alt-lg max-w-5xl mb-8">Moje zkratky</h1> + <div class="card elevation-4 space-y-2 my-4"> + <div class="card__body"> + <div class="grid grid-cols-12 gap-4 row-gap-6"> -<div id="jsGrid"></div> + <div class="form-field col-span-9"> + <label class="form-field__label" for="url">URL</label> + <div class="form-field__wrapper form-field__wrapper--shadowed"> + <input type="text" name="url" class="text-input form-field__control" value="" placeholder="https://www.pirati.cz/program/dlouhodoby/psychotropni-latky/" required="required" v-model="shortcut.url" @focus="formError=''"/> + </div> + </div> -<script> + <div class="form-field col-span-3"> + <label class="form-field__label" for="shortcut">Zkratka</label> + <div class="form-field__wrapper form-field__wrapper--shadowed"> + <input type="text" name="shortcut" class="text-input form-field__control w-40" size="8" maxlength="8" value="" placeholder="thc" v-model="shortcut.shortcut" @focus="formError=''"/> + <button class="btn btn--grey-125 btn--hoveractive ml-4"> + <div class="btn__body">Zkrátit</div> + </button> + </div> + </div> -jsGrid.validators.url = { - message: 'Zadejte platnĂ˝ URL', - validator: function(value, item) { - let url; + </div><%# grid %> + </div><%# card__body %> + </div><%# card %> + <div v-if="formError" class="my-4"><span class="alert alert--red-600 alert--faded">{{ formError }}</span></div> +</form> - try { - url = new URL(value); - } catch (_) { - return false; - } +<table class="table table--striped table--bordered table-auto w-full" v-cloak> + <thead> + <tr> + <th class="text-left w-16">Zkratka</th> + <th class="text-left">URL</th> + <th class="text-left w-16">PĹ™esmÄ›rovánĂ</th> + <th class="text-left w-16">Kliky</th> + <th class="text-left w-40"></th> + </tr> + </thead> + <tbody> + <tr v-for="shortcut in shortcuts" > + <td class="text-bold" @click="showEdit(shortcut)" >{{shortcut.shortcut}}</td> + <td v-bind:title="shortcut.url" @click="showEdit(shortcut)" >{{ stripURL(shortcut.url) }}</td> + <td @click="showEdit(shortcut)">{{ shortcut.code == 301 ? '301 trvalĂ©' : '302 doÄŤasnĂ©'}}</td> + <td @click="showEdit(shortcut)" class="text-right">{{shortcut.counter}}</td> + <td> + <i class="ico--link cursor-pointer mx-1" @click="showInfo(shortcut)" title="Zkopirovat odkaz"></i> + <i class="ico--calendar cursor-pointer mx-1" @click="window.location.href ='/shortcut/' + shortcut.id" title="Statistika"></i> + <i class="ico--equalizer cursor-pointer mx-1" @click="showEdit(shortcut)" title="Editovat"></i> + <i class="ico--bin cursor-pointer mx-1" @click="showDelete(shortcut)" title="Smazat"></i> + </td> + </tr> + </tbody> +</table> - return url.protocol === "http:" || url.protocol === "https:"; - } -} - -$(function() { - $("#jsGrid").jsGrid({ - width: "100%", - height: "auto", - - inserting: false, - editing: true, - sorting: true, - paging: true, - selecting: false, - autoload: true, - invalidMessage: 'Neplatná data', - deleteConfirm: 'Opravdu chcete smazat zkratku?', - - controller: { - loadData: function(filter) { - - return $.ajax({ - url: "/api/shortcuts", - dataType: "json", - headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' }, - data: filter, - }); - }, - - insertItem: function(item) { - return $.ajax({ - type: "POST", - dataType: "json", - headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' }, - url: "/api/shortcuts", - data: JSON.stringify(item) - }); - }, - - updateItem: function(item) { - return $.ajax({ - type: "PUT", - dataType: "json", - headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' }, - url: "/api/shortcuts/" + item.id, - data: JSON.stringify(item) - }); - }, - - deleteItem: function(item) { - return $.ajax({ - type: "DELETE", - headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' }, - url: "/api/shortcuts/" + item.id, - }); - }, +<div class="modal__overlay" v-if="shortcutInfoVisible" v-cloak> + <div class="modal__content" role="dialog"> + <div class="modal__container" role="dialog"> + <div class="modal__container-body elevation-10"> + <button class="modal__close" title="ZavĹ™Ăt" @click="shortcutInfoVisible = false"><i class="ico--cross"></i></button> + <div class="card"> + <div class="card__body w-80 text-center"> + <p class="card-body-text">Zkratka byla zkopĂrovana do schránky. Pro jejĂ sdĂlenĂ mĹŻĹľete pouĹľĂvat i QR kĂłd</p> + <img v-bind:src="'/' + selectedShortcut.shortcut + '/qr.png'" class="mx-auto my-8"> + <b>{{ selectedShortcut.full_url }}</b> + </div> + </div> + </div> + </div> + </div> +</div> + +<div class="modal__overlay" v-if="deleteConfirmVisible"> + <div class="modal__content" role="dialog"> + <div class="modal__container" role="dialog"> + <div class="modal__container-body elevation-10"> + <button class="modal__close" title="ZavĹ™Ăt" @click.prevent="deleteConfirmVisible = false"><i class="ico--cross"></i></button> + <div class="card"> + <div class="card__body"> + <h1 class="card-headline mb-2">Smazat zkratku</h1> + <p class="card-body-text my-8">Opravdu chcete smazat zkratku?</p> + + <p class="card-body-text"> + <button class="btn btn--red-600 btn--hoveractive text-sm" @click.prevent="deleteShortcut(selectedShortcut)"> + <div class="btn__body"><i class="btn__inline-icon ico--bin"></i>ANO</div> + </button> + <button class="btn btn--grey-500 btn--hoveractive text-sm" @click.prevent="deleteConfirmVisible = false"> + <div class="btn__body"><i class="btn__inline-icon ico--cross"></i>NE</div> + </button> + </p> + + </div> + </div> + + </div> + </div> + </div> +</div> + +<div class="modal__overlay" v-if="shortcutEditVisible"> + <div class="modal__content" role="dialog"> + <div class="modal__container w-full max-w-2xl" role="dialog"> + <div class="modal__container-body elevation-10"> + <button class="modal__close" title="ZavĹ™Ăt" @click.prevent="shortcutEditVisible = false"><i class="ico--cross"></i></button> + <div class="card"> + <div class="card__body"> + <h1 class="card-headline mb-2">Editovat zkratku</h1> + + <form class="my-4"> + + <div class="form-field form-field--error form-field--required mb-4"> + <label class="form-field__label" for="url">URL</label> + <div class="form-field__wrapper form-field__wrapper--shadowed"> + <input type="text" class="text-input form-field__control form-field--required" value="" v-model="selectedShortcut.url" id="url" /> + </div> + <div class="form-field__error" v-if="selectedShortcut.url === ''"></div> + </div> + + <div class="form-field form-field--error form-field--required mb-4"> + <label class="form-field__label" for="code">Typ pĹ™esmÄ›rovánĂ</label> + <div class="form-field__wrapper form-field__wrapper--shadowed select"> + <select v-model="selectedShortcut.code" class="select__control form-field__control "> + <option value="301">301 trvalĂ©</option> + <option value="302">302 doÄŤasnĂ©</option> + </select> + </div> + <div class="form-field__error" v-if="shortcut.url === ''"></div> + </div> + + <button class="btn btn--grey-500 btn--hoveractive" @click.prevent="updateShortcut"> + <div class="btn__body">UloĹľit zmÄ›ny</div> + </button> + </form> + </div> + </div> + + </div> + </div> + </div> +</div> + +</div> + +<script type="module"> + const BASE_URL = "/api/shortcuts"; + const API_HEADERS = { + "Content-Type": "application/json", + "Accept": "application/json", + "X-Auth-Token": "<%= current_user->token %>", + }; + + var app = new Vue({ + el: '#App', + + data: { + shortcuts: [], + deleteConfirmVisible: false, + shortcutInfoVisible: false, + shortcutEditVisible: false, + selectedShortcut: {}, + + shortcut: {}, + formError: '', + }, + + methods: { + fetchData: function() { + fetch(BASE_URL, { + headers: API_HEADERS, + }) + .then((res) => res.json()) + .then(res => { + this.shortcuts = res; + }) }, - fields: [ - { name: "shortcut", title: "Zkratka", type: "text", width: '5em', editing: false }, - { name: "url", title: "URL", type: "text", width: '100%', validate: "url" }, - { name: "code", title: "Typ pĹ™esmÄ›rovánĂ", type: "select", width: '10em', items: [ - { name: "302 doÄŤasnĂ©", code: 302 }, - { name: "301 trvalĂ©", code: 301 }, - ], - valueField: "code", - textField: "name" - }, - { type: "text", title: "Statistika", name: "id", editing: false, - itemTemplate: function(value) { - return '<a href="/shortcuts/' + value + '">' + value + '</a>'; - }, - }, - { type: "control", editButton: false, width: 60 }, - - ] - }); -}); -</script> + addShortcut: function() { + fetch(BASE_URL, { + method: "POST", + headers: API_HEADERS, + body: JSON.stringify(this.shortcut), + }) + .then( response => { + if ( response.status == 201 ) { + this.shortcut = {} + app.fetchData(); + } + else { + response.json().then(json => { + this.formError = json.errors[0]['message'] + }) + } + }) + .catch(() => { + this.formError = "ERROR_SERVERSIDE" + }); + }, + + updateShortcut: function() { + + fetch(BASE_URL + '/' + this.selectedShortcut.id, { + method: "PUT", + headers: API_HEADERS, + body: JSON.stringify(this.selectedShortcut), + }) + .then( response => { + if ( response.status == 204 ) { + this.shortcutEditVisible = false; + app.fetchData(); + } + else { + response.json().then(json => { + this.editError = json.errors[0]['message'] + }) + } + }) + .catch(() => { + this.editError = "ERROR_SERVERSIDE" + }); + }, + + deleteShortcut: function() { + if ( ! this.selectedShortcut ) { + return true; + } + + fetch(BASE_URL + '/' + this.selectedShortcut.id, { + method: "DELETE", + headers: API_HEADERS, + }) + .then( response => { + this.deleteConfirmVisible = false; + app.fetchData(); + }) + }, + + stripURL: function(url) { + const urlObj = new URL(url); + urlObj.search = ''; + urlObj.hash = ''; + return urlObj.toString(); + }, + + showDelete: function(shortcut) { + this.selectedShortcut = shortcut; + this.deleteConfirmVisible = true; + }, + + showInfo: function(shortcut) { + this.selectedShortcut = shortcut; + this.selectedShortcut.full_url = 'https://<%= config->{domain} %>/'+ shortcut.shortcut ; + this.shortcutInfoVisible = true; + navigator.clipboard.writeText(this.selectedShortcut.full_url); + }, + + showEdit: function(shortcut) { + this.selectedShortcut = shortcut; + this.shortcutEditVisible = true; + }, + } + }); + + app.fetchData(); + +</script> -- GitLab