newslash
Revisión | 5f382e38c508872851ce8ea0903f9383864f9c7d (tree) |
---|---|
Tiempo | 2019-03-20 22:35:05 |
Autor | hylom <hylom@user...> |
Commiter | hylom |
implement filtering feature for timeline (currently works journal only)
@@ -28,6 +28,7 @@ | ||
28 | 28 | @import "newslash/messages.less"; |
29 | 29 | @import "newslash/progress_bar.less"; |
30 | 30 | @import "newslash/wiki_content.less"; |
31 | +@import "newslash/timeline.less"; | |
31 | 32 | |
32 | 33 | @import "newslash/ads.less"; |
33 | 34 | @import "newslash/system_error.less"; |
@@ -6,6 +6,14 @@ article { | ||
6 | 6 | header { |
7 | 7 | margin-bottom: 10px; |
8 | 8 | h1 { |
9 | + &.color-red { border-left: 2px solid red; } | |
10 | + &.color-orange { border-left: 2px solid orange; } | |
11 | + &.color-yellow { border-left: 2px solid yellow; } | |
12 | + &.color-green { border-left: 2px solid green; } | |
13 | + &.color-blue { border-left: 2px solid blue; } | |
14 | + &.color-indigo { border-left: 2px solid indigo; } | |
15 | + &.color-violet { border-left: 2px solid violet; } | |
16 | + &.color-black { border-left: 2px solid black; } | |
9 | 17 | &:extend(.rectangle-header); |
10 | 18 | &:extend(.large-text); |
11 | 19 | img { |
@@ -0,0 +1,24 @@ | ||
1 | +/* timeline related styles */ | |
2 | + | |
3 | +.timeline-filter-ui { | |
4 | + &:extend(.bordered-box); | |
5 | + .filter-colors { | |
6 | + display: inline-block; | |
7 | + .color-indicator { | |
8 | + display: inline-block; | |
9 | + width: 20px; | |
10 | + border: 1px solid @component-border-color; | |
11 | + cursor: pointer; | |
12 | + float: left; | |
13 | + &.red { background-color: red; color: red; } | |
14 | + &.orange { background-color: orange; color: orange; } | |
15 | + &.yellow { background-color: yellow; color: yellow; } | |
16 | + &.green { background-color: green; color: green; } | |
17 | + &.blue { background-color: blue; color: blue; } | |
18 | + &.indigo { background-color: indigo; color: indigo; } | |
19 | + &.violet { background-color: violet; color: violet; } | |
20 | + &.black { background-color: black; color: black; } | |
21 | + &.active { border-width: 3px; border-color: @primary-color; } | |
22 | + } | |
23 | + } | |
24 | +} |
@@ -122,6 +122,7 @@ sub select { | ||
122 | 122 | my $keys = { uid => "journals.uid", |
123 | 123 | user_id => "journals.uid", |
124 | 124 | karma => "users_info.karma", |
125 | + popularity => "firehose.popularity", | |
125 | 126 | }; |
126 | 127 | my $datetime_keys = { create_time => 'journals.date', |
127 | 128 | update_time => 'journals.last_update', |
@@ -9,7 +9,7 @@ sub select { | ||
9 | 9 | my $params = {@_}; |
10 | 10 | |
11 | 11 | my $uid = $params->{uid}; |
12 | - my $type = "global"; | |
12 | + my $type = $params->{type} || "global"; | |
13 | 13 | if ($uid) { |
14 | 14 | $type = $params->{type} || "user"; |
15 | 15 | } |
@@ -17,6 +17,12 @@ sub select { | ||
17 | 17 | if ($type eq "user") { |
18 | 18 | return $self->_select_user($uid, $params); |
19 | 19 | } |
20 | + elsif ($type eq "tags") { | |
21 | + return $self->_select_tags($uid, $params); | |
22 | + } | |
23 | + elsif ($type eq "user") { | |
24 | + return $self->_select_user($uid, $params); | |
25 | + } | |
20 | 26 | elsif ($type eq "friends") { |
21 | 27 | return $self->_select_friends($uid, $params); |
22 | 28 | } |
@@ -287,6 +287,11 @@ sub _param_to_hash { | ||
287 | 287 | } |
288 | 288 | |
289 | 289 | |
290 | +sub select_nocache { | |
291 | + my ($self, @params) = @_; | |
292 | + return $self->model->select(@params); | |
293 | +} | |
294 | + | |
290 | 295 | sub select { |
291 | 296 | my ($self, @params) = @_; |
292 | 297 |
@@ -69,6 +69,16 @@ my $defaults = { | ||
69 | 69 | |
70 | 70 | Timeline => { popular_period => { hours => 6 }, |
71 | 71 | item_per_page => 20, |
72 | + item_per_page_limit => 100, | |
73 | + heatmap => { black => -999, | |
74 | + violet => -20, | |
75 | + indigo => 25, | |
76 | + blue => 93, | |
77 | + green => 138, | |
78 | + yellow => 175, | |
79 | + orange => 200, | |
80 | + red => 240, | |
81 | + }, | |
72 | 82 | }, |
73 | 83 | |
74 | 84 | Database => { host => "db", |
@@ -414,6 +414,8 @@ sub startup { | ||
414 | 414 | $api->get('/story')->to('API::Story#get'); |
415 | 415 | $api->post('/story')->to('API::Story#post'); |
416 | 416 | |
417 | + $api->get('/timeline')->to('API::Timeline#get'); | |
418 | + | |
417 | 419 | $api->get('/poll')->to('API::Poll#get'); |
418 | 420 | $api->post('/poll')->to('API::Poll#post'); |
419 | 421 | $api->post('/vote')->to('API::Poll#vote', csrf_check_id => 'vote'); |
@@ -0,0 +1,186 @@ | ||
1 | +package Newslash::Web::Controller::API::Timeline; | |
2 | +use Mojo::Base 'Mojolicious::Controller'; | |
3 | +use Data::Dumper; | |
4 | + | |
5 | + | |
6 | +sub _add_url { | |
7 | + my ($c, $item) = @_; | |
8 | + if ($item->{content_type} eq "journal") { | |
9 | + return "/~$item->{author}/journal/$item->{id}/"; | |
10 | + } | |
11 | + elsif ($item->{content_type} eq "story") { | |
12 | + return "/story/$item->{sid}/"; | |
13 | + } | |
14 | + else { | |
15 | + return "/$item->{content_type}/$item->{id}/"; | |
16 | + } | |
17 | + return; | |
18 | +} | |
19 | + | |
20 | +sub _get_heatmap { | |
21 | + my $c = shift; | |
22 | + my $cfg = $c->config("Timeline"); | |
23 | + my $heatmap = $cfg->{heatmap}; | |
24 | + | |
25 | + if (!$heatmap) { | |
26 | + $c->app->log->error("Timeline: no heatmap defined."); | |
27 | + return; | |
28 | + } | |
29 | + | |
30 | + my @keys = keys %$heatmap; | |
31 | + @keys = sort { $heatmap->{$a} <=> $heatmap->{$b} } @keys; | |
32 | + my $rs = []; | |
33 | + for my $k (@keys) { | |
34 | + push @$rs, { $k => $heatmap->{$k } }; | |
35 | + } | |
36 | + return $rs; | |
37 | +} | |
38 | + | |
39 | +sub _get_primary_topic_icon_url { | |
40 | + my ($c, $item) = @_; | |
41 | + my $cfg = $c->config("Site") || {}; | |
42 | + my $base_url = $cfg->{topic_icon_base_url}; | |
43 | + | |
44 | + if (!$base_url) { | |
45 | + $c->app->log->error("Timeline: Site.topic_icon_base_url is not defined"); | |
46 | + return; | |
47 | + } | |
48 | + my $t = $item->{primary_topic} || {}; | |
49 | + my $image = $t->{image}; | |
50 | + | |
51 | + if ($image) { | |
52 | + return "$base_url/$image"; | |
53 | + } | |
54 | + return; | |
55 | +} | |
56 | + | |
57 | +sub _score_to_heatmap_color { | |
58 | + my ($c, $item) = @_; | |
59 | + my $heatmap = _get_heatmap($c); | |
60 | + if (!$heatmap) { | |
61 | + return; | |
62 | + } | |
63 | + | |
64 | + my $last_color; | |
65 | + for my $i (reverse @$heatmap) { | |
66 | + my @k = keys %$i; | |
67 | + my $color = $last_color = $k[0]; | |
68 | + my $threshold = $i->{$color}; | |
69 | + | |
70 | + if ($item->{popularity} > $threshold) { | |
71 | + return $color; | |
72 | + } | |
73 | + } | |
74 | + return $last_color; | |
75 | +} | |
76 | + | |
77 | +sub _threshold_to_popularity { | |
78 | + my ($c, $threshold) = @_; | |
79 | + my $heatmap = _get_heatmap($c); | |
80 | + if (!$heatmap) { | |
81 | + $c->app->log->error("Timeline::_threshold_to_popularity: no heatmap defined."); | |
82 | + return; | |
83 | + } | |
84 | + | |
85 | + my $color = $heatmap->[$threshold]; | |
86 | + if (!$color) { | |
87 | + $c->app->log->error("Timeline::_threshold_to_popularity: no color for threshold $threshold."); | |
88 | + return; | |
89 | + } | |
90 | + | |
91 | + my @k = keys %$color; | |
92 | + return $color->{$k[0]}; | |
93 | +} | |
94 | + | |
95 | + | |
96 | +sub get { | |
97 | + my $c = shift; | |
98 | + my $user = $c->stash('user'); | |
99 | + my $params = $c->req->query_params->to_hash; | |
100 | + my $target = $params->{target} || "all"; | |
101 | + my $cfg = $c->config("Timeline"); | |
102 | + | |
103 | + my $hide_future = !$user->{is_admin} && !$user->{editor}; | |
104 | + my $public_only = !$user->{is_admin} && !$user->{editor}; | |
105 | + | |
106 | + my $limit = $params->{limit} || $cfg->{item_per_page} || 10; | |
107 | + my $max_limit = $cfg->{item_per_page_limit} || 1000; | |
108 | + $limit = $max_limit if $limit > $max_limit; | |
109 | + | |
110 | + my $skip = $params->{skip} || 0; | |
111 | + my $min_popularity; | |
112 | + if ($params->{threshold}) { | |
113 | + $min_popularity = _threshold_to_popularity($c, $params->{threshold}); | |
114 | + $c->app->log->debug("Timeline::_threshold_to_popularity: use min_pop $min_popularity."); | |
115 | + } | |
116 | + my $result; | |
117 | + my $model; | |
118 | + | |
119 | + if ($target eq "story") { | |
120 | + $model = $c->ccache->model('stories'); | |
121 | + } | |
122 | + elsif ($target eq "journal") { | |
123 | + $model = $c->ccache->model('journals'); | |
124 | + } | |
125 | + elsif ($target eq "comment") { | |
126 | + $model = $c->ccache->model('comments'); | |
127 | + } | |
128 | + elsif ($target eq "poll") { | |
129 | + $model = $c->ccache->model('polls'); | |
130 | + } | |
131 | + elsif ($target eq "submission") { | |
132 | + $model = $c->ccache->model('submissions'); | |
133 | + } | |
134 | + elsif ($target eq "all") { | |
135 | + $model = $c->ccache->model('timeline'); | |
136 | + } | |
137 | + else { | |
138 | + $c->render(json => { error => { code => -1, message => "invalid_request" }}); | |
139 | + $c->rendered(400); | |
140 | + return; | |
141 | + } | |
142 | + | |
143 | + if ($user->{is_login}) { | |
144 | + $result = $model->select_nocache(hide_future => $hide_future, | |
145 | + public_only => $public_only, | |
146 | + limit => $limit, | |
147 | + skip => $skip, | |
148 | + order_by => {create_time => 'desc'}, | |
149 | + popularity => $min_popularity ? { ge => $min_popularity } : undef, | |
150 | + ); | |
151 | + } | |
152 | + else { | |
153 | + $result = $model->select(hide_future => $hide_future, | |
154 | + public_only => $public_only, | |
155 | + limit => $limit, | |
156 | + skip => $skip, | |
157 | + order_by => {create_time => 'desc'}, | |
158 | + popularity => $min_popularity ? { ge => $min_popularity } : undef, | |
159 | + ); | |
160 | + } | |
161 | + | |
162 | + if (!$result) { | |
163 | + $c->render(json => { error => { code => -1, message => "internal_server_error" }}); | |
164 | + $c->rendered(500); | |
165 | + return; | |
166 | + } | |
167 | + | |
168 | + if (!@$result) { | |
169 | + $c->render(json => { error => { code => -1, message => "not_found" }}); | |
170 | + $c->rendered(404); | |
171 | + return; | |
172 | + } | |
173 | + | |
174 | + # add headmap info and topic icon url | |
175 | + for my $item (@$result) { | |
176 | + $item->{color} = _score_to_heatmap_color($c, $item); | |
177 | + $item->{icon_url} = _get_primary_topic_icon_url($c, $item); | |
178 | + $item->{url} = _add_url($c, $item); | |
179 | + } | |
180 | + | |
181 | + | |
182 | + $c->render(json => { result => $result }); | |
183 | +} | |
184 | + | |
185 | + | |
186 | +1; |
@@ -60,6 +60,16 @@ sub _render_timeline { | ||
60 | 60 | content_type => $params->{content_type}, |
61 | 61 | }; |
62 | 62 | |
63 | + if ($params->{content_type} eq "journal") { | |
64 | + $self->render("timeline/timeline2", | |
65 | + items => $items, | |
66 | + prev => $prev, | |
67 | + page => $page, | |
68 | + ); | |
69 | + $self->stats->add_event_counter("timeline_view"); | |
70 | + return; | |
71 | + } | |
72 | + | |
63 | 73 | $self->render("timeline/base", |
64 | 74 | items => $items, |
65 | 75 | prev => $prev, |
@@ -201,6 +201,17 @@ function _initNewslash() { | ||
201 | 201 | return this.post("/journal", data); |
202 | 202 | }; |
203 | 203 | |
204 | + Newslash.prototype.getTimeline = function getTimeline (target, options) { | |
205 | + if (!target) { target = "all"; } | |
206 | + options = options || {}; | |
207 | + | |
208 | + var url = "/timeline?target=" + target; | |
209 | + if (options.threshold !== undefined) { | |
210 | + url = url + "&threshold=" + options.threshold; | |
211 | + } | |
212 | + | |
213 | + return this.get(url); | |
214 | + }; | |
204 | 215 | } |
205 | 216 | |
206 | 217 | _initNewslash(); |
@@ -0,0 +1,68 @@ | ||
1 | +/* timeline.js */ | |
2 | +var timeline = {}; | |
3 | + | |
4 | +timeline.run = function (params) { | |
5 | + /* define exotic parameters */ | |
6 | + params = params || {}; | |
7 | + var userConfig = params.userConfig || {}; | |
8 | + var siteConfig = params.siteConfig || {}; | |
9 | + var pageInfo = params.pageInfo || {}; | |
10 | + var user = params.user || {}; | |
11 | + | |
12 | + if (!params.el) { | |
13 | + console.log('error in commentTree.run(): no element given'); | |
14 | + return; | |
15 | + } | |
16 | + | |
17 | + /* | |
18 | + * register <timeline-item> | |
19 | + */ | |
20 | + Vue.component('timeline-item', { | |
21 | + template: '#timeline-item-template', | |
22 | + props: {item: Object}, | |
23 | + data: function () { return {}; }, | |
24 | + created: function () { return; }, | |
25 | + }); | |
26 | + | |
27 | + /* | |
28 | + * register <timeline-filter-ui> | |
29 | + */ | |
30 | + Vue.component('timeline-filter-ui', { | |
31 | + template: '#timeline-filter-ui-template', | |
32 | + props: {}, | |
33 | + data: function () { return { threshold: 1}; }, | |
34 | + created: function () { return; }, | |
35 | + methods: { | |
36 | + setThreshold: function setThreshold (threshold) { | |
37 | + if (this.threshold != threshold) { | |
38 | + this.threshold = threshold; | |
39 | + vm.$emit('updateTimeline', {threshold: threshold}); | |
40 | + } | |
41 | + }, | |
42 | + }, | |
43 | + }); | |
44 | + | |
45 | + function updateTimeline(vm, target, threshold) { | |
46 | + newslash.getTimeline(target, {threshold: threshold}).then( | |
47 | + (resp) => { // success | |
48 | + vm.items = resp.result; | |
49 | + }, | |
50 | + (resp) => { // fail | |
51 | + statusIndicator.error("comment_loading_error"); | |
52 | + } | |
53 | + ); | |
54 | + } | |
55 | + | |
56 | + var vm = this.vm = new Vue({ | |
57 | + el: params.el, | |
58 | + data: { items: [] }, | |
59 | + created: function created() { | |
60 | + updateTimeline(this, params.target, 1); | |
61 | + }, | |
62 | + }); | |
63 | + | |
64 | + vm.$on("updateTimeline", function (args) { | |
65 | + updateTimeline(this, params.target, args.threshold); | |
66 | + }); | |
67 | +}; | |
68 | + |
@@ -0,0 +1,63 @@ | ||
1 | +# -*-Perl-*- | |
2 | +# timeline api tests | |
3 | +use Mojo::Base -strict; | |
4 | +use Mojo::Date; | |
5 | + | |
6 | +use Test::More; | |
7 | +use Test::Mojo; | |
8 | +use Mojo::Util qw(dumper); | |
9 | + | |
10 | +my $t = Test::Mojo->new('Newslash::Web'); | |
11 | + | |
12 | +subtest 'get timeline' => sub { | |
13 | + | |
14 | + # get all items | |
15 | + $t->get_ok("/api/v1/timeline?target=all") | |
16 | + ->status_is(200) | |
17 | + ->content_type_like(qr/application\/json/) | |
18 | + ->json_has('/result') | |
19 | + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); | |
20 | + | |
21 | + # get story items | |
22 | + $t->get_ok("/api/v1/timeline?target=story") | |
23 | + ->status_is(200) | |
24 | + ->content_type_like(qr/application\/json/) | |
25 | + ->json_has('/result') | |
26 | + ->json_has('/result/0/stoid') | |
27 | + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); | |
28 | + | |
29 | + # get comment items | |
30 | + $t->get_ok("/api/v1/timeline?target=comment") | |
31 | + ->status_is(200) | |
32 | + ->content_type_like(qr/application\/json/) | |
33 | + ->json_has('/result') | |
34 | + ->json_has('/result/0/cid') | |
35 | + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); | |
36 | + | |
37 | + # get journal items | |
38 | + $t->get_ok("/api/v1/timeline?target=journal") | |
39 | + ->status_is(200) | |
40 | + ->content_type_like(qr/application\/json/) | |
41 | + ->json_has('/result') | |
42 | + ->json_has('/result/0/journal_id') | |
43 | + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); | |
44 | + | |
45 | + # get submission items | |
46 | + $t->get_ok("/api/v1/timeline?target=submission") | |
47 | + ->status_is(200) | |
48 | + ->content_type_like(qr/application\/json/) | |
49 | + ->json_has('/result') | |
50 | + ->json_has('/result/0/submission_id') | |
51 | + ->or(sub {diag "message: " . dumper($t->tx->res->json);}); | |
52 | + | |
53 | + # get poll items | |
54 | + #$t->get_ok("/api/v1/timeline?target=poll") | |
55 | + # ->status_is(200) | |
56 | + # ->content_type_like(qr/application\/json/) | |
57 | + # ->json_has('/result') | |
58 | + # ->json_has('/result/0/qid') | |
59 | + # ->or(sub {diag "message: " . dumper($t->tx->res->json);}); | |
60 | +}; | |
61 | + | |
62 | + | |
63 | +done_testing(); |
@@ -41,7 +41,7 @@ END; | ||
41 | 41 | |
42 | 42 | -%] |
43 | 43 | |
44 | -<article id="[% item.id %]" type="[% item.content_type %]" item-id="[% content_id %]" | |
44 | +<article id="[% item.id %]" type="[% item.content_type %]" item-id="[% item.content_id %]" | |
45 | 45 | [% IF !x_template %]v-if="0"[% ELSE %]v-if="mode != 'editing' || enableAutoPreview"[% END %]> |
46 | 46 | <header> |
47 | 47 | <h1> |
@@ -0,0 +1,86 @@ | ||
1 | +<script type="text/x-template" id="timeline-filter-ui-template"> | |
2 | + <div class="timeline-filter-ui"> | |
3 | + <span>表示するアイテムのしきい値:</span> | |
4 | + <div class="filter-colors"> | |
5 | + <span :class="{active: threshold == 0}" class="color-indicator black" title="0" @click="setThreshold(0)">0</span> | |
6 | + <span :class="{active: threshold == 1}" class="color-indicator violet" title="1" @click="setThreshold(1)">1</span> | |
7 | + <span :class="{active: threshold == 2}" class="color-indicator indigo" title="2" @click="setThreshold(2)">2</span> | |
8 | + <span :class="{active: threshold == 3}" class="color-indicator blue" title="3" @click="setThreshold(3)">3</span> | |
9 | + <span :class="{active: threshold == 4}" class="color-indicator green" title="4" @click="setThreshold(4)">4</span> | |
10 | + <span :class="{active: threshold == 5}" class="color-indicator yellow" title="5" @click="setThreshold(5)">5</span> | |
11 | + <span :class="{active: threshold == 6}" class="color-indicator orange" title="6" @click="setThreshold(6)">6</span> | |
12 | + <span :class="{active: threshold == 7}" class="color-indicator red" title="7" @click="setThreshold(7)">7</span> | |
13 | + </div> | |
14 | + </div> | |
15 | +</script> | |
16 | + | |
17 | +<script type="text/x-template" id="timeline-item-template"> | |
18 | + <article> | |
19 | + <header> | |
20 | + <h1 :class="item.color ? 'color-' + item.color : ''"> | |
21 | + <img :src="item.icon_url" v-if="item.icon_url" /> | |
22 | + <a :href="item.url" v-html="item.title" v-if="item.url"> | |
23 | + <span v-html="item.title"></span> | |
24 | + </a> | |
25 | + <span v-html="item.title" v-else></span> | |
26 | + </h1> | |
27 | + | |
28 | + <div class="property"> | |
29 | + <span class="content-type" v-html="item.content_type"></span> | |
30 | + <span class="author"> | |
31 | + by <a :href="'/~' + item.author + '/'" v-text="item.author"></a> | |
32 | + </span> | |
33 | + <span class="create-time" v-text="item.create_time"></span> | |
34 | + | |
35 | + [%- IF user.is_admin %] | |
36 | + <span class="score"> | |
37 | + pop: <span v-text="item.popularity"></span> | |
38 | + epop: <span v-text="item.editorpop"></span> | |
39 | + need: <span v-text="item.neediness"></span> | |
40 | + act: <span v-text="item.activity"></span> | |
41 | + </span> | |
42 | + [%- END %] | |
43 | + | |
44 | + <span class="dept" v-if="item.content_type == 'story' && item.dept"> | |
45 | + <span class="dept-name" v-text="item.dept"></span> 部門より | |
46 | + </span> | |
47 | + </div><!-- .property --> | |
48 | + | |
49 | + [% IF user.author || user.is_admin %] | |
50 | + <div class="alert alert-info" v-if="item.public != 'yes'">この記事は非公開に設定されています</div> | |
51 | + [% END %] | |
52 | + </header> | |
53 | + | |
54 | + <div class="body contents-text" v-html="item.intro_text" v-if="item.intro_text"></div> | |
55 | + <div class="body contents-text" v-html="item.body_text" v-if="item.body_text"></div> | |
56 | + <div class="body contents-text" v-html="item.full_text" v-if="item.full_text"></div> | |
57 | + <div class="body contents-text" v-html="item.media" v-if="item.media"></div> | |
58 | + <div class="body contents-text" v-if="item.url"><p><a :href="item.url">情報元へのリンク</a></p></div> | |
59 | + | |
60 | + <footer> | |
61 | + <div class="link-to-story"> | |
62 | + <a :href="item.url"> | |
63 | + <span v-if='item.comment_count > 0'> | |
64 | + <span v-text="item.comment_count"></span>件のコメントを見る | |
65 | + </span> | |
66 | + <span v-else> | |
67 | + 続きを読む | |
68 | + </span> | |
69 | + </a> | |
70 | + </div> | |
71 | + | |
72 | + <div class="tag-bar"> | |
73 | + <ul class="tags"> | |
74 | + <li v-for="tag in item.tags" v-if="tag.private == 'no' && tag.tagname != 'mainpage' && tag.uid == item.uid"> | |
75 | + <a :href="'/tag/' + tag.tagname" v-text="tag.tagname"></a> | |
76 | + </li> | |
77 | + </ul> | |
78 | + </div> | |
79 | + | |
80 | + </footer> | |
81 | + </article> | |
82 | +</script> | |
83 | + | |
84 | + | |
85 | +[% helpers.load_js("timeline.js") %] | |
86 | + |
@@ -0,0 +1,34 @@ | ||
1 | +[% WRAPPER common/layout enable_sidebar=1 %] | |
2 | + | |
3 | +<div class="sidebar-wrapper"> | |
4 | + [%- helpers.ad_code("timeline-top") %] | |
5 | + <div class="index main-contents" id="timeline"> | |
6 | + <timeline-filter-ui></timeline-filter-ui> | |
7 | + <div class="timeline-items" v-if="0"> | |
8 | + [%- FOREACH item IN items -%] | |
9 | + [%- INCLUDE common/article/article hide_bodytext=1 %] | |
10 | + [%- END -%] | |
11 | + </div> | |
12 | + | |
13 | + <div class="timeline-items" v-else v-for="item in items"> | |
14 | + <timeline-item :item="item"></timeline-item> | |
15 | + </div> | |
16 | + | |
17 | + [%- IF prev -%] | |
18 | + <div class="pager"> | |
19 | + <span class="prev"> | |
20 | + <a href="/[% prev.type %]/[% prev.date %]/[% IF prev.id %]#[% prev.id %][% END %]">前の記事</a> | |
21 | + </span> | |
22 | + </div> | |
23 | + [%- END -%] | |
24 | + | |
25 | + </div><!-- .index --> | |
26 | + | |
27 | + [%- INCLUDE common/sidebar -%] | |
28 | + | |
29 | +</div><!-- .timeline-wrapper --> | |
30 | +[% INCLUDE common/components/timeline %] | |
31 | +<script> | |
32 | + timeline.run({el: "#timeline", target: "journal"}); | |
33 | +</script> | |
34 | +[% END %] |