Skip to content
Snippets Groups Projects
Verified Commit 001210c3 authored by Andrej Ramašeuski's avatar Andrej Ramašeuski
Browse files

Kompletni refaktoring

parent d210dd40
No related branches found
No related tags found
No related merge requests found
Pipeline #10956 passed
Showing
with 400 additions and 444 deletions
- statistika
- loop detect
- 302 nebo informacni stranka
- https://apexcharts.com/
1.3.0
2.0.0
......@@ -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');
......
......@@ -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;
......@@ -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;
......@@ -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 ) {
......
......@@ -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;
......
......@@ -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;
......
......@@ -28,6 +28,7 @@ __PACKAGE__->add_columns(
code
title
description
counter
),
);
......
......@@ -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:
......
......@@ -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;
......@@ -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';
% }
......@@ -22,12 +22,12 @@
<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>
<script src="https://cdn-unpkg.pirati.cz/jquery@3.6.0/dist/jquery.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" 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>
<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"/>
......@@ -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 ) {
......
"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" =%><% } =%>
% 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>
<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 id="App">
<h1 class="head-base mb-8">Zkratka "{{ shortcut.shortcut }} ({{ shortcut.url }})"</h1>
</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>
<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>
<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>
<span class="alert alert--red-600 alert--faded">
<span><%= $error %></span>
</span>
% 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>
% 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:";
}
}
<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>
$(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,
});
<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: '',
},
insertItem: function(item) {
return $.ajax({
type: "POST",
dataType: "json",
headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' },
url: "/api/shortcuts",
data: JSON.stringify(item)
});
methods: {
fetchData: function() {
fetch(BASE_URL, {
headers: API_HEADERS,
})
.then((res) => res.json())
.then(res => {
this.shortcuts = res;
})
},
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)
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"
});
},
deleteItem: function(item) {
return $.ajax({
type: "DELETE",
headers: { 'X-Auth-Token': '<%= $c->current_user->token %>' },
url: "/api/shortcuts/" + item.id,
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();
})
},
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"
stripURL: function(url) {
const urlObj = new URL(url);
urlObj.search = '';
urlObj.hash = '';
return urlObj.toString();
},
{ type: "text", title: "Statistika", name: "id", editing: false,
itemTemplate: function(value) {
return '<a href="/shortcuts/' + value + '">' + value + '</a>';
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);
},
{ type: "control", editButton: false, width: 60 },
]
});
showEdit: function(shortcut) {
this.selectedShortcut = shortcut;
this.shortcutEditVisible = true;
},
}
});
</script>
app.fetchData();
</script>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment