diff --git a/routes/global.js b/routes/global.js index 83aa326..cf1501e 100644 --- a/routes/global.js +++ b/routes/global.js @@ -14,35 +14,105 @@ function format_yymmdd(x){ return `${year}-${month}-${day}`; } +function cleanup_sysdep_desc(str){ + if(!str) return ""; + var str = str.charAt(0).toUpperCase() + str.slice(1); + return str.replace(/\(.*\)$/, '').replace('SASL -', 'SASL').replace(/[-,]+ .*(shared|runtime|binary|library|legacy|precision|quantum).*$/i, ''); +} + +function check_to_color(check){ + switch (check) { + case 'ERROR': + return 'text-danger'; + case 'WARNING': + return 'text-warning'; + case 'NOTE': + return 'text-success'; + case 'OK': + return 'text-success'; + default: + return 'text-dark'; + } +} + +function os_icon(type){ + switch (type) { + case 'win': + return 'fa-windows'; + case 'linux': + return 'fa-linux'; + case 'mac': + return 'fa-apple'; + default: + return 'fa-question'; + } +} + router.get("/_global/search", function(req, res, next){ - res.render("search"); + res.render("global/search"); }); router.get("/_global/activity", function(req, res, next){ - res.render("activity"); + res.render("global/activity"); +}); + +router.get("/_global/builds", function(req, res, next){ + db.get_builds().then(function(packages){ + packages.forEach(function(x){ + var checks = x.runs.filter(run => run.check && run.built.Platform !== 'x86_64-apple-darwin20'); + x.check_icon_html = function(version, type){ + var bin = checks.find(run => run.built.R.startsWith(version) && run.type == type); + if(bin){ + return `` + } else if(type != 'linux' && x.user == 'cran'){ + return ''; + } else { + return ''; + } + }; + x.date = format_yymmdd(x.timestamp * 1000); + x.src = x.runs.find(run => run.type == 'src') || {}; + x.failure = x.runs.find(run => run.type == 'failure'); + }); + res.render('global/builds', {packages: packages}); + }).catch(next); }); router.get("/_global/organizations", function(req, res, next){ db.get_organizations().then(function(orgs){ - res.render('organizations', {orgs: orgs}); + orgs.forEach(function(x){ + x.avatar = x.uuid ? `https://avatars.githubusercontent.com/u/${x.uuid}` : `https://r-universe.dev/avatars/${x.universe}.png`; + }); + res.render('global/organizations', {orgs: orgs}); }).catch(next); }); router.get("/_global/repositories", function(req, res, next){ db.get_repositories().then(function(repos){ - res.render('repositories', {repos: repos, format_yymmdd: format_yymmdd}); + res.render('global/repositories', {repos: repos, format_yymmdd: format_yymmdd}); }); }); router.get("/_global/scores", function(req, res, next){ db.get_scores().then(function(packages){ - res.render('scores', {packages: packages}); + res.render('global/scores', {packages: packages}); + }); +}); + +router.get("/_global/sysdeps", function(req, res, next){ + db.get_sysdeps().then(function(sysdeps){ + sysdeps = sysdeps.filter(x => x.library && x.library !== 'c++'); + sysdeps.forEach(function(x){ + x.description = cleanup_sysdep_desc(x.description); + x.usedby = x.usedby.sort((a,b) => a.package.toLowerCase() < b.package.toLowerCase() ? -1 : 1); + }); + res.render('global/sysdeps', {sysdeps: sysdeps}); }); }); router.get("/_global/galaxy", function(req, res, next){ db.get_organizations().then(function(orgs){ - res.render('galaxy', {rnd: rnd, orgs: orgs.slice(0, 250)}); + res.render('global/galaxy', {rnd: rnd, orgs: orgs.slice(0, 250)}); }).catch(next); }); diff --git a/src/db.js b/src/db.js index 125cc11..bb1c7d2 100644 --- a/src/db.js +++ b/src/db.js @@ -184,6 +184,128 @@ function mongo_universe_vignettes(user){ return cursor.toArray(); } +function mongo_all_universes(organizations_only){ + var query = {_type: 'src', _registered: true}; + if(organizations_only){ + query['_userbio.type'] = 'organization'; + query['_user'] = {$ne: 'cran'}; + } + var cursor = mongo_aggregate([ + {$match: query}, + {$sort:{ _id: -1}}, + {$group: { + _id : '$_user', + updated: { $max: '$_commit.time'}, + packages: { $sum: 1 }, + indexed: { $sum: { $toInt: '$_indexed' }}, + name: { $first: '$_userbio.name'}, + type: { $first: '$_userbio.type'}, + uuid: { $first: '$_userbio.uuid'}, + bio: { $first: '$_userbio.description'}, + emails: { $addToSet: '$_maintainer.email'} + }}, + {$project: {_id: 0, universe: '$_id', packages: 1, updated: 1, type: 1, uuid: 1, + indexed:1, name: 1, type: 1, bio: 1, maintainers: { $size: '$emails' }, + }}, + {$sort:{ indexed: -1}} + ]); + return cursor.toArray(); +} + +function mongo_all_scores(){ + function array_size(key){ + return {$cond: [{ $isArray: key }, {$size: key}, 0 ]}; + } + var query = {_type: 'src', _indexed: true}; + var projection = { + _id: 0, + package: '$Package', + universe: "$_user", + score: '$_score', + stars: "$_stars", + downloads: "$_downloads.count", + scripts: "$_searchresults", + dependents: '$_usedby', + commits: {$sum: '$_updates.n'}, + contributors: array_size({$objectToArray: '$_contributions'}), + datasets: array_size('$_datasets'), + vignettes: array_size('$_vignettes'), + releases: array_size('$_releases') + } + var cursor = mongo_find(query).sort({_score: -1}).project(projection); + return cursor.toArray(); +} + +function mongo_all_sysdeps(distro){ + var query = {_type: 'src', '_sysdeps': {$exists: true}}; + if(distro){ + query['_distro'] = distro; + } + var cursor = mongo_aggregate([ + {$match: query}, + {$unwind: '$_sysdeps'}, + {$group: { + _id : '$_sysdeps.name', + packages: { $addToSet: '$_sysdeps.package'}, + headers: { $addToSet: '$_sysdeps.headers'}, + version: { $first: '$_sysdeps.version'}, + homepage: { $addToSet: '$_sysdeps.homepage'}, + description: { $addToSet: '$_sysdeps.description'}, + distro : { $addToSet: '$_distro'}, + usedby : { $addToSet: {owner: '$_owner', package:'$Package'}} + }}, + {$project: {_id: 0, library: '$_id', packages: 1, headers: 1, version: 1, usedby: 1, + homepage: { '$first' : '$homepage'}, description: { '$first' : '$description'}, distro:{ '$first' : '$distro'}}}, + {$sort:{ library: 1}} + ]); + return cursor.toArray(); +} + +function days_ago(n){ + var now = new Date(); + return now.getTime()/1000 - (n*60*60*24); +} + +function mongo_recent_builds(days = 7){ + var query = {'_commit.time' : {'$gt': days_ago(days)}}; + var cursor = mongo_aggregate([ + {$match: query}, + {$group : { + _id : { user: '$_user', package: '$Package', commit: '$_commit.id'}, + version: { $first : "$Version" }, + maintainer: { $first : "$_maintainer.name" }, + maintainerlogin: { $first : "$_maintainer.login" }, + timestamp: { $first : "$_commit.time" }, + upstream: { $first : "$_upstream" }, + registered: { $first: "$_registered" }, + os_restriction: { $addToSet: '$OS_type'}, + macbinary: { $addToSet : '$_macbinary' }, + winbinary: { $addToSet : '$_winbinary' }, + runs : { $addToSet: + { type: "$_type", built: '$Built', date:'$_published', url: '$_buildurl', status: '$_status', distro: '$_distro', check: '$_check'} + } + }}, + {$sort : {"timestamp" : -1}}, + {$project: { + _id: 0, + user: '$_id.user', + package: '$_id.package', + commit: '$_id.commit', + maintainer: 1, + maintainerlogin: 1, + version: 1, + timestamp: 1, + registered: 1, + runs: 1, + upstream: 1, + macbinary: { $first: "$macbinary" }, + winbinary: { $first: "$winbinary" }, + os_restriction:{ $first: "$os_restriction" } + }} + ]); + return cursor.toArray(); +} + function get_package_info(package, universe){ if(production){ return mongo_package_info(package, universe); @@ -236,15 +358,35 @@ function get_scores(){ function get_organizations(){ if(production){ - return mongo_all_universes() + return mongo_all_universes(true); } else { console.warn(`Fetching universes data from API...`); return get_ndjson(`https://r-universe.dev/api/universes?type=organization&skipcran=1&stream=1`); } } +function get_sysdeps(){ + if(production){ + return mongo_all_sysdeps() + } else { + console.warn(`Fetching sysdeps data from API...`); + return get_ndjson(`https://r-universe.dev/stats/sysdeps?all=1`); + } +} + +function get_builds(){ + if(production){ + return mongo_recent_builds() + } else { + console.warn(`Fetching builds data from API...`); + return get_ndjson(`https://r-universe.dev/stats/builds?limit=1000`); + } +} + module.exports = { get_scores: get_scores, + get_builds : get_builds, + get_sysdeps : get_sysdeps, get_repositories: get_repositories, get_organizations: get_organizations, get_package_info: get_package_info, diff --git a/static/layout.css b/static/layout.css index 4d78767..f1d9efc 100644 --- a/static/layout.css +++ b/static/layout.css @@ -149,4 +149,31 @@ canvas { p.overflow-x-invisible::-webkit-scrollbar { display: none; } -} \ No newline at end of file +} + +.logo-img{ + height: 70px +} + +#global-sidebar { + width: 60px; + padding: 15px 5px 15px 5px; +} + +#global-sidebar:hover{ + width: 240px; + margin-right: -180px; + z-index: 9; +} + +#global-sidebar .menu-expanded { + display: none; +} + +#global-sidebar:hover .menu-expanded { + display: unset; +} + +#global-sidebar:hover .menu-collapsed { + display: none; +} diff --git a/static/logo-big.svg b/static/logo-big.svg new file mode 100644 index 0000000..94b95d5 --- /dev/null +++ b/static/logo-big.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + R-universe + \ No newline at end of file diff --git a/static/logo-icon.svg b/static/logo-icon.svg new file mode 100644 index 0000000..267398b --- /dev/null +++ b/static/logo-icon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/static/organizations.js b/static/organizations.js deleted file mode 100644 index 6810fb4..0000000 --- a/static/organizations.js +++ /dev/null @@ -1,8 +0,0 @@ -window.onload = function(){ - /* Workaround for: https://github.com/desandro/masonry/issues/1147 */ - var msnry = new Masonry('.card-rows'); - $('.avatar').on('load', function(){ - msnry.layout(); - console.log("refresh") - }); -}; diff --git a/testprod.sh b/testprod.sh new file mode 100755 index 0000000..6b19ea9 --- /dev/null +++ b/testprod.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# NB: docker compose server should be restarted without -d to open external mongo port +export CRANLIKE_MONGODB_SERVER=packages.r-universe.dev +eval $(cat ../production-server/secrets.env) npm start diff --git a/views/activity.pug b/views/activity.pug deleted file mode 100644 index ec80ce1..0000000 --- a/views/activity.pug +++ /dev/null @@ -1,20 +0,0 @@ -doctype html -head - meta(charset='utf-8') - meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no') - link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/css/bootstrap.min.css') - title R-universe: personal package repositories for R! - -#content-container.container(style='max-width: 1440px') - // Careful: padding may result unsharp/pixelated bitmap rendering - .col.p-0 - canvas#activity-canvas.col.p-0(height='300') - .col.p-0 - canvas#contributors-canvas.col.p-0(height='700') - -include includes/default-scripts.pug -script(src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js') -script(src='https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/3.0.1/chartjs-plugin-annotation.min.js') -script(src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js') -script(src='https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.1/chartjs-adapter-moment.min.js') -script(src='/activity.js') diff --git a/views/global.pug b/views/global.pug deleted file mode 100644 index f56b918..0000000 --- a/views/global.pug +++ /dev/null @@ -1,26 +0,0 @@ -doctype html -html - head - meta(charset='utf-8') - meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no') - link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/css/bootstrap.min.css') - link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css') - link(rel='stylesheet' href='/layout.css') - title R-universe - Organizations - - body - header.mb-3.border-bottom.bg-body-tertiary - .container(style='max-width: 1440px') - nav.navbar - form#searchform.mx-auto.d-none.d-md-flex.input-group.col-auto(role='search') - input.form-control(type='search' placeholder=`search for a package, author, keyword, ...`) - button.btn.btn-outline-secondary(type='submit') Search - a.m-2.d-block(href='#' data-bs-toggle='dropdown') - img#mainlogo(src='https://r-universe.dev/runiverse.svg', width=110) - - main.col.mx-sm-2 - block content - - block before_scripts - include includes/default-scripts.pug - block after_scripts \ No newline at end of file diff --git a/static/activity.js b/views/global/activity.js similarity index 97% rename from static/activity.js rename to views/global/activity.js index 7253342..5e29944 100644 --- a/static/activity.js +++ b/views/global/activity.js @@ -126,10 +126,10 @@ function make_contributor_chart(universe, max, imsize){ max = max || 100; return get_ndjson(`https://${universe && universe + "." || ""}r-universe.dev/stats/contributors?all=true&limit=${max}`).then(function(contributors){ const size = imsize || 50; - contributors = contributors.sort(function(x,y){return x.repos.length < y.repos.length ? 1 : -1}); + //contributors = contributors.sort(function(x,y){return x.repos.length < y.repos.length ? 1 : -1}); contributors = contributors.filter(x => x.login != 'nturaga') //this is a bot from BioConductor const logins = contributors.map(x => x.login); - const totals = contributors.map(x => x.repos.length); + const totals = contributors.map(x => x.total); const counts = contributors.map(x => sort_packages(x.repos).map(x => x.upstream.split(/[\\/]/).pop())); const avatars = logins.map(x => `https://r-universe.dev/avatars/${x.replace('[bot]', '')}.png?size=${size}`); const images = avatars.map(x => undefined); diff --git a/views/global/activity.pug b/views/global/activity.pug new file mode 100644 index 0000000..ae785d0 --- /dev/null +++ b/views/global/activity.pug @@ -0,0 +1,18 @@ +extends global + +block content + #content-container.container-fluid.p-0 + // Careful: padding may result unsharp/pixelated bitmap rendering + .col.p-0 + canvas#activity-canvas.col.p-0(height='300') + .col.p-0 + canvas#contributors-canvas.col.p-0(height='700') + + +block after_scripts + script(src='https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.js') + script(src='https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-annotation/3.0.1/chartjs-plugin-annotation.min.js') + script(src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.30.1/moment.min.js') + script(src='https://cdnjs.cloudflare.com/ajax/libs/chartjs-adapter-moment/1.0.1/chartjs-adapter-moment.min.js') + script + include activity.js diff --git a/views/global/builds.pug b/views/global/builds.pug new file mode 100644 index 0000000..ac193c4 --- /dev/null +++ b/views/global/builds.pug @@ -0,0 +1,56 @@ +extends global + +block content + #content-container.container-fluid + table.table + thead + tr.border-bottom.border-dark + th Commit + th Universe + th Package + th Version + th Maintainer + th Vignettes + th R-4.5 | R-4.4 | R-4.3 + tbody + each x in packages + tr + td.text-nowrap #{x.date} + td + a.text-secondary(href=`https://${x.user}.r-universe.dev`) #{x.user} + td + if x.src.status + a.fw-semibold(href=`https://${x.user}.r-universe.dev/${x.package}`) #{x.package} + else + s #{x.package} + if x.failure + a.ms-1.text-danger.fw-semibold(href=`${x.failure.url}` target='_blank') (build failure) + td + a.text-dark(href=`${x.upstream}/commit/${x.commit}` target='_blank') #{x.version} + td + if x.maintainerlogin + a.text-secondary(href=`https://${x.maintainerlogin}.r-universe.dev/`) #{x.maintainer} + else + span.text-secondary #{x.maintainer} + td + if x.src.status + a(href=x.src.url target='_blank' class=`${x.src.status == 'success' ? "text-success" : "text-danger"}`).noline + i.fa-fw.fa-solid.fa-box-archive + td + a(href=x.src.url target='_blank').text-secondary.noline + | !{x.check_icon_html('4.5', 'linux')} + | !{x.check_icon_html('4.5', 'win')} + | | + | !{x.check_icon_html('4.4', 'win')} + | !{x.check_icon_html('4.4', 'mac')} + | | + | !{x.check_icon_html('4.3', 'win')} + | !{x.check_icon_html('4.3', 'mac')} + +block after_head + link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css') + +block after_scripts + script(src='https://cdn.datatables.net/2.1.3/js/dataTables.min.js') + script + include updates.js diff --git a/views/includes/galaxy.js b/views/global/galaxy.js similarity index 100% rename from views/includes/galaxy.js rename to views/global/galaxy.js diff --git a/views/galaxy.pug b/views/global/galaxy.pug similarity index 79% rename from views/galaxy.pug rename to views/global/galaxy.pug index 7fff542..6276ddb 100644 --- a/views/galaxy.pug +++ b/views/global/galaxy.pug @@ -10,9 +10,8 @@ img#runiverselogo(src='https://r-universe.dev/runiverse.svg') svg(viewbox='0 0 800 600' preserveaspectratio='xMinYMid slice') g#stars each x in orgs - if x.universe != 'cran' - a(href=`https://${x.universe}.r-universe.dev` data-bs-toggle="tooltip" data-bs-title=x.universe) - circle(cx=`${rnd(100)}%`, cy=`${rnd(100)}%`, r=Math.sqrt(Math.sqrt(x.indexed)), style=`animationDelay: ${rnd(100)/10}s`) + a(href=`https://${x.universe}.r-universe.dev` data-bs-toggle="tooltip" data-bs-title=x.universe) + circle(cx=`${rnd(100)}%`, cy=`${rnd(100)}%`, r=Math.sqrt(Math.sqrt(x.indexed)), style=`animationDelay: ${rnd(100)/10}s`) g#comets line(x1='1185' y1='-15' x2='77.5' y2='1092.5') @@ -28,6 +27,6 @@ svg(viewbox='0 0 800 600' preserveaspectratio='xMinYMid slice') circle polygon(points='371.3,338.6 355.7,344.7 363.1,329.7') -include includes/default-scripts.pug +include ../includes/default-scripts.pug script - include includes/galaxy.js \ No newline at end of file + include galaxy.js \ No newline at end of file diff --git a/views/global/global.css b/views/global/global.css new file mode 100644 index 0000000..5f40d7e --- /dev/null +++ b/views/global/global.css @@ -0,0 +1,173 @@ +main { + font-size: .875rem; + padding-left: 80px; +} + +.menu-logo { + background-image: url("/logo-icon.svg"); + background-repeat: no-repeat; + background-size: 60%; + background-position: center; + filter: invert(100%); + height: 80px; +} + +#global-sidebar { + width: 72px; + box-shadow: 10px 0 10px -2px #888; +} + +#global-sidebar i.bi { + font-size: 1.2em; +} + +#global-sidebar:hover{ + width: 250px; +} + +#global-sidebar .menu-expanded { + display: none; +} + +#global-sidebar:hover .menu-expanded { + display: unset; + padding-left: 10px; +} + +#global-sidebar:hover .menu-logo{ + background-image: url("/logo-big.svg"); + filter: unset; +} + +a { + color: #3b71ca; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +a.noline:hover { + text-decoration: none; +} + +.maintainer-card{ + transition: 0.3s; +} + +.maintainer-card:hover { + background-color: #eee; +} + +svg { + height: 150px; +} + +#rocket { + animation: shakes 3s linear infinite; +} + +@keyframes shakes { + 10% { + transform: translate(5px, 4px); + } + 20% { + transform: translate(1px, 4px); + } + 30% { + transform: translate(1px, 4px); + } + 40% { + transform: translate(1px, 1px); + } + 50% { + transform: translate(5px, 3px); + } + 60% { + transform: translate(2px, 4px); + } + 70% { + transform: translate(3px, 3px); + } + 80% { + transform: translate(4px, 3px); + } + 90% { + transform: translate(4px, 3px); + } + 100% { + transform: translate(0, 0); + } +} +#rocket path, #rocket circle, #rocket polygon { + fill: #333; +} +#rocket circle { + cx: 369.5; + cy: 331.8; + r: 7; +} +#rocket polygon { + animation: burst 0.5s infinite; + transform-origin: 363px 337px; +} + +@keyframes burst { + 20% { + transform: scale(0.8); + } + 40% { + transform: scale(0.6); + } + 60% { + transform: scale(1); + } + 80% { + transform: scale(0.8); + } + 100% { + transform: scale(1); + } +} + +.carousel-control{ + color: #000 !important; + width: auto !important; +} + +.carousel-control-prev { + left: -35px; +} + +.carousel-control-next { + right: -35px; +} + +#extra-search-fields ::placeholder { + opacity: .6; + font-style: italic; +} + +.blog-posts { + padding-left: 10px; +} + +.blog-posts li { + padding: 3px 5px; + list-style-type: "ยป" !important; +} + +.blog-posts li span { + float: right; + padding-top: 3px; + font-size: 12px; + color: #999; + text-transform: uppercase; +} + +.avatar { + width: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; +} diff --git a/views/global/global.pug b/views/global/global.pug new file mode 100644 index 0000000..b63ed6a --- /dev/null +++ b/views/global/global.pug @@ -0,0 +1,64 @@ +doctype html +html + head + meta(charset='utf-8') + meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no') + link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/css/bootstrap.min.css') + link(rel='stylesheet' href='https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css') + block after_head + style + include global.css + + title R-universe - Organizations + + body + #global-sidebar.text-bg-dark.fixed-top.vh-100 + ul.nav.nav-pills.nav-flush.flex-column.mb-auto + li.nav-item.text-center.menu-logo + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='./search') + i.bi.bi-house-door.pe-none.px-2 + span.menu-expanded Search + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='./organizations') + i.bi.bi-globe-americas.pe-none.px-2 + span.menu-expanded Community + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='./repositories') + i.bi.bi-grid.pe-none.px-2 + span.menu-expanded Repositories + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='./scores') + i.bi.bi-boxes.pe-none.px-2 + span.menu-expanded Scoreboard + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='./builds') + i.bi.bi-arrow-clockwise.pe-none.px-2 + span.menu-expanded Recent Builds + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='./sysdeps') + i.bi.bi-gear.pe-none.px-2 + span.menu-expanded System libs + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='./activity') + i.bi.bi-people.pe-none.px-2 + span.menu-expanded Activity + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='./galaxy') + i.bi.bi-rocket-takeoff.pe-none.px-2 + span.menu-expanded Galaxy + li.nav-item + a.nav-link.py-3.border-bottom.rounded-0.text-white(href='https://docs.r-universe.dev' target='_blank') + i.bi.bi-info-circle.pe-none.px-2 + span.menu-expanded Docs + + script + | var navlink = document.querySelector(`a.nav-link[href='${location.pathname.replace(/.*\//, './')}']`); + | if(navlink) navlink.classList.add('active') + + main.col.vw-100 + block content + + block before_scripts + include ../includes/default-scripts.pug + block after_scripts \ No newline at end of file diff --git a/views/global/organizations.js b/views/global/organizations.js new file mode 100644 index 0000000..3d98bd2 --- /dev/null +++ b/views/global/organizations.js @@ -0,0 +1,14 @@ +window.onload = function(){ + /* Workaround for: https://github.com/desandro/masonry/issues/1147 */ + const msnry = new Masonry('.card-rows'); + const refresh = debounce(() => msnry.layout()); + $('.avatar').on('load', refresh); +}; + +function debounce(func, timeout = 100){ + let timer; + return (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { func.apply(this, args); }, timeout); + }; +} diff --git a/views/organizations.pug b/views/global/organizations.pug similarity index 58% rename from views/organizations.pug rename to views/global/organizations.pug index 29669e6..934ff43 100644 --- a/views/organizations.pug +++ b/views/global/organizations.pug @@ -1,12 +1,12 @@ extends global block content - #content-container.container(style='max-width: 1440px' data-masonry='{"percentPosition":true}') + #content-container.container-fluid(style='max-width: 1600px' data-masonry='{"percentPosition":true}') .card-group.card-rows each x in orgs .col-xl-2.col-lg-3.col-md-4.col-6 a.card.m-2.package-description-card(href=`https://${x.universe}.r-universe.dev`).text-decoration-none - img.p-2.avatar.card-img-top(src=`https://r-universe.dev/avatars/${x.universe}.png?size=214` loading='lazy') + img.p-2.avatar.card-img-top(src=`${x.avatar}` loading='lazy') .card-body h5 #{x.universe} p.card-text #{x.bio || x.name} @@ -15,5 +15,5 @@ block content block after_scripts - script(src='https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.min.js') - script(src='/organizations.js') + script + include organizations.js diff --git a/static/repositories.js b/views/global/repositories.js similarity index 100% rename from static/repositories.js rename to views/global/repositories.js diff --git a/views/repositories.pug b/views/global/repositories.pug similarity index 62% rename from views/repositories.pug rename to views/global/repositories.pug index 2261f2d..11d4155 100644 --- a/views/repositories.pug +++ b/views/global/repositories.pug @@ -1,17 +1,17 @@ extends global block content - #content-container.container(style='max-width: 1440px') - table.table.table-bordered.d-none + #content-container.container-fluid + table.table.d-none thead - tr - th universe - th packages - th indexed - th maintainers - th type - th name - th updated + tr.border-bottom.border-dark + th Universe + th Packages + th Indexed + th Maintainers + th Type + th Name + th Updated tbody each x in repos tr @@ -26,4 +26,5 @@ block content block after_scripts script(src='https://cdn.datatables.net/2.1.3/js/dataTables.min.js') - script(src="/repositories.js") + script + include repositories.js diff --git a/static/scores.js b/views/global/scores.js similarity index 100% rename from static/scores.js rename to views/global/scores.js diff --git a/views/scores.pug b/views/global/scores.pug similarity index 59% rename from views/scores.pug rename to views/global/scores.pug index 4d0a30a..56e582c 100644 --- a/views/scores.pug +++ b/views/global/scores.pug @@ -1,25 +1,25 @@ extends global block content - #content-container.container(style='max-width: 1440px') - table.table.table-bordered.d-none + #content-container.container-fluid + table.table.d-none thead - tr - th package - th score - th stars - th downloads - th scripts - th dependents - th contributors - th vignettes - th datasets - th releases - th commits/year + tr.border-bottom.border-dark + th Package + th Score + th Stars + th Downloads + th Scripts + th Dependents + th Contributors + th Vignettes + th Datasets + th Releases + th Commits/year tbody each x in packages tr - td + th a(href=`https://${x.universe}.r-universe.dev/${x.package}`) #{x.package} td #{Math.pow(x.score-1, 2).toFixed(2)} td #{x.stars} @@ -34,4 +34,5 @@ block content block after_scripts script(src='https://cdn.datatables.net/2.1.3/js/dataTables.min.js') - script(src="/scores.js") + script + include scores.js diff --git a/static/search.js b/views/global/search.js similarity index 99% rename from static/search.js rename to views/global/search.js index f689131..92be158 100644 --- a/static/search.js +++ b/views/global/search.js @@ -167,12 +167,14 @@ function get_query(){ function update_results(){ var q = $("#search-input").val(); $("title").text(`R-universe search: ${q}`) - if(!q){ + if(q.length < 2){ $('#search-results').empty(); + $('#search-results-comment').empty(); $('#results-placeholder').show(); $('svg').show('fast', () => $('#search-input').focus()); + msnry.layout(); + return; } - if(q.length < 2) return; $('#search-results').empty(); $('#results-placeholder').hide(); $('svg').hide('fast'); diff --git a/views/global/search.pug b/views/global/search.pug new file mode 100644 index 0000000..b24795e --- /dev/null +++ b/views/global/search.pug @@ -0,0 +1,101 @@ +extends global + +block content + .container.p-4#search-container + .d-flex.justify-content-center + svg(viewbox='250 250 300 150' preserveaspectratio='xMinYMid slice') + g#rocket + path(d='M445.3,256.3c-47.3-3.5-62.8,27-62.8,27c-18.5,2-24,10-24,10l15.5,12.3c-0.5,15.3,3.5,16.5,3.5,16.5c5.5,7.5,17.8,4.8,17.8,4.8l12.8,17c12-12.5,10.3-25.5,10.3-25.5C450.8,300,445.3,256.3,445.3,256.3zM416.3,297c-6.5,0-11.8-5.3-11.8-11.8c0-6.5,5.3-11.8,11.8-11.8s11.8,5.3,11.8,11.8C428,291.7,422.7,297,416.3,297z') + circle + polygon(points='371.3,338.6 355.7,344.7 363.1,329.7') + #searchbox.row.justify-content-center + .col-12.col-lg-10.col-xl-8.col-xxl-7 + .input-group.input-group-lg + input#search-input.form-control(type='text' autocomplete='off' placeholder='Search for a package, author, topic, keyword, ...') + button#search-button.btn.btn-outline-secondary(type='button') Search + button.btn.btn-outline-secondary.dropdown-toggle.dropdown-toggle-split(type='button' data-bs-toggle='collapse' data-bs-target='#extra-search-fields') + #extra-search-fields.card.card-body.border-top-0.collapse.noanim.shadow(style='margin-right: 46px;') + #results-placeholder.py-4(style='max-width: 920px; margin: auto;') + p.summary-paragraph + | Currently serving + span.ps-1#summary-n-packages .. + a.ps-1(href='../builds') packages + | , + span.ps-1#summary-n-datasets .. + | datasets, and + span.ps-1#summary-n-articles .. + a.ps-1(href='../articles') articles + | by + span.ps-1#summary-n-maintainers .. + a.ps-1(href='../maintainers') maintainers + | and + span.ps-1#summary-n-contributors .. + | contributors. + | Not sure what to search for? Why not try: + span.ps-1#topics-list + .d-none.d-xl-block + br + br + #maintainers-carousel.carousel.slide.border-top.border-bottom.mt-4.text-center(data-bs-ride='carousel') + span.bg-white.position-relative.d-inline.p-2.text-muted(style='top: -1em;') Organizations + button.text-decoration-none.btn.btn-link.carousel-control.carousel-control-prev(type='button' data-bs-target='#maintainers-carousel' data-bs-slide='prev') + i.fas.fa-angle-double-left + button.text-decoration-none.btn.btn-link.carousel-control.carousel-control-next(type='button' data-bs-target='#maintainers-carousel' data-bs-slide='next') + i.fas.fa-angle-double-right + .carousel-inner + p(style='margin-top: 50px;') + | Want to learn more about r-universe? Have a look at + a.mx-1(href='https://ropensci.org/r-universe') ropensci.org/r-universe + | or updates from the rOpenSci blog: + ul.blog-posts + .container(style='max-width: 1440px') + #search-results-comment.text-secondary.search-results.col-12.mt-2 + .row#search-results(data-masonry='{"percentPosition": true }') + + #templatezone.d-none + .package-description-item.col-md-12.col-xl-6 + .card.mt-4.package-description-card + .card-body + .float-end.m-2(style="max-width: 140px;") + img.img.border.border-secondary-subtle.rounded.p-1.mx-1(src='https://r-universe.dev/static/nobody.jpg' style='max-height: 120px;') + p.pt-1.m-1.text-center.package-org.text-secondary + a.package-link.text-decoration-none.text-reset + h2.h5.pb-1 + span.package-name.text-dark + | : + small.ms-1.text-secondary.package-title + p.card-text.package-description.fst-italic + p.card-text.pt-2 + small.text-muted.description-maintainer + small.text-muted.description-last-updated + p.card-text.description-topics.d-none + span.me-1.text-muted.badge.badge-light.description-score.d-none.p-1.border + i.fas.fa-chart-bar.text-primary + span.me-1.text-muted.badge.badge-light.description-stars.d-none.p-1.border + i.fas.fa-star.text-warning + span.me-1.text-muted.badge.badge-light.description-pkgscore.p-1.border.d-none + i.fas.fa-chart-line.text-primary + span.me-1.text-muted.badge.badge-light.description-dependencies.p-1.border.d-none + i.fas.fa-cube.text-danger + span.me-1.text-muted.badge.badge-light.description-dependents.p-1.border.d-none + i.fas.fa-cubes.text-success + + .organization-item.col-xl-2.col-md-3.col-4.mb-2 + a.card.maintainer-card.border-0.text-decoration-none.p-2 + img.card-img-top.img-rounded + .card-body.px-0.py-1 + p.card-text.text-center.text-muted.text-truncate + .carousel-item(data-interval='5000') + .row.justify-content-center.maintainer-row + .form-group.row.search-field-item.mb-3 + label.col-sm-3.col-lg-2.col-form-label.col-form-label-sm + .col-sm-9.col-lg-10 + input.form-control.form-control-sm.search-item-input(type='search' autocomplete='off') + +block after_scripts + script + include search.js + + +block after_head + link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css') diff --git a/views/global/sysdeps.js b/views/global/sysdeps.js new file mode 100644 index 0000000..7105fbc --- /dev/null +++ b/views/global/sysdeps.js @@ -0,0 +1,13 @@ +$(function(){ + new DataTable('table', { + searching: true, + paging: false, + lengthChange: false, + info: false, + language: { search: "Filter: "}, + initComplete: function () { + $('div.dt-search').addClass("float-end") + $('div.dt-search input').removeClass('dt-input').addClass('m-2') + } + }); +}); diff --git a/views/global/sysdeps.pug b/views/global/sysdeps.pug new file mode 100644 index 0000000..4e7445d --- /dev/null +++ b/views/global/sysdeps.pug @@ -0,0 +1,36 @@ +extends global + +block content + #content-container.container-fluid + table.table + thead + tr.border-bottom.border-dark + th Library + th Description + th Debian (Headers) + th Used by R packages + tbody + each x in sysdeps + tr + th.text-nowrap #{x.library} + if x.homepage + a.ms-1(href=x.homepage target='_blank') + sup.fa-solid.fa-up-right-from-square + td.text-nowrap #{x.description} + td + each deb in x.headers + a.text-nowrap(href=`https://packages.ubuntu.com/${x.distro}/${deb}`) #{deb} + br + td + each pkg in x.usedby + a.text-nowrap.text-dark(href=`https://${pkg.owner}.r-universe.dev/${pkg.package}`) #{pkg.package} + | + +block after_scripts + script(src='https://cdn.datatables.net/2.1.3/js/dataTables.min.js') + script + include sysdeps.js + + +block after_head + link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css') diff --git a/views/global/updates.js b/views/global/updates.js new file mode 100644 index 0000000..e69de29 diff --git a/views/includes/default-scripts.pug b/views/includes/default-scripts.pug index 1fb26e6..da7c55d 100644 --- a/views/includes/default-scripts.pug +++ b/views/includes/default-scripts.pug @@ -1,4 +1,5 @@ script(src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.slim.min.js') script(src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/js/bootstrap.bundle.min.js') +script(src='https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.min.js') if node_env == 'production' script(src='https://r-universe.dev/matomo.js' async) diff --git a/views/search.pug b/views/search.pug deleted file mode 100644 index d38054b..0000000 --- a/views/search.pug +++ /dev/null @@ -1,108 +0,0 @@ -doctype html -html - head - meta(charset='utf-8') - meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no') - link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.2/css/bootstrap.min.css') - link(rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css') - link(rel='stylesheet' href='/layout.css') - link(rel='stylesheet' href='/search.css') - title R-universe - Discover and Publish R Packages - - body - .row - .col.p-4 - .container - .d-flex.justify-content-center - svg(viewbox='250 250 300 150' preserveaspectratio='xMinYMid slice') - g#rocket - path(d='M445.3,256.3c-47.3-3.5-62.8,27-62.8,27c-18.5,2-24,10-24,10l15.5,12.3c-0.5,15.3,3.5,16.5,3.5,16.5c5.5,7.5,17.8,4.8,17.8,4.8l12.8,17c12-12.5,10.3-25.5,10.3-25.5C450.8,300,445.3,256.3,445.3,256.3zM416.3,297c-6.5,0-11.8-5.3-11.8-11.8c0-6.5,5.3-11.8,11.8-11.8s11.8,5.3,11.8,11.8C428,291.7,422.7,297,416.3,297z') - circle - polygon(points='371.3,338.6 355.7,344.7 363.1,329.7') - #searchbox.row.justify-content-center - .col-12.col-lg-10.col-xl-8.col-xxl-7 - .input-group.input-group-lg - input#search-input.form-control(type='text' autocomplete='off' placeholder='Search for a package, author, topic, keyword, ...') - button#search-button.btn.btn-outline-secondary(type='button') Search - button.btn.btn-outline-secondary.dropdown-toggle.dropdown-toggle-split(type='button' data-bs-toggle='collapse' data-bs-target='#extra-search-fields') - #extra-search-fields.card.card-body.border-top-0.collapse.noanim.shadow(style='margin-right: 46px;') - #results-placeholder.py-4(style='max-width: 920px; margin: auto;') - p.summary-paragraph - | Currently serving - span.ps-1#summary-n-packages .. - a.ps-1(href='../builds') packages - | , - span.ps-1#summary-n-datasets .. - | datasets, and - span.ps-1#summary-n-articles .. - a.ps-1(href='../articles') articles - | by - span.ps-1#summary-n-maintainers .. - a.ps-1(href='../maintainers') maintainers - | and - span.ps-1#summary-n-contributors .. - | contributors. - | Not sure what to search for? Why not try: - span.ps-1#topics-list - .d-none.d-xl-block - br - br - #maintainers-carousel.carousel.slide.border-top.border-bottom.mt-4.text-center(data-bs-ride='carousel') - span.bg-white.position-relative.d-inline.p-2.text-muted(style='top: -1em;') Organizations - button.text-decoration-none.btn.btn-link.carousel-control.carousel-control-prev(type='button' data-bs-target='#maintainers-carousel' data-bs-slide='prev') - i.fas.fa-angle-double-left - button.text-decoration-none.btn.btn-link.carousel-control.carousel-control-next(type='button' data-bs-target='#maintainers-carousel' data-bs-slide='next') - i.fas.fa-angle-double-right - .carousel-inner - p(style='margin-top: 50px;') - | Want to learn more about r-universe? Have a look at - a.mx-1(href='https://ropensci.org/r-universe') ropensci.org/r-universe - | or updates from the rOpenSci blog: - ul.blog-posts - .container(style='max-width: 1440px') - #search-results-comment.text-secondary.search-results.col-12.mt-2 - .row#search-results(data-masonry='{"percentPosition": true }') - - #templatezone.d-none - .package-description-item.col-md-12.col-xl-6 - .card.mt-4.package-description-card - .card-body - .float-end.m-2(style="max-width: 140px;") - img.img.border.border-secondary-subtle.rounded.p-1.mx-1(src='https://r-universe.dev/static/nobody.jpg' style='max-height: 120px;') - p.pt-1.m-1.text-center.package-org.text-secondary - a.package-link.text-decoration-none.text-reset - h2.h5.pb-1 - span.package-name.text-dark - | : - small.ms-1.text-secondary.package-title - p.card-text.package-description.fst-italic - p.card-text.pt-2 - small.text-muted.description-maintainer - small.text-muted.description-last-updated - p.card-text.description-topics.d-none - span.me-1.text-muted.badge.badge-light.description-score.d-none.p-1.border - i.fas.fa-chart-bar.text-primary - span.me-1.text-muted.badge.badge-light.description-stars.d-none.p-1.border - i.fas.fa-star.text-warning - span.me-1.text-muted.badge.badge-light.description-pkgscore.p-1.border.d-none - i.fas.fa-chart-line.text-primary - span.me-1.text-muted.badge.badge-light.description-dependencies.p-1.border.d-none - i.fas.fa-cube.text-danger - span.me-1.text-muted.badge.badge-light.description-dependents.p-1.border.d-none - i.fas.fa-cubes.text-success - - .organization-item.col-xl-2.col-md-3.col-4.mb-2 - a.card.maintainer-card.border-0.text-decoration-none.p-2 - img.card-img-top.img-rounded - .card-body.px-0.py-1 - p.card-text.text-center.text-muted.text-truncate - .carousel-item(data-interval='5000') - .row.justify-content-center.maintainer-row - .form-group.row.search-field-item.mb-3 - label.col-sm-3.col-lg-2.col-form-label.col-form-label-sm - .col-sm-9.col-lg-10 - input.form-control.form-control-sm.search-item-input(type='search' autocomplete='off') - - include includes/default-scripts.pug - script(src='https://cdnjs.cloudflare.com/ajax/libs/masonry/4.2.2/masonry.pkgd.min.js') - script(src='/search.js')