diff options
Diffstat (limited to 'lib/VN3/User')
-rw-r--r-- | lib/VN3/User/Lib.pm | 31 | ||||
-rw-r--r-- | lib/VN3/User/Login.pm | 50 | ||||
-rw-r--r-- | lib/VN3/User/Page.pm | 207 | ||||
-rw-r--r-- | lib/VN3/User/RegReset.pm | 137 | ||||
-rw-r--r-- | lib/VN3/User/Settings.pm | 98 | ||||
-rw-r--r-- | lib/VN3/User/VNList.pm | 325 |
6 files changed, 0 insertions, 848 deletions
diff --git a/lib/VN3/User/Lib.pm b/lib/VN3/User/Lib.pm deleted file mode 100644 index c63e4286..00000000 --- a/lib/VN3/User/Lib.pm +++ /dev/null @@ -1,31 +0,0 @@ -package VN3::User::Lib; - -use VN3::Prelude; - -our @EXPORT = qw/show_list TopNav/; - - -# Whether we can see the user's list -sub show_list { - my $u = shift; - die "Can't determine show_list() when hide_list preference is not known" if !exists $u->{hide_list}; - auth->permUsermod || !$u->{hide_list} || $u->{id} == (auth->uid||0); -} - - -sub TopNav { - my($page, $u) = @_; - - Div class => 'nav raised-top-nav', sub { - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'details'), sub { A href => "/u$u->{id}", class => 'nav__link', 'Details'; }; - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'list'), sub { A href => "/u$u->{id}/list", class => 'nav__link', 'List'; } if show_list $u; - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'wish'), sub { A href => "/u$u->{id}/wish", class => 'nav__link', 'Wishlist'; } if show_list $u; - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'posts'), sub { A href => "/u$u->{id}/posts", class => 'nav__link', 'Posts'; }; - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'discussions'),sub { A href => "/t/u$u->{id}", class => 'nav__link', 'Discussions'; }; - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'tags'), sub { A href => "/g/links?uid=$u->{id}", class => 'nav__link', 'Tags'; }; - Div mkclass('nav__item' => 1, 'nav__item--active' => $page eq 'hist'), sub { A href => "/u$u->{id}/hist", class => 'nav__link', 'Contributions'; }; - }; -} - -1; - diff --git a/lib/VN3/User/Login.pm b/lib/VN3/User/Login.pm deleted file mode 100644 index 7660762a..00000000 --- a/lib/VN3/User/Login.pm +++ /dev/null @@ -1,50 +0,0 @@ -package VN3::User::Login; - -use VN3::Prelude; - -# TODO: Redirect to a password change form when a user logs in with an insecure password. - -TUWF::get '/u/login' => sub { - return tuwf->resRedirect('/', 'temp') if auth; - Framework title => 'Login', center => 1, sub { - Div 'data-elm-module' => 'User.Login', ''; - }; -}; - - -my $elm_Throttled = elm_api 'Throttled'; -my $elm_BadLogin = elm_api 'BadLogin'; - -json_api '/u/login', { - username => { username => 1 }, - password => { password => 1 } -}, sub { - my $data = shift; - - my $conf = tuwf->conf->{login_throttle} || [ 24*3600/10, 24*3600 ]; - my $ip = norm_ip tuwf->reqIP; - - my $tm = tuwf->dbVali( - 'SELECT', sql_totime('greatest(timeout, now())'), 'FROM login_throttle WHERE ip =', \$ip - ) || time; - - return $elm_Throttled->() if $tm-time() > $conf->[1]; - return $elm_Success->() if auth->login($data->{username}, $data->{password}); - - # Failed login, update throttle. - my $upd = { - ip => \$ip, - timeout => sql_fromtime $tm+$conf->[0] - }; - tuwf->dbExeci('INSERT INTO login_throttle', $upd, 'ON CONFLICT (ip) DO UPDATE SET', $upd); - $elm_BadLogin->() -}; - - -TUWF::get qr{/$UID_RE/logout}, sub { - return tuwf->resNotFound if !auth || auth->uid != tuwf->capture('id'); - auth->logout; - tuwf->resRedirect('/', 'temp'); -}; - -1; diff --git a/lib/VN3/User/Page.pm b/lib/VN3/User/Page.pm deleted file mode 100644 index 886ad39a..00000000 --- a/lib/VN3/User/Page.pm +++ /dev/null @@ -1,207 +0,0 @@ -package VN3::User::Page; - -use VN3::Prelude; -use VN3::User::Lib; - - -sub StatsLeft { - my $u = shift; - my $vns = show_list($u) && tuwf->dbVali('SELECT COUNT(*) FROM vnlists WHERE uid =', \$u->{id}); - my $rel = show_list($u) && tuwf->dbVali('SELECT COUNT(*) FROM rlists WHERE uid =', \$u->{id}); - my $posts = tuwf->dbVali('SELECT COUNT(*) FROM threads_posts WHERE uid =', \$u->{id}); - my $threads = tuwf->dbVali('SELECT COUNT(*) FROM threads_posts WHERE num = 1 AND uid =', \$u->{id}); - - Div class => 'card__title mb-4', 'Stats'; - Div class => 'big-stats mb-5', sub { - A href => "/u$u->{id}/list", class => 'big-stats__stat', sub { - Txt 'Votes'; - Div class => 'big-stats__value', show_list($u) ? $u->{c_votes} : '-'; - }; - A href => "/u$u->{id}/hist", class => 'big-stats__stat', sub { - Txt 'Edits'; - Div class => 'big-stats__value', $u->{c_changes}; - }; - A href => "/g/links?u=$u->{id}", class => 'big-stats__stat', sub { - Txt 'Tags'; - Div class => 'big-stats__value', $u->{c_tags}; - }; - }; - Div class => 'user-stats__text', sub { - Dl class => 'dl--horizontal', sub { - if(show_list $u) { - Dt 'List stats'; - Dd sprintf '%d release%s of %d visual novel%s', $rel, $rel == 1 ? '' : 's', $vns, $vns == 1 ? '' : 's'; - } - Dt 'Forum stats'; - Dd sprintf '%d post%s, %d new thread%s', $posts, $posts == 1 ? '' : 's', $threads, $threads == 1 ? '' : 's'; - Dt 'Registered'; - Dd date_display $u->{registered}; - }; - }; -} - - -sub Stats { - my $u = shift; - - my($count, $Graph) = show_list($u) ? VoteGraph u => $u->{id} : (); - - Div class => 'card card--white card--no-separators flex-expand mb-5', sub { - Div class => 'card__section fs-medium', sub { - Div class => 'user-stats', sub { - Div class => 'user-stats__left', sub { StatsLeft $u }; - Div class => 'user-stats__right', sub { - Div class => 'card__title mb-2', 'Vote distribution'; - $Graph->(); - } if $count; - } - } - } -} - - -sub List { - my $u = shift; - return if !show_list $u; - - # XXX: This query doesn't catch vote or list *changes*, only new entries. - # We don't store the modification date in the DB at the moment. - my $l = tuwf->dbAlli(q{ - SELECT il.vid, EXTRACT('epoch' FROM GREATEST(v.date, l.added)) AS date, vn.title, vn.original, v.vote, l.status - FROM ( - SELECT vid FROM votes WHERE uid = }, \$u->{id}, q{ - UNION SELECT vid FROM vnlists WHERE uid = }, \$u->{id}, q{ - ) AS il (vid) - LEFT JOIN votes v ON v.vid = il.vid - LEFT JOIN vnlists l ON l.vid = il.vid - JOIN vn ON vn.id = il.vid - WHERE v.uid = }, \$u->{id}, q{ - AND l.uid = }, \$u->{id}, q{ - ORDER BY GREATEST(v.date, l.added) DESC - LIMIT 10 - }); - return if !@$l; - - Div class => 'card card--white card--no-separators mb-5', sub { - Div class => 'card__header', sub { - Div class => 'card__title', 'Recent list additions'; - }; - Table class => 'table table--responsive-single-sm fs-medium', sub { - Thead sub { - Tr sub { - Th width => '15%', 'Date'; - Th width => '50%', 'Visual novel'; - Th width => '10%', 'Vote'; - Th width => '25%', 'Status'; - }; - }; - Tbody sub { - for my $i (@$l) { - Tr sub { - Td class => 'tabular-nums muted', date_display $i->{date}; - Td sub { - A href => "/v$i->{vid}", title => $i->{original}||$i->{title}, $i->{title}; - }; - Td vote_display $i->{vote}; - Td $i->{status} ? $VNLIST_STATUS{$i->{status}} : ''; - }; - } - }; - }; - Div class => 'card__section fs-medium', sub { - A href => "/u$u->{id}/list", 'View full list'; - } - }; -} - - -sub Edits { - my $u = shift; - # XXX: This is a lazy implementation, could probably share code/UI with the database entry history tables (as in VNDB 2) - - my $l = tuwf->dbAlli(q{ - SELECT ch.id, ch.itemid, ch.rev, ch.type, EXTRACT('epoch' FROM ch.added) AS added - FROM changes ch - WHERE ch.requester =}, \$u->{id}, q{ - ORDER BY ch.added DESC LIMIT 10 - }); - return if !@$l; - - # This can also be written as a UNION, haven't done any benchmarking yet. - # It doesn't matter much with only 10 entries, but it will matter if this - # query is re-used for other history browsing purposes. - enrich id => q{ - SELECT ch.id, COALESCE(d.title, v.title, p.name, r.title, c.name, sa.name) AS title - FROM changes ch - LEFT JOIN docs_hist d ON ch.type = 'd' AND d.chid = ch.id - LEFT JOIN vn_hist v ON ch.type = 'v' AND v.chid = ch.id - LEFT JOIN producers_hist p ON ch.type = 'p' AND p.chid = ch.id - LEFT JOIN releases_hist r ON ch.type = 'r' AND r.chid = ch.id - LEFT JOIN chars_hist c ON ch.type = 'c' AND c.chid = ch.id - LEFT JOIN staff_hist s ON ch.type = 's' AND s.chid = ch.id - LEFT JOIN staff_alias_hist sa ON ch.type = 's' AND sa.chid = ch.id AND s.aid = sa.aid - WHERE ch.id IN}, $l; - - Div class => 'card card--white card--no-separators mb-5', sub { - Div class => 'card__header', sub { - Div class => 'card__title', 'Recent database contributions'; - }; - Table class => 'table table--responsive-single-sm fs-medium', sub { - Thead sub { - Tr sub { - Th width => '15%', 'Date'; - Th width => '10%', 'Rev.'; - Th width => '75%', 'Entry'; - }; - }; - Tbody sub { - for my $i (@$l) { - my $id = "$i->{type}$i->{itemid}.$i->{rev}"; - Tr sub { - Td class => 'tabular-nums muted', date_display $i->{added}; - Td sub { - A href => "/$id", $id; - }; - Td sub { - A href => "/$id", $i->{title}; - }; - } - } - } - }; - Div class => 'card__section fs-medium', sub { - A href => "/u$u->{id}/hist", 'View all'; - } - }; -} - - -TUWF::get qr{/$UID_RE}, sub { - my $uid = tuwf->capture('id'); - my $u = tuwf->dbRowi(q{ - SELECT u.id, u.username, EXTRACT('epoch' FROM u.registered) AS registered, u.c_votes, u.c_changes, u.c_tags, hd.value AS hide_list - FROM users u - LEFT JOIN users_prefs hd ON hd.uid = u.id AND hd.key = 'hide_list' - WHERE u.id =}, \$uid - ); - return tuwf->resNotFound if !$u->{id}; - - Framework - title => lcfirst($u->{username}), - index => 0, - single_col => 1, - top => sub { - Div class => 'col-md', sub { - EntryEdit u => $u; - Div class => 'detail-page-title', ucfirst $u->{username}; - TopNav details => $u; - } - }, - sub { - Stats $u; - List $u; - Edits $u; - }; -}; - -1; diff --git a/lib/VN3/User/RegReset.pm b/lib/VN3/User/RegReset.pm deleted file mode 100644 index ed815547..00000000 --- a/lib/VN3/User/RegReset.pm +++ /dev/null @@ -1,137 +0,0 @@ -# User registration and password reset. These functions share some common code. -package VN3::User::RegReset; - -use VN3::Prelude; - - -TUWF::get '/u/newpass' => sub { - return tuwf->resRedirect('/', 'temp') if auth; - Framework title => 'Password reset', center => 1, sub { - Div 'data-elm-module' => 'User.PassReset', ''; - }; -}; - - -my $elm_BadEmail = elm_api 'BadEmail'; -my $elm_BadPass = elm_api 'BadPass'; -my $elm_Bot = elm_api 'Bot'; -my $elm_Taken = elm_api 'Taken'; -my $elm_DoubleEmail = elm_api 'DoubleEmail'; -my $elm_DoubleIP = elm_api 'DoubleIP'; - - -json_api '/u/newpass', { - email => { email => 1 }, -}, sub { - my $data = shift; - - my($id, $token) = auth->resetpass($data->{email}); - return $elm_BadEmail->() if !$id; - - my $name = tuwf->dbVali('SELECT username FROM users WHERE id =', \$id); - my $body = sprintf - "Hello %s," - ."\n\n" - ."Your VNDB.org login has been disabled, you can now set a new password by following the link below:" - ."\n\n" - ."%s" - ."\n\n" - ."Now don't forget your password again! :-)" - ."\n\n" - ."vndb.org", - $name, tuwf->reqBaseURI()."/u$id/setpass/$token"; - - tuwf->mail($body, - To => $data->{email}, - From => 'VNDB <noreply@vndb.org>', - Subject => "Password reset for $name", - ); - $elm_Success->(); -}; - - -my $reset_url = qr{/$UID_RE/setpass/(?<token>[a-f0-9]{40})}; - -TUWF::get $reset_url, sub { - return tuwf->resRedirect('/', 'temp') if auth; - - my $id = tuwf->capture('id'); - my $token = tuwf->capture('token'); - my $name = tuwf->dbVali('SELECT username FROM users WHERE id =', \$id); - - return tuwf->resNotFound if !$name || !auth->isvalidtoken($id, $token); - - Framework title => 'Set password', center => 1, sub { - Div 'data-elm-module' => 'User.PassSet', 'data-elm-flags' => '"'.tuwf->reqPath().'"', ''; - }; -}; - - -json_api $reset_url, { - pass => { password => 1 }, -}, sub { - my $data = shift; - my $id = tuwf->capture('id'); - my $token = tuwf->capture('token'); - - return $elm_BadPass->() if tuwf->isUnsafePass($data->{pass}); - die "Invalid reset token" if !auth->setpass($id, $token, undef, $data->{pass}); - tuwf->dbExeci('UPDATE users SET email_confirmed = true WHERE id =', \$id); - $elm_Success->() -}; - - -TUWF::get '/u/register', sub { - return tuwf->resRedirect('/', 'temp') if auth; - Framework title => 'Register', center => 1, sub { - Div 'data-elm-module' => 'User.Register', ''; - }; -}; - - -json_api '/u/register', { - username => { username => 1 }, - email => { email => 1 }, - vns => { int => 1 }, -}, sub { - my $data = shift; - - my $num = tuwf->dbVali("SELECT count FROM stats_cache WHERE section = 'vn'"); - return $elm_Bot->() if $data->{vns} < $num*0.995 || $data->{vns} > $num*1.005; - return $elm_Taken->() if tuwf->dbVali('SELECT 1 FROM users WHERE username =', \$data->{username}); - return $elm_DoubleEmail->() if tuwf->dbVali(select => sql_func user_emailexists => \$data->{email}); - - my $ip = tuwf->reqIP; - return $elm_DoubleIP->() if tuwf->dbVali( - q{SELECT 1 FROM users WHERE registered >= NOW()-'1 day'::interval AND ip <<}, - $ip =~ /:/ ? \"$ip/48" : \"$ip/30" - ); - - my $id = tuwf->dbVali('INSERT INTO users', { - username => $data->{username}, - mail => $data->{email}, - ip => $ip, - }, 'RETURNING id'); - my(undef, $token) = auth->resetpass($data->{email}); - - my $body = sprintf - "Hello %s," - ."\n\n" - ."Someone has registered an account on VNDB.org with your email address. To confirm your registration, follow the link below." - ."\n\n" - ."%s" - ."\n\n" - ."If you don't remember creating an account on VNDB.org recently, please ignore this e-mail." - ."\n\n" - ."vndb.org", - $data->{username}, tuwf->reqBaseURI()."/u$id/setpass/$token"; - - tuwf->mail($body, - To => $data->{email}, - From => 'VNDB <noreply@vndb.org>', - Subject => "Confirm registration for $data->{username}", - ); - $elm_Success->() -}; - -1; diff --git a/lib/VN3/User/Settings.pm b/lib/VN3/User/Settings.pm deleted file mode 100644 index a63de232..00000000 --- a/lib/VN3/User/Settings.pm +++ /dev/null @@ -1,98 +0,0 @@ -package VN3::User::Settings; - -use VN3::Prelude; - - -my $FORM = { - username => { username => 1 }, - mail => { email => 1 }, - perm => { uint => 1, func => sub { ($_[0] & ~auth->allPerms) == 0 } }, - ign_votes => { anybool => 1 }, - hide_list => { anybool => 1 }, - show_nsfw => { anybool => 1 }, - traits_sexual => { anybool => 1 }, - tags_all => { anybool => 1 }, - tags_cont => { anybool => 1 }, - tags_ero => { anybool => 1 }, - tags_tech => { anybool => 1 }, - spoilers => { uint => 1, range => [ 0, 2 ] }, - - password => { _when => 'in', required => 0, type => 'hash', keys => { - old => { password => 1 }, - new => { password => 1 } - } }, - - id => { _when => 'out', uint => 1 }, - authmod => { _when => 'out', anybool => 1 }, -}; - -my $FORM_OUT = form_compile out => $FORM; -my $FORM_IN = form_compile in => $FORM; - -elm_form UserEdit => $FORM_OUT, $FORM_IN; - -my $elm_BadPass = elm_api 'BadPass'; -my $elm_BadLogin = elm_api 'BadLogin'; - -TUWF::get qr{/$UID_RE/edit}, sub { - my $u = tuwf->dbRowi('SELECT id, username, perm, ign_votes FROM users WHERE id =', \tuwf->capture('id')); - - return tuwf->resNotFound if !can_edit u => $u; - - $u->{mail} = tuwf->dbVali(select => sql_func user_getmail => \$u->{id}, \auth->uid, sql_fromhex auth->token); - $u->{authmod} = auth->permUsermod; - - # Let's not disclose this (though it's not hard to find out through other means) - if(!auth->permUsermod) { - $u->{ign_votes} = 0; - $u->{perm} = auth->defaultPerms; - } - - my $prefs = { map +($_->{key}, $_->{value}), @{ tuwf->dbAlli('SELECT key, value FROM users_prefs WHERE uid =', \$u->{id}) }}; - $u->{$_} = $prefs->{$_}||'' for qw/hide_list show_nsfw traits_sexual tags_all spoilers/; - $u->{spoilers} ||= 0; - $u->{"tags_$_"} = (($prefs->{tags_cat}||'cont,tech') =~ /$_/) for qw/cont ero tech/; - - my $title = $u->{id} == auth->uid ? 'My Preferences' : "Edit $u->{username}"; - Framework title => $title, noindex => 1, narrow => 1, sub { - FullPageForm module => 'User.Settings', data => $u, schema => $FORM_OUT; - }; -}; - - -json_api qr{/$UID_RE/edit}, $FORM_IN, sub { - my $data = shift; - my $id = tuwf->capture('id'); - - return $elm_Unauth->() if !can_edit u => { id => $id }; - - if(auth->permUsermod) { - tuwf->dbExeci(update => users => set => { - username => $data->{username}, - ign_votes => $data->{ign_votes}, - email_confirmed => 1, - }, where => { id => $id }); - tuwf->dbExeci(select => sql_func user_setperm => \$id, \auth->uid, sql_fromhex(auth->token), \$data->{perm}); - } - - if($data->{password}) { - return $elm_BadPass->() if tuwf->isUnsafePass($data->{password}{new}); - - if(auth->uid == $id) { - return $elm_BadLogin->() if !auth->setpass($id, undef, $data->{password}{old}, $data->{password}{new}); - } else { - tuwf->dbExeci(select => sql_func user_admin_setpass => \$id, \auth->uid, - sql_fromhex(auth->token), sql_fromhex auth->_preparepass($data->{password}{new}) - ); - } - } - - tuwf->dbExeci(select => sql_func user_setmail => \$id, \auth->uid, sql_fromhex(auth->token), \$data->{mail}); - - auth->prefSet($_, $data->{$_}, $id) for qw/hide_list show_nsfw traits_sexual tags_all spoilers/; - auth->prefSet(tags_cat => join(',', map $data->{"tags_$_"} ? $_ : (), qw/cont ero tech/), $id); - - $elm_Success->(); -}; - -1; diff --git a/lib/VN3/User/VNList.pm b/lib/VN3/User/VNList.pm deleted file mode 100644 index 922f81d6..00000000 --- a/lib/VN3/User/VNList.pm +++ /dev/null @@ -1,325 +0,0 @@ -package VN3::User::VNList; - -use POSIX 'ceil'; -use VN3::Prelude; -use VN3::User::Lib; - - -sub mkurl { - my $opt = shift; - $opt = { %$opt, @_ }; - delete $opt->{t} if $opt->{t} == -1; - delete $opt->{g} if !$opt->{g}; - '?'.join ';', map "$_=$opt->{$_}", sort keys %$opt; -} - - -sub SideBar { - my $opt = shift; - - Div class => 'fixed-size-left-sidebar-xl', sub { - Div class => 'vertical-selector-label', 'Status'; - Div class => 'vertical-selector', sub { - for (-1, keys %VNLIST_STATUS) { - A href => mkurl($opt, t => $_, p => 1), mkclass( - 'vertical-selector__item' => 1, - 'vertical-selector__item--active' => $_ == $opt->{t} - ), $_ < 0 ? 'All' : $VNLIST_STATUS{$_}; - } - }; - }; -} - - -sub NextPrev { - my($opt, $count) = @_; - my $numpage = ceil($count/50); - - Div class => 'd-lg-flex jc-between align-items-center', sub { - Div class => 'd-flex align-items-center', ''; - Div class => 'd-block d-lg-none mb-2', ''; - Div class => 'd-flex jc-right align-items-center', sub { - A href => mkurl($opt, p => $opt->{p}-1), mkclass(btn => 1, 'btn--disabled' => $opt->{p} <= 1), '< Prev'; - Div class => 'mx-3 semi-muted', sprintf 'page %d of %d', $opt->{p}, $numpage; - A href => mkurl($opt, p => $opt->{p}+1), mkclass(btn => 1, 'btn--disabled' => $opt->{p} >= $numpage), 'Next >'; - }; - }; -} - - -sub EditDropDown { - my($u, $opt, $item) = @_; - return if $u->{id} != (auth->uid||0); - Div 'data-elm-module' => 'UVNList.Options', - 'data-elm-flags' => JSON::XS->new->encode({uid => $u->{id}, item => $item}), - ''; -} - - -sub VNTable { - my($u, $lst, $opt) = @_; - - my $SortHeader = sub { - my($id, $label) = @_; - my $isasc = $opt->{s} eq $id && $opt->{o} eq 'a'; - A mkclass( - 'table-header' => 1, - 'with-sort-icon' => 1, - 'with-sort-icon--down' => !$isasc, - 'with-sort-icon--up' => $isasc, - 'with-sort-icon--active' => $opt->{s} eq $id, - ), href => mkurl($opt, p => 1, s => $id, o => $isasc ? 'd' : 'a'), $label; - }; - - Table class => 'table table--responsive-single-sm fs-medium vn-list', sub { - Thead sub { - Tr sub { - Th width => '15%', class => 'th--nopad', sub { $SortHeader->(date => 'Date' ) }; - Th width => '40%', class => 'th--nopad', sub { $SortHeader->(title => 'Title') }; - Th width => '10%', class => 'th--nopad', sub { $SortHeader->(vote => 'Vote' ) }; - Th width => '13%', 'Status'; - Th width => '7.33%', ''; - Th width => '7.33%', ''; - Th width => '7.33%', ''; - }; - }; - Tbody sub { - for my $l (@$lst) { - Tr sub { - Td class => 'tabular-nums muted', date_display $l->{date}; - Td sub { - A href => "/v$l->{id}", title => $l->{original}||$l->{title}, $l->{title}; - }; - - if($u->{id} == (auth->uid||0)) { - Td class => 'table-edit-overlay-base', sub { - Div 'data-elm-module' => 'UVNList.Vote', - 'data-elm-flags' => JSON::XS->new->encode({uid => int $u->{id}, vid => int $l->{id}, vote => ''.vote_display $l->{vote}}), - vote_display $l->{vote}; - }; - Td class => 'table-edit-overlay-base', sub { - Div 'data-elm-module' => 'UVNList.Status', - 'data-elm-flags' => JSON::XS->new->encode({uid => int $u->{id}, vid => int $l->{id}, status => int $l->{status}||0}), - $VNLIST_STATUS{$l->{status}||0}; - }; - } else { - Td vote_display $l->{vote}; - Td $VNLIST_STATUS{$l->{status}||0}; - } - - # Release info - Td sub { - A href => 'javascript:;', class => 'vn-list__expand-releases', sub { - Span class => 'expand-arrow mr-2', ''; - Txt sprintf '%d/%d', (scalar grep $_->{status}==2, @{$l->{rel}}), scalar @{$l->{rel}}; - } if @{$l->{rel}}; - }; - - # Notes - Td sub { - # TODO: vn-list__expand-comment--empty for 'add comment' things - A href => 'javascript:;', class => 'vn-list__expand-comment', sub { - Span class => 'expand-arrow mr-2', ''; - Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/heavy/comment.svg'; - } if $l->{notes}; - }; - - Td sub { EditDropDown $u, $opt, $l }; - }; - - # Release info - Tr class => 'vn-list__releases-row d-none', sub { - Td colspan => '6', sub { - Div class => 'vn-list__releases', sub { - Table class => 'table table--responsive-single-sm ml-3', sub { - Tbody sub { - for my $r (@{$l->{rel}}) { - Tr sub { - Td width => '15%', class => 'tabular-nums muted pl-0', date_display $r->{date}; - Td width => '50%', sub { - A href => "/v$r->{rid}", title => $r->{original}||$r->{title}, $r->{title}; - }; - # TODO: Editabe - Td width => '20%', $RLIST_STATUS{$l->{status}}; - Td width => '15%', ''; # TODO: Edit menu - } - } - } - } - } - } - } if @{$l->{rel}}; - - # Notes - Tr class => 'vn-list__comment-row d-none', sub { - Td colspan => '6', sub { - # TODO: Editable - Div class => 'vn-list__comment ml-3', $l->{notes}; - } - } if $l->{notes}; - }; - }; - }; -} - - -sub VNGrid { - my($u, $lst, $opt) = @_; - - Div class => 'vn-grid mb-4', sub { - for my $l (@$lst) { - Div class => 'vn-grid__item', sub { - # TODO: NSFW hiding? What about missing images? - Div class => 'vn-grid__item-bg', style => sprintf("background-image: url('%s')", tuwf->imgurl(cv => $l->{image})), ''; - Div class => 'vn-grid__item-overlay', sub { - A href => 'javascript:;', class => 'vn-grid__item-link', ''; # TODO: Open modal on click - Div class => 'vn-grid__item-top', sub { - EditDropDown $u, $opt, $l; - Div class => 'vn-grid__item-rating', sub { - Img class => 'svg-icon', src => tuwf->conf->{url_static}.'/v3/heavy/comment.svg' if $l->{notes}; - Lit ' '; - Txt vote_display $l->{vote}; - } - }; - Div class => 'vn-grid__item-name', $l->{title}; - } - } - } - } -} - - -sub List { - my($u, $opt) = @_; - - my $lst = tuwf->dbAlli(q{ - SELECT v.id, v.title, v.original, vl.status, vl.notes, vo.vote, v.image, }, - sql_totime('LEAST(vl.added, vo.date)'), q{AS date, - count(*) OVER() AS full_count - FROM vn v - LEFT JOIN votes vo ON vo.vid = v.id AND vo.uid =}, \$u->{id}, q{ - LEFT JOIN vnlists vl ON vl.vid = v.id AND vl.uid =}, \$u->{id}, q{ - WHERE }, sql_and( - 'vo.vid IS NOT NULL OR vl.vid IS NOT NULL', - $opt->{t} >= 1 ? sql('vl.status =', \$opt->{t}) : $opt->{t} == 0 ? 'vl.status = 0 OR vl.status IS NULL' : () - ), - 'ORDER BY', { - title => 'v.title', - date => 'LEAST(vl.added, vo.date)', - vote => 'vo.vote', - }->{$opt->{s}}, - $opt->{o} eq 'a' ? 'ASC' : 'DESC', - 'NULLS LAST', - 'LIMIT', \50, - 'OFFSET', \(($opt->{p}-1)*50) - ); - my $count = @$lst ? $lst->[0]{full_count} : 0; - delete $_->{full_count} for @$lst; - - enrich_list rel => id => vid => sub { sql q{ - SELECT rv.vid, rl.rid, rl.status, r.title, r.original, }, sql_totime('rl.added'), q{ AS date - FROM rlists rl - JOIN releases r ON r.id = rl.rid - JOIN releases_vn rv ON rv.id = r.id - WHERE rl.uid =}, \$u->{id}, q{AND rv.vid IN}, $_[0] - }, $lst; - - Div class => 'col-md', sub { - Div class => 'card card--white card--no-separators mb-5', sub { - Div class => 'card__header', sub { - Div class => 'card__title', 'List'; - Debug $lst; - Div class => 'card__header-buttons', sub { - Div class => 'btn-group', sub { - A href => mkurl($opt, g => 0), mkclass(btn => 1, active => !$opt->{g}, 'js-show-vn-list' => 1), \&ListIcon; - A href => mkurl($opt, g => 1), mkclass(btn => 1, active => $opt->{g}, 'js-show-vn-grid' => 1), \&GridIcon; - }; - }; - }; - - VNTable $u, $lst, $opt unless $opt->{g}; - Div class => 'card__body fs-medium', sub { - VNGrid $u, $lst, $opt if $opt->{g}; - NextPrev $opt, $count; - }; - } - }; -} - - -TUWF::get qr{/$UID_RE/list}, sub { - my $uid = tuwf->capture('id'); - my $u = tuwf->dbRowi(q{ - SELECT u.id, u.username, hd.value AS hide_list - FROM users u - LEFT JOIN users_prefs hd ON hd.uid = u.id AND hd.key = 'hide_list' - WHERE u.id =}, \$uid - ); - return tuwf->resNotFound if !$u->{id} || !show_list $u; - - my $opt = tuwf->validate(get => - t => { vnlist_status => 1, required => 0, default => -1 }, # status - p => { page => 1 }, # page - o => { enum => ['d','a'], required => 0, default => 'a' }, # order (asc/desc) - s => { enum => ['title', 'date', 'vote'], required => 0, default => 'title' }, # sort column - g => { anybool => 1 }, # grid - )->data; - - Framework - title => $u->{username}, - index => 0, - top => sub { - Div class => 'col-md', sub { - Div class => 'detail-page-title', ucfirst $u->{username}; - TopNav list => $u; - } - }, - sub { - Div class => 'row', sub { - SideBar $opt; - List $u, $opt; - }; - }; -}; - - -json_api '/u/setvote', { - uid => { id => 1 }, - vid => { id => 1 }, - vote => { vnvote => 1 } -}, sub { - my $data = shift; - return $elm_Unauth->() if (auth->uid||0) != $data->{uid}; - - tuwf->dbExeci( - 'DELETE FROM votes WHERE', - { vid => $data->{vid}, uid => $data->{uid} } - ) if !$data->{vote}; - - tuwf->dbExeci( - 'INSERT INTO votes', - { vid => $data->{vid}, uid => $data->{uid}, vote => $data->{vote} }, - 'ON CONFLICT (vid, uid) DO UPDATE SET', - { vote => $data->{vote} } - ) if $data->{vote}; - - $elm_Success->() -}; - - -json_api '/u/setvnstatus', { - uid => { id => 1 }, - vid => { id => 1 }, - status => { vnlist_status => 1 } -}, sub { - my $data = shift; - return $elm_Unauth->() if (auth->uid||0) != $data->{uid}; - - tuwf->dbExeci( - 'INSERT INTO vnlists', - { vid => $data->{vid}, uid => $data->{uid}, status => $data->{status} }, - 'ON CONFLICT (vid, uid) DO UPDATE SET', - { status => $data->{status} } - ); - $elm_Success->(); -}; |