├── app ├── pages │ └── contribute.html ├── sass │ ├── pages │ │ ├── pages.sass │ │ ├── search.sass │ │ ├── activities.sass │ │ ├── owner.sass │ │ ├── categories.sass │ │ ├── home.sass │ │ └── release.sass │ ├── components │ │ ├── breadcrumbs.sass │ │ ├── label.sass │ │ ├── with-image.sass │ │ ├── site-footer.sass │ │ ├── tabs.sass │ │ ├── shard-info.sass │ │ ├── version-info.sass │ │ ├── shard-card.sass │ │ ├── metrics.sass │ │ └── site-header.sass │ ├── _vars.scss │ ├── main.sass │ ├── _typography.sass │ └── _fonts.css └── views │ ├── macros │ ├── icons.j2 │ ├── date.j2 │ ├── links.j2 │ └── shard-card.j2 │ ├── releases │ ├── show.html.j2 │ ├── releases.html.j2 │ ├── activity.html.j2 │ ├── _infobox.html.j2 │ ├── _layout.html.j2 │ ├── dependencies.html.j2 │ ├── _dependencies.html.j2 │ └── _header.html.j2 │ ├── search.html.j2 │ ├── categories │ ├── index.html.j2 │ └── show.html.j2 │ ├── includes │ ├── footer.html.j2 │ └── icons.svg │ ├── layout.html.j2 │ ├── owners │ ├── index.html.j2 │ └── show.html.j2 │ ├── pages │ ├── contribute.html.j2 │ └── imprint.html.j2 │ ├── stats.html.j2 │ └── home.html.j2 ├── CHECKS ├── .gitignore ├── public ├── assets │ └── fonts │ │ ├── roboto-v20-latin-300.eot │ │ ├── roboto-v20-latin-300.ttf │ │ ├── roboto-v20-latin-300.woff │ │ ├── roboto-v20-latin-500.eot │ │ ├── roboto-v20-latin-500.ttf │ │ ├── roboto-v20-latin-500.woff │ │ ├── roboto-v20-latin-700.eot │ │ ├── roboto-v20-latin-700.ttf │ │ ├── roboto-v20-latin-700.woff │ │ ├── roboto-mono-v7-latin-100.eot │ │ ├── roboto-mono-v7-latin-100.ttf │ │ ├── roboto-mono-v7-latin-300.eot │ │ ├── roboto-mono-v7-latin-300.ttf │ │ ├── roboto-mono-v7-latin-500.eot │ │ ├── roboto-mono-v7-latin-500.ttf │ │ ├── roboto-v20-latin-300.woff2 │ │ ├── roboto-v20-latin-500.woff2 │ │ ├── roboto-v20-latin-700.woff2 │ │ ├── roboto-v20-latin-italic.eot │ │ ├── roboto-v20-latin-italic.ttf │ │ ├── roboto-v20-latin-italic.woff │ │ ├── roboto-v20-latin-regular.eot │ │ ├── roboto-v20-latin-regular.ttf │ │ ├── roboto-mono-v7-latin-100.woff │ │ ├── roboto-mono-v7-latin-100.woff2 │ │ ├── roboto-mono-v7-latin-300.woff │ │ ├── roboto-mono-v7-latin-300.woff2 │ │ ├── roboto-mono-v7-latin-500.woff │ │ ├── roboto-mono-v7-latin-500.woff2 │ │ ├── roboto-v20-latin-300italic.eot │ │ ├── roboto-v20-latin-300italic.ttf │ │ ├── roboto-v20-latin-500italic.eot │ │ ├── roboto-v20-latin-500italic.ttf │ │ ├── roboto-v20-latin-700italic.eot │ │ ├── roboto-v20-latin-700italic.ttf │ │ ├── roboto-v20-latin-italic.woff2 │ │ ├── roboto-v20-latin-regular.woff │ │ ├── roboto-v20-latin-regular.woff2 │ │ ├── roboto-mono-v7-latin-regular.eot │ │ ├── roboto-mono-v7-latin-regular.ttf │ │ ├── roboto-mono-v7-latin-regular.woff │ │ ├── roboto-v20-latin-300italic.woff │ │ ├── roboto-v20-latin-300italic.woff2 │ │ ├── roboto-v20-latin-500italic.woff │ │ ├── roboto-v20-latin-500italic.woff2 │ │ ├── roboto-v20-latin-700italic.woff │ │ ├── roboto-v20-latin-700italic.woff2 │ │ └── roboto-mono-v7-latin-regular.woff2 └── prism.css ├── src ├── cli.cr ├── assets.cr ├── page │ ├── owner.cr │ ├── category.cr │ └── shard.cr ├── page.cr ├── raven.cr ├── crinja_lib.cr ├── crinja_models.cr ├── api.cr ├── app.cr └── db.cr ├── .travis ├── integration-spec.sh └── deploy.sh ├── Procfile ├── shard.yml ├── docker-compose.yml ├── LICENSE ├── Dockerfile ├── .travis.yml ├── shard.lock └── Makefile /app/pages/contribute.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contribute 3 | --- 4 | -------------------------------------------------------------------------------- /app/sass/pages/pages.sass: -------------------------------------------------------------------------------- 1 | .containers.page 2 | max-width: 60rem 3 | -------------------------------------------------------------------------------- /CHECKS: -------------------------------------------------------------------------------- 1 | WAIT=1 2 | ATTEMPTS=3 3 | 4 | /deploy_status OK 5 | /shards/crinja crinja 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /docs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | *.dwarf 6 | .env 7 | /catalog/* 8 | /public/assets/css/style.css 9 | -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-300.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-300.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-300.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-500.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-500.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-500.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-700.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-700.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-700.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-100.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-100.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-100.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-100.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-300.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-300.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-500.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-500.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-300.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-500.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-700.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-italic.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-italic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-italic.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-regular.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-regular.ttf -------------------------------------------------------------------------------- /src/cli.cr: -------------------------------------------------------------------------------- 1 | require "./app" 2 | 3 | case command = ARGV[0]? 4 | when "assets:precompile" 5 | assets_precompile 6 | else 7 | Kemal.run 8 | end 9 | -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-100.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-100.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-100.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-300.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-300.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-500.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-500.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-500.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-300italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-300italic.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-300italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-300italic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-500italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-500italic.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-500italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-500italic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-700italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-700italic.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-700italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-700italic.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-italic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-regular.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-regular.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-regular.eot -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-regular.ttf -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-regular.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-300italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-300italic.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-300italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-300italic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-500italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-500italic.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-500italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-500italic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-700italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-700italic.woff -------------------------------------------------------------------------------- /public/assets/fonts/roboto-v20-latin-700italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-v20-latin-700italic.woff2 -------------------------------------------------------------------------------- /public/assets/fonts/roboto-mono-v7-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shardbox/shardbox-web/HEAD/public/assets/fonts/roboto-mono-v7-latin-regular.woff2 -------------------------------------------------------------------------------- /app/views/macros/icons.j2: -------------------------------------------------------------------------------- 1 | {% macro icon(name) %} 2 | 3 | 4 | 5 | {% endmacro %} 6 | -------------------------------------------------------------------------------- /.travis/integration-spec.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -e 3 | 4 | make bin/app 5 | export DATABASE_URL=$TEST_DATABASE_URL 6 | bin/app & 7 | sleep 1 8 | curl http://localhost:3000/ -v > /dev/null 9 | -------------------------------------------------------------------------------- /app/sass/components/breadcrumbs.sass: -------------------------------------------------------------------------------- 1 | .breadcrumbs 2 | .breadcrumb 3 | text-decoration: none 4 | color: var(--color-lighter) 5 | text-transform: uppercase 6 | 7 | ::after 8 | content: " ›" 9 | -------------------------------------------------------------------------------- /.travis/deploy.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | set -e 3 | echo -e "$SSH_PRIVATE_KEY" > private_ssh_key 4 | chmod 0700 private_ssh_key 5 | 6 | GIT_SSH_COMMAND='ssh -i private_ssh_key' git push ssh://dokku@shardbox.org/shardbox master 7 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: env SENTRY_DSN_VAR=SENTRY_DSN_WEB KEMAL_ENV=production bin/app --port $PORT 2 | worker: env SENTRY_DSN_VAR=SENTRY_DSN_WORKER bin/worker loop 3 | release: dbmate --no-dump-schema --migrations-dir lib/shardbox-core/db/migrations up 4 | -------------------------------------------------------------------------------- /app/sass/components/label.sass: -------------------------------------------------------------------------------- 1 | .label 2 | color: #333 3 | padding: .1em .2em 4 | border-radius: .3em 5 | font-size: 90% 6 | font-weight: 500 7 | &__archived 8 | background-color: var(--color-archived) 9 | &__sync_failed 10 | background-color: var(--color-sync-failed) 11 | -------------------------------------------------------------------------------- /app/sass/pages/search.sass: -------------------------------------------------------------------------------- 1 | 2 | .search-results 3 | > .shard-card 4 | display: grid 5 | grid-template-columns: 1fr min-content 6 | grid-template-areas: "categories repo-info" "name repo-info" "description description" "shard-info shard-info" 7 | & + & 8 | margin-top: 1rem 9 | -------------------------------------------------------------------------------- /app/sass/pages/activities.sass: -------------------------------------------------------------------------------- 1 | .activity-set 2 | margin: .5em 0 3 | 4 | + .activity-set::before 5 | content: "" 6 | width: 4em 7 | border-top: 3px dotted #ccc 8 | display: block 9 | margin-bottom: .5em 10 | 11 | .activity:not(:first-child) 12 | margin-left: 2rem 13 | color: #555 14 | -------------------------------------------------------------------------------- /app/views/macros/date.j2: -------------------------------------------------------------------------------- 1 | {% macro release_date(released_at) -%} 2 | {{ time_date(released_at, title="Released on {{date}}") }} 3 | {%- endmacro %} 4 | 5 | {% macro time_date(date) -%} 6 | {%- endmacro %} 8 | -------------------------------------------------------------------------------- /app/sass/_vars.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-body: #222; 3 | --color-link: #20b; 4 | --color-link-hover: #30a; 5 | --color-light: #555; 6 | --color-light-link: #888; 7 | --color-light-hover: #444; 8 | --color-lighter: #888; 9 | --color-archived: #ddccbb; 10 | --color-sync-failed: #ffeecc; 11 | --color-code-background: #fdf6e3; 12 | --color-code: #657b83; 13 | --color-latest: hsl(116, 100%, 33%); 14 | --color-yanked: hsl(46, 100%, 33%); 15 | } 16 | -------------------------------------------------------------------------------- /app/sass/components/with-image.sass: -------------------------------------------------------------------------------- 1 | .with-image 2 | display: grid 3 | grid-template-columns: max-content max-content 4 | 5 | > img 6 | grid-row: 1/3 7 | grid-column: 1 8 | align-self: center 9 | margin-right: .6em 10 | border-radius: 50% 11 | width: 2em 12 | 13 | > :not(img) 14 | grid-column: 2 15 | line-height: 1.3 16 | align-self: flex-end 17 | margin: 0 18 | 19 | + :not(img) 20 | align-self: flex-start 21 | -------------------------------------------------------------------------------- /app/sass/components/site-footer.sass: -------------------------------------------------------------------------------- 1 | .site-footer 2 | display: flex 3 | background: #f3f3f3 4 | padding: 1rem 3rem 5 | border-top: 2px solid #ccc 6 | 7 | > * 8 | flex-basis: 33% 9 | flex-grow: 1 10 | margin: 1rem 11 | 12 | > h3 13 | font-weight: 300 14 | font-size: 1rem 15 | 16 | .links 17 | display: flex 18 | flex-direction: column 19 | align-items: start 20 | --color-link: var(--color-light-link) 21 | --color-link-hover: var(--color-light-hover) 22 | 23 | > a 24 | margin: .1em 0 25 | -------------------------------------------------------------------------------- /app/sass/components/tabs.sass: -------------------------------------------------------------------------------- 1 | .tabs 2 | display: flex 3 | flex-direction: row 4 | 5 | > a 6 | padding: .4em .8em .2em 7 | color: var(--color-body) 8 | background: #eee 9 | margin-right: 0px 10 | border-top: 4px solid #eee 11 | border-bottom: 1px solid #ddd 12 | font-size: 110% 13 | 14 | + a 15 | border-left: 1px solid #ddd 16 | 17 | &.current 18 | font-weight: bold 19 | background: white 20 | border-top-color: #ddd 21 | border-bottom-color: transparent 22 | 23 | &:hover, &:focus 24 | border-top-color: #ddd 25 | -------------------------------------------------------------------------------- /app/sass/pages/owner.sass: -------------------------------------------------------------------------------- 1 | .owner-title.with-image 2 | margin-top: 1rem 3 | margin-bottom: 2rem 4 | > img 5 | width: 10em 6 | margin-right: 1em 7 | 8 | .metrics--owner 9 | background-color: #fff 10 | border-radius: .75em 11 | max-width: 60em 12 | padding: 1em 13 | margin: auto 14 | 15 | // owners index 16 | .owner-item > .metrics 17 | padding: 0 18 | 19 | .owner-item 20 | @extend .category-item 21 | padding: .5em 22 | 23 | > .owner 24 | margin: .3em .5em .6em 25 | 26 | .owner_slug 27 | font-size: 90% 28 | color: var(--color-light) 29 | letter-spacing: .02em 30 | -------------------------------------------------------------------------------- /app/sass/components/shard-info.sass: -------------------------------------------------------------------------------- 1 | .shard-infos, .shard-info 2 | color: var(--color-light) 3 | font-weight: 300 4 | .shard-info 5 | margin: 0 6 | 7 | > * + * 8 | margin-left: .6em 9 | &::before 10 | content: "• " 11 | margin-left: -.5em 12 | position: absolute 13 | &--release > * + * 14 | &::before 15 | line-height: 1.7 16 | 17 | &--release 18 | .datapoint--version 19 | font-size: 1.5rem 20 | 21 | &--list 22 | .datapoint--dependencies 23 | width: 100% 24 | 25 | + .datapoint 26 | margin-left: 0 27 | &::before 28 | display: none 29 | -------------------------------------------------------------------------------- /app/sass/main.sass: -------------------------------------------------------------------------------- 1 | @import "vars" 2 | @import "./fonts" 3 | @import "typography" 4 | @import "components/breadcrumbs" 5 | @import "components/label" 6 | @import "components/metrics" 7 | @import "components/shard-card" 8 | @import "components/shard-info" 9 | @import "components/site-header" 10 | @import "components/site-footer" 11 | @import "components/tabs" 12 | @import "components/version-info" 13 | @import "components/with-image" 14 | @import "pages/activities" 15 | @import "pages/categories" 16 | @import "pages/home" 17 | @import "pages/owner" 18 | @import "pages/pages" 19 | @import "pages/release" 20 | @import "pages/search" 21 | -------------------------------------------------------------------------------- /src/assets.cr: -------------------------------------------------------------------------------- 1 | require "sass" 2 | 3 | PUBLIC_PATH = Path.posix("public") 4 | CSS_PATH = Path.posix("/", "assets", "css") 5 | STYLE_PATH = CSS_PATH.join("style.css") 6 | 7 | unless File.readable?(PUBLIC_PATH.join(STYLE_PATH)) 8 | get STYLE_PATH.to_s do |context| 9 | context.response.headers["Content-Type"] = "text/css" 10 | compile_sass 11 | end 12 | end 13 | 14 | def compile_sass 15 | Sass.compile_file("app/sass/main.sass", is_indented_syntax_src: true, include_path: "app/sass/") 16 | end 17 | 18 | def assets_precompile 19 | Dir.mkdir_p(PUBLIC_PATH.join(CSS_PATH)) 20 | File.write(PUBLIC_PATH.join(STYLE_PATH), compile_sass) 21 | end 22 | -------------------------------------------------------------------------------- /app/sass/components/version-info.sass: -------------------------------------------------------------------------------- 1 | .version-info 2 | display: flex 3 | align-items: center 4 | 5 | .shard-card & 6 | align-items: baseline 7 | 8 | > .icon 9 | font-size: 80% 10 | align-self: center 11 | margin-top: 0 12 | 13 | > .version 14 | margin-left: .2rem 15 | margin-right: .3rem 16 | 17 | > .badge--latest, 18 | > .badge--yanked 19 | @extend .badge 20 | margin-left: .2rem 21 | margin-right: .2rem 22 | font-size: 70% 23 | 24 | > .badge--latest 25 | color: var(--color-latest) 26 | > .badge--yanked 27 | color: var(--color-yanked) 28 | 29 | > .released_at, 30 | > .created_at 31 | margin-left: .3rem 32 | font-size: 80% 33 | -------------------------------------------------------------------------------- /app/views/releases/show.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "releases/_layout.html.j2" %} 2 | 3 | {% block page_title %}{{ shard.display_name }}@{{ release.version }} on Shardbox{% endblock %} 4 | 5 | {% block release_main %} 6 | {% if readme %} 7 |
8 | {{ readme | markdown_repo_content(repo_ref = repo.ref, revision = release.revision_identifier) }} 9 |
10 | {% else %} 11 |
12 |

You can add this shard as a dependency by adding the following lines 13 | to the dependencies section in your shard.yml:

14 |
{{ shard.name }}:
15 |   {{ repo.ref.resolver }}: {{ repo.ref.url }}
16 |   version: ~> {{ release.version }}
17 |
18 | {% endif %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /app/views/search.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/date.j2" %} 3 | {% import "macros/links.j2" %} 4 | {% import "macros/shard-card.j2" %} 5 | 6 | {% block page_title %}"{{ query }}" Search on Shardbox{% endblock %} 7 | {% block main %} 8 |
9 |

Search for "{{ query }}"

10 |
11 | 12 |
13 | {% for result in shards %} 14 | {{ shard_card(result.shard, result, result.categories, 15 | show_description = true, 16 | repo_ref = result.repo.ref 17 | ) }} 18 | {% else %} 19 |

There were no results for your search ;-(

20 |

Maybe you want to browse the categories instead?

21 | {% endfor %} 22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: shardbox-web 2 | version: 0.1.0 3 | 4 | authors: 5 | - Johannes Müller 6 | 7 | targets: 8 | app: 9 | main: src/cli.cr 10 | worker: 11 | main: lib/shardbox-core/src/worker.cr 12 | 13 | crystal: ">= 1.0" 14 | 15 | license: MIT 16 | 17 | dependencies: 18 | shardbox-core: 19 | github: shardbox/shardbox-core 20 | humanize_time: 21 | github: mamantoha/humanize_time 22 | version: 0.7.0 23 | kemal: 24 | github: kemalcr/kemal 25 | crinja: 26 | github: straight-shoota/crinja 27 | version: 0.8.0 28 | baked_file_system: 29 | github: schovi/baked_file_system 30 | version: 0.10.0 31 | sass: 32 | github: straight-shoota/sass.cr 33 | markd: 34 | github: icyleaf/markd 35 | sanitize: 36 | github: straight-shoota/sanitize 37 | -------------------------------------------------------------------------------- /app/sass/components/shard-card.sass: -------------------------------------------------------------------------------- 1 | .shard-card 2 | //flex-basis: 13em 3 | padding: .5em .8em 4 | background: #fff 5 | border-top: 4px solid #ddd 6 | 7 | > * + * 8 | margin-top: .4em 9 | 10 | > .shard-name 11 | font-size: 1.35em 12 | grid-area: name 13 | margin-top: .3em 14 | 15 | > .shard-infos 16 | grid-area: shard-info 17 | display: flex 18 | flex-flow: column 19 | 20 | > .description 21 | color: var(--color-light) 22 | font-size: .95em 23 | flex-grow: 1 24 | grid-area: description 25 | 26 | > .repo-info 27 | grid-area: repo-info 28 | 29 | > .categories 30 | grid-area: categories 31 | 32 | > .category-link 33 | font-weight: 400 34 | 35 | > .metrics 36 | grid-area: metrics 37 | padding: 0 38 | font-size: 95% 39 | line-height: 1.3 40 | margin-top: 0 41 | -------------------------------------------------------------------------------- /app/sass/components/metrics.sass: -------------------------------------------------------------------------------- 1 | .metrics 2 | display: grid 3 | grid-template-rows: minmax(1.5em, auto) minmax(1.5em, auto) 4 | grid-template-columns: auto 5 | padding: .5em 6 | 7 | > .metric 8 | display: contents 9 | text-align: center 10 | 11 | > .metric-label 12 | grid-row: 1 13 | font-size: 70% 14 | color: var(--color-lighter) 15 | font-weight: 100 16 | 17 | > .metric-value 18 | grid-row: 2 19 | font-weight: 400 20 | color: var(--color-light) 21 | font-size: 110% 22 | white-space: nowrap 23 | 24 | > time 25 | font-size: 90% 26 | 27 | > .metric-label, > .metric-value 28 | padding: 0.2em .4em 29 | align-self: stretch 30 | vertical-align: middle 31 | 32 | + .metric 33 | > .metric-label, > .metric-value 34 | border-left: 1px solid #eee 35 | -------------------------------------------------------------------------------- /src/page/owner.cr: -------------------------------------------------------------------------------- 1 | require "shardbox-core/repo/owner" 2 | 3 | @[Crinja::Attributes] 4 | struct Page::Owner 5 | include Page 6 | 7 | def self.new(db, context) 8 | owner = find_owner(db, context) 9 | return owner if owner.nil? || owner.is_a?(String) 10 | 11 | new db, owner 12 | end 13 | 14 | def initialize(@db : ShardsDB, @owner : Repo::Owner) 15 | end 16 | 17 | private def initialize_context(context) 18 | context["owner"] = @owner 19 | context["shards"] = @db.shards_owned_by(@owner.id) 20 | context["metrics"] = @db.get_owner_metrics(@owner.id) 21 | end 22 | 23 | def render(io) 24 | render(io, "owners/show.html.j2") 25 | end 26 | 27 | def self.find_owner(db, context) 28 | resolver = context.params.url["resolver"] 29 | slug = context.params.url["slug"] 30 | 31 | db.get_owner?(resolver, slug) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | postgres: 4 | image: postgres:12 5 | environment: 6 | POSTGRES_USER: shardbox 7 | POSTGRES_PASSWORD: shardbox 8 | POSTGRES_DB: shardbox_dev 9 | volumes: 10 | - "postgres-data:/var/lib/postgresql/data" 11 | restart: unless-stopped 12 | 13 | web: 14 | build: 15 | context: . 16 | dockerfile: local.dockerfile 17 | depends_on: 18 | - postgres 19 | environment: 20 | KEMAL_ENV: development 21 | DATABASE_URL: postgres://shardbox:shardbox@postgres/shardbox_dev 22 | command: bin/app 23 | ports: 24 | - 3000 25 | 26 | worker: 27 | build: 28 | context: . 29 | dockerfile: local.dockerfile 30 | depends_on: 31 | - postgres 32 | environment: 33 | DATABASE_URL: postgres://shardbox:shardbox@postgres/shardbox_dev 34 | GITHUB_TOKEN: "${GITHUB_TOKEN}" 35 | command: bin/worker sync_repos 36 | 37 | volumes: 38 | postgres-data: 39 | -------------------------------------------------------------------------------- /app/views/categories/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/links.j2" %} 3 | 4 | {% block page_title %}Categories on Shardbox{% endblock %} 5 | {% block main %} 6 |
7 |

Categories

8 |
9 | 10 |
11 | {% for item in categories %} 12 |
13 | 14 | {{ item.name }} 15 | {{ item.entries_count }} 16 | 17 | 18 |
19 | {# TODO: For some reason this accessor doesn't work: top_shards[item.id] #} 20 | {% for shard in top_shards[item.id] %} 21 | 22 | 23 | {{- shard.display_name -}} 24 | 25 | 26 | {% endfor %} 27 |
28 |
29 | {% endfor %} 30 |
31 | {% endblock %} 32 | -------------------------------------------------------------------------------- /app/views/includes/footer.html.j2: -------------------------------------------------------------------------------- 1 | 24 | -------------------------------------------------------------------------------- /src/page/category.cr: -------------------------------------------------------------------------------- 1 | @[Crinja::Attributes] 2 | struct Page::Category 3 | include Page 4 | 5 | getter db : ShardsDB 6 | getter category : ::Category 7 | getter entries : Array(ShardsDB::CategoryResult) 8 | getter? uncategorized : Bool 9 | 10 | def initialize(@db, @category) 11 | @uncategorized = category.slug == "Uncategorized" 12 | @entries = @db.shards_in_category_with_releases(uncategorized? ? nil : category.id) 13 | end 14 | 15 | private def initialize_context(context) 16 | context["category"] = category 17 | context["shards"] = entries 18 | 19 | if uncategorized? 20 | initialize_context_uncategorized(context) 21 | else 22 | context["entries_count"] = category.entries_count 23 | end 24 | end 25 | 26 | private def initialize_context_uncategorized(context) 27 | context["entries_count"] = @db.uncategorized_count 28 | context["homonymous_shards"] = homonymous_shards 29 | end 30 | 31 | private def homonymous_shards 32 | db.find_homonymous_shards(entries.map(&.shard.name)) 33 | end 34 | 35 | def render(io) 36 | render(io, "categories/show.html.j2") 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-2019 Johannes Müller 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/views/releases/releases.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "releases/_layout.html.j2" %} 2 | 3 | {% block page_title %}{{ shard.display_name }}@{{ release.version }} on Shardbox{% endblock %} 4 | 5 | {% block release_main %} 6 |
7 |

Releases

8 | 28 |
29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /src/page.cr: -------------------------------------------------------------------------------- 1 | module Page 2 | class_getter crinja : Crinja = initialize_crinja 3 | 4 | def self.initialize_crinja 5 | crinja = Crinja.new 6 | crinja.loader = Crinja::Loader::FileSystemLoader.new("app/views/") 7 | 8 | crinja.filters["humanize_time_span"] = Crinja.filter({now: Crinja::UNDEFINED}) do 9 | time = target.as_time 10 | now = arguments["now"] 11 | now = now.undefined? ? Time.utc : now.as_time 12 | if time <= now 13 | formatted = HumanizeTime.distance_of_time_in_words(time, now) 14 | "#{formatted} ago" 15 | else 16 | formatted = HumanizeTime.distance_of_time_in_words(now, time) 17 | "in #{formatted}" 18 | end 19 | end 20 | 21 | crinja 22 | end 23 | 24 | macro included 25 | include Crinja::Object::Auto 26 | end 27 | 28 | getter context : Crinja::Context do 29 | Crinja::Context.new(crinja.context).tap do |context| 30 | context["page"] = self 31 | initialize_context(context) 32 | end 33 | end 34 | 35 | private def initialize_context(context) 36 | end 37 | 38 | def render(io : IO, template : String) 39 | render(io, crinja.get_template(template)) 40 | end 41 | 42 | def render(io : IO, template) 43 | template.render(io, context) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/views/layout.html.j2: -------------------------------------------------------------------------------- 1 | {% import "macros/icons.j2" %} 2 | 3 | 4 | 5 | {% block page_title %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 25 |
26 | {% block main %} 27 | {% endblock %} 28 |
29 | 30 | {% include "includes/footer.html.j2" %} 31 | {% include "includes/icons.svg" %} 32 | 33 | 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM crystallang/crystal:1.5.0-alpine AS builder 2 | 3 | RUN apk add --no-cache --update-cache \ 4 | libgit2-dev libsass-dev libssh2-static yaml-static 5 | 6 | WORKDIR /src 7 | ADD shard.yml shard.lock ./ 8 | RUN shards install --production 9 | 10 | ADD . ./ 11 | 12 | # TODO: Can't get static linking with libsass and libgit2 with openssl, so we're manually 13 | # specifying the static libraries available 14 | #RUN shards build \ 15 | # --production \ 16 | RUN mkdir -p bin && shards build app worker \ 17 | --release \ 18 | --progress \ 19 | --link-flags='/usr/lib/libyaml.a /usr/lib/libpcre.a /usr/lib/libm.a' \ 20 | --link-flags='/usr/lib/libpthread.a /usr/lib/libevent.a /usr/lib/librt.a /usr/lib/libxml2.a /usr/lib/liblzma.a' 21 | 22 | RUN bin/app assets:precompile 23 | 24 | FROM alpine:3.12 AS runtime 25 | RUN apk add --no-cache --update-cache \ 26 | # bash needed for dokku enter 27 | bash \ 28 | # executables needed at runtime 29 | git openssh \ 30 | # Couldn't get libsass and libgit2 with openssl to link statically, so they're 31 | # needed as runtime dependencies 32 | libsass libgit2 \ 33 | gc 34 | 35 | RUN wget -qO /usr/local/bin/dbmate https://github.com/amacneil/dbmate/releases/download/v1.7.0/dbmate-linux-musl-amd64 \ 36 | && chmod +x /usr/local/bin/dbmate 37 | 38 | WORKDIR /app 39 | COPY --from=builder /src /app 40 | -------------------------------------------------------------------------------- /app/views/macros/links.j2: -------------------------------------------------------------------------------- 1 | {% macro shard_path(shard, release = none) -%}/shards/{{ shard.slug | default(shard) }}{% if release %}/releases/{{ release }}{% endif %}{%- endmacro %} 2 | {% macro release_path(shard, release) -%}{{ shard_path(shard, release.version | default(release) ) }}{%- endmacro %} 3 | 4 | {% macro repo_link(repo_ref, owner=none) %} 5 | 6 | {% if owner and owner.avatar_url %} 7 | 8 | {% else %} 9 | {{ icon(repo_ref.resolver) }} 10 | {% endif %} 11 | {{ repo_ref.nice_url }}{% endmacro %} 12 | 13 | {% macro shard_name(shard) %} 14 | {{ shard.name }}{% if shard.qualifier != "" %}~{{ shard.qualifier }}{% endif %} 15 | {% endmacro %} 16 | 17 | {% macro owner_path(owner) %}/owners/{{ owner.resolver }}/{{ owner.slug }}{% endmacro %} 18 | {% macro owner_ext_url(owner) %}/owners/{{ owner.resolver }}/{{ owner.slug }}{% endmacro %} 19 | 20 | {% macro owner_image(owner) %} 21 | 22 | {% endmacro %} 23 | 24 | {% macro nice_url(url) %}{{ url | replace("https://", "") | replace("http://", "") }}{% endmacro %} 25 | -------------------------------------------------------------------------------- /app/sass/pages/categories.sass: -------------------------------------------------------------------------------- 1 | .categories-list 2 | margin: 0 -1em 3 | padding: 0 4 | display: flex 5 | flex-wrap: wrap 6 | 7 | .category-item 8 | flex-basis: 20em 9 | border: 1px solid #ddd 10 | background-color: #fff 11 | padding: 1em 12 | margin: 1em 13 | flex-grow: 1 14 | 15 | > .category-title 16 | font-weight: 500 17 | font-size: 1.1em 18 | display: flex 19 | margin-bottom: .5rem 20 | justify-content: space-between 21 | 22 | .top-shard-link 23 | & + .top-shard-link 24 | &::before 25 | content: "•" 26 | margin-left: .2em 27 | color: var(--color-lighter) 28 | 29 | > a 30 | color: var(--color-light) 31 | font-weight: 400 32 | 33 | .category-edit 34 | margin-top: 3rem 35 | 36 | .category-entries 37 | display: flex 38 | flex-wrap: wrap 39 | 40 | > .category-entry 41 | margin-bottom: .5rem 42 | 43 | @media(min-width: 1200px) 44 | width: 50% 45 | padding: .6rem 46 | @media(max-width: 1200px) 47 | width: 100% 48 | margin-bottom: .6rem 49 | 50 | > .shard-card 51 | display: grid 52 | grid-template-columns: 1fr min-content 53 | grid-template-areas: "name repo-info" "description description" "shard-info shard-info" "metrics metrics" 54 | 55 | > .metrics 56 | margin-top: .5rem 57 | 58 | > .shards-spec 59 | 60 | > .homonymous_shards 61 | padding: .5em 62 | 63 | > small 64 | color: var(--color-lighter) 65 | 66 | > .shard-name + .shard-name::before 67 | content: "•" 68 | color: var(--color-lighter) 69 | -------------------------------------------------------------------------------- /app/views/owners/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/links.j2" %} 3 | 4 | {% block page_title %}Owners on Shardbox{% endblock %} 5 | {% block main %} 6 |
7 |

{{ icon("people") }} Owners

8 |
9 | 10 |
11 | {% for item in owners %} 12 |
13 |
14 | {{ owner_image(item.owner) }} 15 | {{ item.owner.name | default(item.owner.slug) }} 16 | @{{ item.owner.slug }} 17 |
18 | {% if item.metrics %} 19 |
20 | 21 | shards 22 | {{ icon("package") }} {{ item.metrics.shards_count }} 23 | 24 | 25 | direct dependents 26 | {{ icon("package-dependents") }} {{ item.metrics.dependents_count}} 27 | 28 | 29 | total dependents 30 | {{ icon("package-dependents") }} {{ icon("plus") }} {{ item.metrics.transitive_dependents_count }} 31 | 32 |
33 | {% endif %} 34 |
35 | {% endfor %} 36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | language: crystal 3 | 4 | build_addons: &build_addons 5 | postgresql: "12" 6 | apt: 7 | packages: 8 | - postgresql-12 9 | - postgresql-client-12 10 | - libgit2-dev 11 | - libsass-dev 12 | 13 | crystal: 14 | - latest 15 | 16 | cache: 17 | - shards 18 | - directories: 19 | - vendor/ 20 | 21 | env: 22 | global: 23 | - PGPORT: 5433 24 | - TEST_DATABASE_URL: "postgres://postgres:@localhost:5433/shardbox" 25 | - DBMATE: "vendor/bin/dbmate" 26 | - SHARDS_OPTS: "--ignore-crystal-version" 27 | 28 | jobs: 29 | include: 30 | - stage: test 31 | name: unit tests 32 | services: 33 | - postgresql 34 | addons: *build_addons 35 | before_script: 36 | - ./lib/shardbox-core/.travis/setup-database.sh 37 | - make -C ./lib/shardbox-core test_db 38 | script: make test 39 | 40 | - stage: test 41 | name: crystal format 42 | install: skip 43 | script: crystal tool format src spec --check 44 | 45 | - stage: test 46 | name: integration test 47 | services: 48 | - postgresql 49 | addons: *build_addons 50 | before_script: 51 | - ./lib/shardbox-core/.travis/setup-database.sh 52 | - make -C ./lib/shardbox-core test_db 53 | script: ./.travis/integration-spec.sh 54 | 55 | - stage: test 56 | name: build docker container 57 | services: 58 | - docker 59 | script: docker build . 60 | 61 | - stage: deploy 62 | name: dokku 63 | language: generic 64 | addons: 65 | ssh_known_hosts: shardbox.org 66 | deploy: 67 | provider: script 68 | script: .travis/deploy.sh 69 | on: 70 | branch: master 71 | if: branch = master 72 | -------------------------------------------------------------------------------- /app/views/releases/activity.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "releases/_layout.html.j2" %} 2 | 3 | {% block page_title %}{{ shard.display_name }}@{{ release.version }} activity on Shardbox{% endblock %} 4 | 5 | {% block release_main %} 6 |
7 |

Activity

8 | {% for activity_set in activities %} 9 |
10 | {% for activity in activity_set %} 11 | {% if not loop.first and activity.event == "sync_release:created" %} 12 |
13 |
14 | {% endif %} 15 |
16 | {% if activity.event == "sync_dependencies:created" %} 17 | {% if activity.scope == "development" %}development {% endif %}dependency: 18 | 19 | {{ activity.metadata.name }} 20 | {{ activity.metadata.release }} 21 | 22 | {% elif activity.event == "sync_release:created" %} 23 | {{ icon("star") }} New release {{ icon("tag") }} 24 | {{ activity.metadata.version }} 25 | {% elif activity.event == "update_shard:description_changed" %} 26 | Updated description 27 | {% elif activity.event == "import_catalog:mirror:switched" %} 28 | Mirror switched: {{ repo_link(activity.repo_ref) }} from {{ activity.metadata.old_role }} to {{ activity.metadata.role }} 29 | {% elif activity.event == "import_shard:created" %} 30 | Shard added to database 31 | {% else %} 32 | {{ activity.event }} {{ activity.metadata }} 33 | {% endif %} 34 | {% if loop.first or activity.event == "sync_release:created" %} 35 | {{ time_date(activity.created_at) }} 36 | {% endif %} 37 |
38 | {% endfor %} 39 |
40 | {% endfor %} 41 |
42 | {% endblock %} 43 | -------------------------------------------------------------------------------- /app/views/categories/show.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/date.j2" %} 3 | {% import "macros/links.j2" %} 4 | {% import "macros/shard-card.j2" %} 5 | 6 | {% set uncategorized = category.slug == "Uncategorized" %} 7 | 8 | {% block page_title %}{{ category.name }} on Shardbox{% endblock %} 9 | {% block main %} 10 |
11 | 14 |

{{ category.name }}

15 |
16 | 17 | {% if category.description %} 18 |

{{ category.description | markdown_inline }}

19 | {% endif %} 20 | 21 |

{{ entries_count }} shards

22 | 23 |
24 | {% for result in shards %} 25 |
26 | {{ shard_card(result.shard, result, result.categories, 27 | repo_ref = result.repo.ref, 28 | repo = result.repo, 29 | released_at = result.release.released_at, 30 | version = result.release.version, 31 | show_topics = true, 32 | show_description = true, show_created_at = false) }} 33 | {% if uncategorized -%} 34 |
- {{ result.repo.ref.resolver }}: {{ result.repo.ref.url }} 35 | description: {{ result.release.description | default(result.repo.metadata.description) }}
36 | {% set homonymous = homonymous_shards[result.shard.name] %} 37 | {% if homonymous and homonymous | length > 1 %} 38 |
39 | Similar: 40 | {% for entry in homonymous if entry.qualifier != result.shard.qualifier %} 41 | 42 | {{ shard_name(entry) }} 43 | 44 | {% endfor %} 45 |
46 | {% endif %} 47 | {% endif %} 48 |
49 | {% endfor %} 50 |
51 | 52 |
53 | {{ icon("edit") }} Edit this category 54 |
55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /app/sass/pages/home.sass: -------------------------------------------------------------------------------- 1 | .home-header 2 | @extend %max-width 3 | @media(min-width: 768px) 4 | display: grid 5 | grid-template-columns: 3fr 1fr 6 | 7 | font-size: 1.3em 8 | 9 | > h1 10 | grid-column-start: 1 11 | grid-column-end: 3 12 | font-size: 3.8rem 13 | margin-bottom: 1.5rem 14 | 15 | .home-stats 16 | > p 17 | margin: 0 18 | 19 | .home-nav 20 | display: flex 21 | align-items: flex-end 22 | justify-content: flex-end 23 | 24 | .containers, .home-header 25 | margin-left: 1rem 26 | margin-right: 1rem 27 | 28 | .containers--home 29 | h3 30 | padding: 0 .5rem 31 | font-weight: 100 32 | font-size: 3rem 33 | margin: 2.5rem 0 .5rem 34 | 35 | .shards-lists 36 | display: flex 37 | flex-wrap: wrap 38 | flex-direction: row 39 | 40 | .shards-list 41 | flex-grow: 1 42 | min-width: 25em 43 | flex-basis: 25em 44 | margin: 0 -.6em 45 | display: flex 46 | flex-flow: row wrap 47 | 48 | > .shard-card 49 | min-width: 15em 50 | max-width: 21em 51 | margin: .6em 52 | display: flex 53 | flex-direction: column 54 | 55 | > .description 56 | //max-width: 12em 57 | 58 | .stats-container 59 | display: flex 60 | 61 | table.stats 62 | width: 100% 63 | > tbody > tr 64 | > th, > td 65 | padding-bottom: .3em 66 | padding-top: .3em 67 | border-bottom: 1px dashed #ddd 68 | > th 69 | text-align: right 70 | vertical-align: top 71 | width: 10em 72 | > td 73 | padding-left: .5em 74 | vertical-align: top 75 | 76 | table.counts 77 | with: 100% 78 | > tbody > tr 79 | > td 80 | text-align: right 81 | color: var(--color-light) 82 | font-weight: 500 83 | vertical-align: top 84 | > th 85 | text-align: left 86 | font-weight: 500 87 | color: var(--color-lighter) 88 | vertical-align: top 89 | padding-left: .8em 90 | 91 | &::before 92 | content: "·" 93 | margin-left: -.7em 94 | margin-right: .3em 95 | color: var(--color-lighter) 96 | font-weight: bold 97 | 98 | .count 99 | color: var(--color-light) 100 | font-weight: 500 101 | -------------------------------------------------------------------------------- /app/sass/components/site-header.sass: -------------------------------------------------------------------------------- 1 | .site-header 2 | margin: 0 3 | background: #333 4 | color: #ddd 5 | 6 | @media(min-width: 1024px) 7 | padding: 0 $main-margin / 3 8 | @media(min-width: 1200px) 9 | padding: 0 2rem 10 | 11 | .site-header_items 12 | @extend %max-width 13 | margin: 0 auto 14 | display: flex 15 | flex-direction: row 16 | align-items: center 17 | min-height: 4rem 18 | 19 | a 20 | text-decoration: none 21 | padding: .3em .5em 22 | font-size: 1.2rem 23 | color: inherit 24 | white-space: nowrap 25 | 26 | &:hover, &:focus 27 | color: #fff 28 | 29 | .search-field 30 | display: flex 31 | flex-direction: row 32 | align-items: stretch 33 | margin: .3em 0 34 | flex-grow: 1 35 | max-width: 30em 36 | position: relative 37 | 38 | > input[type=search] 39 | border: none 40 | padding: .3em 41 | padding-left: 1.7em 42 | background: var(--color-light) 43 | color: #eee 44 | font-size: 1.1em 45 | font-family: Roboto 46 | letter-spacing: 0.02em 47 | border-radius: .5rem 48 | flex-grow: 1 49 | 50 | &::placeholder 51 | color: #bbb 52 | 53 | &:active, &:focus 54 | box-shadow: .1em .1em .3em #222 55 | 56 | > button 57 | border: none 58 | padding: 0 59 | color: inherit 60 | cursor: pointer 61 | font-size: 1.5rem 62 | border-radius: .5rem 63 | background: transparent 64 | position: absolute 65 | top: 0 66 | bottom: 0 67 | left: 0 68 | 69 | &:hover, &:focus 70 | color: var(--color-lighter) 71 | 72 | > .icon 73 | margin: 0 74 | display: block 75 | width: 1.4em 76 | height: 1.4em 77 | padding: .3em 78 | color: #333 79 | 80 | .header-nav 81 | margin: .5em 82 | 83 | a 84 | font-size: 1rem 85 | border-radius: .3em 86 | font-size: 1.2em 87 | font-weight: 200 88 | 89 | &:hover, &:focus 90 | background-color: #404040 91 | 92 | @media(max-width: 1024px) 93 | > span 94 | display: none 95 | 96 | a.site-title 97 | font-size: 1.5rem 98 | margin: 0 1rem 99 | font-weight: bold 100 | letter-spacing: 0.04em 101 | text-transform: uppercase 102 | color: #fff 103 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | shards: 3 | any_hash: 4 | git: https://github.com/sija/any_hash.cr.git 5 | version: 0.2.5 6 | 7 | backtracer: 8 | git: https://github.com/sija/backtracer.cr.git 9 | version: 1.2.2 10 | 11 | baked_file_system: 12 | git: https://github.com/schovi/baked_file_system.git 13 | version: 0.10.0 14 | 15 | crinja: 16 | git: https://github.com/straight-shoota/crinja.git 17 | version: 0.8.0 18 | 19 | db: 20 | git: https://github.com/crystal-lang/crystal-db.git 21 | version: 0.11.0 22 | 23 | exception_page: 24 | git: https://github.com/crystal-loot/exception_page.git 25 | version: 0.2.2 26 | 27 | git: 28 | git: https://github.com/smacker/libgit2.cr.git 29 | version: 0.1.0+git.commit.6267162e7d9e16edace62f942d08565518bc73d9 30 | 31 | github-cr: 32 | git: https://github.com/arnavb/github-cr.git 33 | version: 0.1.0+git.commit.119df4c747a81260b37af0878b24eab9345a94f4 34 | 35 | humanize_time: 36 | git: https://github.com/mamantoha/humanize_time.git 37 | version: 0.7.0 38 | 39 | i18n: 40 | git: https://github.com/techmagister/i18n.cr.git 41 | version: 0.4.1+git.commit.289c22bfa80b212a93361b2121b4e366036be5e4 42 | 43 | kemal: 44 | git: https://github.com/kemalcr/kemal.git 45 | version: 1.2.0 46 | 47 | markd: 48 | git: https://github.com/icyleaf/markd.git 49 | version: 0.5.0 50 | 51 | molinillo: 52 | git: https://github.com/crystal-lang/crystal-molinillo.git 53 | version: 0.2.0 54 | 55 | pg: 56 | git: https://github.com/will/crystal-pg.git 57 | version: 0.26.0 58 | 59 | radix: 60 | git: https://github.com/luislavena/radix.git 61 | version: 0.4.1 62 | 63 | raven: 64 | git: https://github.com/sija/raven.cr.git 65 | version: 1.9.2 66 | 67 | sanitize: 68 | git: https://github.com/straight-shoota/sanitize.git 69 | version: 0.1.0+git.commit.75c141b619c77956e88f557149566cd28876398b 70 | 71 | sass: 72 | git: https://github.com/straight-shoota/sass.cr.git 73 | version: 0.6.0 74 | 75 | shardbox-core: 76 | git: https://github.com/shardbox/shardbox-core.git 77 | version: 0.2.0+git.commit.f6feed28afe49bccf7b018c7dc46940c15f00071 78 | 79 | shards: 80 | git: https://github.com/crystal-lang/shards.git 81 | version: 0.17.0 82 | 83 | -------------------------------------------------------------------------------- /app/views/pages/contribute.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/links.j2" %} 3 | 4 | {% block page_title %}Shardbox: Contribute{% endblock %} 5 | {% block main %} 6 |
7 |

Contribute

8 |
9 | 10 |
11 |
12 |

13 | Help wanted!
14 | You're welcome to help improve shardbox by categorizing shards, updating 15 | categories and contributing to the shardbox app. 16 |

17 | 18 |

Catalog

19 |

20 | The catalog is managed at 21 | 22 | {{ icon("github") }} 23 | shardbox/catalog 24 | . 25 | Feel free to send pull requests against this repo. 26 |

27 |

28 | Changes will be imported into the shardbox database. 29 | 30 | Instructions on how to edit the catalog are available in the 31 | README.md. 32 |

33 |

34 | Good places to start: 35 |

36 |
    37 |
  • 38 | Add a shard that you know, but is not yet listed in the database. Either 39 | add it to an existing category, or suggest a new one. 40 |
  • 41 |
  • 42 | Categorize uncategorized shards. 43 | They're in the database because they were discovered as dependency 44 | of another shard, but need to be inserted into the catalog. 45 |
  • 46 |
47 | 48 |

App

49 |

50 | The shardbox app is written in Crystal and consist of two shards: 51 |

52 | 66 |
67 | 68 |
69 |
70 |
71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /src/raven.cr: -------------------------------------------------------------------------------- 1 | require "raven" 2 | require "raven/integrations/kemal" 3 | 4 | # Perform basic raven configuration, none of it is required though 5 | Raven.configure do |config| 6 | if env_var = ENV["SENTRY_DSN_VAR"]? 7 | config.dsn = ENV[env_var] 8 | end 9 | 10 | # Keep main fiber responsive by sending the events in the background 11 | config.async = true 12 | # Set the environment name using `Kemal.config.env`, which uses `KEMAL_ENV` variable under-the-hood 13 | config.current_environment = Kemal.config.env 14 | 15 | # If your requests are failing because of connection 16 | # timeout error, try setting bigger value 17 | # (defaults to `1.second`). 18 | # 19 | # NOTE: Avoid using bigger values without `#async` option enabled 20 | config.connect_timeout = 5.seconds 21 | 22 | # In case of hitting rate limit you might want to try 23 | # lower sample rate threshold, in this case to 75% 24 | config.sample_rate = 0.75 25 | 26 | # Remove default processors you don't need 27 | # config.processors -= [Raven::Processor::Cookies, Raven::Processor::RequestMethodData] 28 | 29 | # Ignore certain exception classes 30 | # `Kemal::Exceptions::RouteNotFound` is added automatically 31 | # config.excluded_exceptions << NotImplementedError 32 | 33 | # Sanitize additional fields 34 | # config.sanitize_fields << /\Aaddress_(.*?)\Z/i 35 | 36 | # Setup `#before_send` hook, which allows modifying 37 | # the event before sending, or dropping it entirely 38 | # config.before_send do |event, hint| 39 | # # Group events by topic based on exception message 40 | # if hint.try(&.exception).try(&.message) =~ /database unavailable/i 41 | # event.fingerprint << "database-unavailable" 42 | # end 43 | # # Conditionally skip sending the event 44 | # event unless ENV["CI"]? == "1" 45 | # end 46 | end 47 | 48 | # Replace the built-in `Kemal::LogHandler` with a 49 | # dedicated `Raven::Kemal::LogHandler`, capturing all 50 | # sent messages and requests as Sentry breadcrumbs 51 | 52 | # If you'd like to preserve default logging provided by 53 | # Kemal, pass `Kemal::LogHandler.new` to the constructor 54 | if Kemal.config.logging 55 | Kemal.config.logger = Raven::Kemal::LogHandler.new(Kemal::LogHandler.new) 56 | else 57 | Kemal.config.logger = Raven::Kemal::LogHandler.new 58 | end 59 | 60 | # Add raven's exception handler in order to capture 61 | # all unhandled exceptions thrown inside your routes. 62 | # Captured exceptions are re-raised afterwards 63 | Kemal.config.add_handler Raven::Kemal::ExceptionHandler.new 64 | -------------------------------------------------------------------------------- /app/views/owners/show.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/date.j2" %} 3 | {% import "macros/links.j2" %} 4 | {% import "macros/shard-card.j2" %} 5 | 6 | {% block page_title %}{{ owner.name | default(owner.slug) }} on Shardbox{% endblock %} 7 | {% block main %} 8 |
9 | 12 |
13 | {{ owner_image(owner) }} 14 |

15 | {{ owner.name | default(owner.slug) }} 16 |

17 |

18 | {% if owner.resolver == "github" %} 19 | 20 | @{{ owner.slug }} 21 | 22 | {% else %} 23 | @{{ owner.slug }} 24 | {% endif %} 25 |

26 |
27 | 28 | {% if owner.description %} 29 |

{{ owner.description | markdown_inline }}

30 | {% endif %} 31 | 32 | {% if owner.website_url %} 33 |

34 | {{ icon("globe") }} {{ nice_url(owner.website_url) }} 35 |

36 | {% endif %} 37 |
38 | 39 | {% if metrics %} 40 |
41 | 42 | shards 43 | {{ icon("package") }} {{ metrics.shards_count }} 44 | 45 | 46 | direct dependents 47 | {{ icon("package-dependents") }} {{ metrics.dependents_count}} 48 | 49 | 50 | total dependents 51 | {{ icon("package-dependents") }}{{ icon("plus") }} {{ metrics.transitive_dependents_count }} 52 | 53 | 54 | dev dependents 55 | {{ icon("package-dependents") }}{{ icon("code") }} {{ metrics.dev_dependents_count }} 56 | 57 |
58 | {% endif %} 59 | 60 |

Shards {{ shards | length }}

61 | 62 |
63 | {% for item in shards %} 64 | {{ shard_card(item.shard, item, item.categories, 65 | version = item.version, 66 | released_at = item.released_at) }} 67 | {% endfor %} 68 |
69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /app/views/stats.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/links.j2" %} 3 | {% macro percent(value) %}{{ (value * 100) | round(2) }}%{% endmacro %} 4 | 5 | {% block page_title %}Shardbox: Stats{% endblock %} 6 | {% block main %} 7 |
8 |

Statistics

9 |
10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 20 |
Total shards{{ stats.shards_count }}
Total repos{{ stats.repos_count }}
Dependencies{{ stats.dependencies_count }}
Dev dependencies{{ stats.dev_dependencies_count }}
Uncategorized shards{{ stats.uncategorized_count }} 19 | {{ percent(stats.uncategorized_count / stats.shards_count) }}
21 |
22 | 23 |
24 |
25 | 26 | 33 | 34 | 41 | 42 | 43 | 50 |
Resolvers 27 | 28 | {% for name, count in stats.resolver_counts %} 29 | 30 | {% endfor %} 31 |
{{ count }}{{ percent(count / stats.repos_count) }}{{ name }}
32 |
Licenses 35 | 36 | {% for name, count in stats.license_counts %} 37 | 38 | {% endfor %} 39 |
{{ count }}{{ percent(count / stats.shards_count) }}{{ name }}
40 |
shard.yml Keys 44 | 45 | {% for name, count in stats.shard_yml_keys_counts %} 46 | 47 | {% endfor %} 48 |
{{ count }}{{ percent(count / stats.shards_count) }}{{ name }}
49 |
51 | 52 | 59 | 60 |
Crystal Versions 53 | 54 | {% for name, count in stats.crystal_version_counts %} 55 | 56 | {% endfor %} 57 |
{{ count }}{{ percent(count / stats.shards_count) }}{{ name }}
58 |
61 |
62 |
63 |
64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /app/views/releases/_infobox.html.j2: -------------------------------------------------------------------------------- 1 |
2 |
{{ shard.name }}:
 3 |   {{ repo.ref.resolver }}: {{ repo.ref.url }}
 4 |   {% if release.version != "HEAD" %}version: ~> {{ release.version }}{% endif %}
5 | 6 | 39 | 40 |
41 | {% if release.license %} 42 |
43 | {{ icon("law") }} License 44 | 45 | {{ release.license }} 46 | 47 |
48 | {% endif %} 49 |
50 | {{ icon("crystal") }} Crystal 51 | {{ release.crystal }} 52 |
53 |
54 | 55 | {% if release.spec.authors %} 56 |
57 |

{{ icon("person") }} Authors

58 |
    59 | {% for author in release.spec_authors %} 60 |
  • 61 | 62 | {{ author.name }} 63 | {% if author.email %} 64 | {{ author.email }} 65 | {% endif %} 66 |
  • 67 | {% endfor %} 68 |
69 |
70 | {% endif %} 71 | 72 | {% include "releases/_dependencies.html.j2" %} 73 |
74 | -------------------------------------------------------------------------------- /src/crinja_lib.cr: -------------------------------------------------------------------------------- 1 | require "markd" 2 | require "digest" 3 | require "sanitize" 4 | 5 | INLINE_SANITIZER = Sanitize::Policy::HTMLSanitizer.inline 6 | 7 | MARKD_SANITIZER = Sanitize::Policy::HTMLSanitizer.common 8 | # Allow classes with `language-` prefix which are used for syntax highlighting. 9 | MARKD_SANITIZER.valid_classes << /language-.+/ 10 | 11 | Crinja.filter({base_url: nil}, "markdown_inline") do 12 | render_markdown(target, arguments, INLINE_SANITIZER) 13 | end 14 | 15 | def render_markdown(target, arguments, sanitizer) 16 | html = Markd.to_html(target.to_s) 17 | 18 | if base_url = arguments["base_url"].as_s?.to_s 19 | sanitizer = sanitizer.dup 20 | sanitizer.uri_sanitizer = sanitizer.uri_sanitizer.dup 21 | sanitizer.uri_sanitizer.base_url = URI.parse(base_url) 22 | end 23 | sanitized = sanitizer.process(html) 24 | 25 | Crinja::SafeString.new(sanitized) 26 | end 27 | 28 | Crinja.filter({base_url: nil}, "markdown") do 29 | render_markdown(target, arguments, MARKD_SANITIZER) 30 | end 31 | 32 | Crinja.filter({repo_ref: Crinja::UNDEFINED, revision: nil}, "markdown_repo_content") do 33 | repo_ref = arguments["repo_ref"].raw.as(Repo::Ref) 34 | refname = arguments["revision"].as_s? 35 | 36 | sanitizer = ReadmeSanitizer.new(repo_ref.base_url_source(refname), repo_ref.base_url_raw(refname)) 37 | 38 | html = Markd.to_html(target.to_s) 39 | sanitized = sanitizer.process(html) 40 | 41 | Crinja::SafeString.new(sanitized) 42 | end 43 | 44 | Crinja.function({repo_ref: Crinja::UNDEFINED, release: Crinja::UNDEFINED}, "crystaldoc_info_url") do 45 | repo_ref = arguments["repo_ref"].raw.as(Repo::Ref) 46 | release = arguments["release"].raw.as(Release) 47 | 48 | repo_ref.crystaldoc_info_url(release) 49 | end 50 | 51 | class ReadmeSanitizer < Sanitize::Policy::HTMLSanitizer 52 | property src_uri_sanitizer : Sanitize::URISanitizer? 53 | 54 | def self.new(base_url, src_url) 55 | common.tap do |instance| 56 | instance.uri_sanitizer.base_url = base_url 57 | src_sanitizer = Sanitize::URISanitizer.new 58 | src_sanitizer.base_url = src_url 59 | instance.src_uri_sanitizer = src_sanitizer 60 | instance.valid_classes << /language-.+/ 61 | end 62 | end 63 | 64 | def transform_uri(tag, attributes, attribute, uri : URI) : String? 65 | if attribute == "src" && (src_uri_sanitizer = self.src_uri_sanitizer) 66 | uri = src_uri_sanitizer.sanitize(uri) 67 | 68 | return unless uri 69 | 70 | # Make sure special characters are properly encoded to avoid interpretation 71 | # of tweaked relative paths as "javascript:" URI (for example) 72 | if path = uri.path 73 | uri.path = URI.encode(URI.decode(path)) 74 | end 75 | 76 | uri.to_s 77 | else 78 | super 79 | end 80 | end 81 | end 82 | 83 | Crinja.filter("gravatar_hash") do 84 | mail = target.as_s 85 | Digest::MD5.hexdigest(mail.strip.downcase) 86 | end 87 | -------------------------------------------------------------------------------- /src/crinja_models.cr: -------------------------------------------------------------------------------- 1 | @[Crinja::Attributes] 2 | class URI 3 | include Crinja::Object::Auto 4 | end 5 | 6 | @[Crinja::Attributes] 7 | class Shard 8 | include Crinja::Object::Auto 9 | 10 | @[Crinja::Attribute] 11 | def archived 12 | archived? 13 | end 14 | end 15 | 16 | @[Crinja::Attributes] 17 | class Category 18 | include Crinja::Object::Auto 19 | end 20 | 21 | @[Crinja::Attributes] 22 | struct ShardsDB::CategoryResult 23 | include Crinja::Object::Auto 24 | 25 | @[Crinja::Attribute(ignore: true)] 26 | def clone 27 | previous_def 28 | end 29 | end 30 | 31 | @[Crinja::Attributes(exclude: scope)] 32 | class Dependency 33 | include Crinja::Object::Auto 34 | 35 | @[Crinja::Attribute(ignore: true)] 36 | def scope : Scope 37 | previous_def 38 | end 39 | 40 | def crinja_attribute(attr : Crinja::Value) 41 | if attr.to_string == "scope" 42 | return Crinja::Value.new(scope.to_s) 43 | end 44 | 45 | super 46 | end 47 | end 48 | 49 | @[Crinja::Attributes] 50 | class Release 51 | include Crinja::Object::Auto 52 | 53 | def crinja_attribute(attr : Crinja::Value) 54 | case attr.to_string 55 | when "latest" 56 | Crinja::Value.new(latest?) 57 | when "yanked_at" 58 | Crinja::Value.new(yanked_at?) 59 | else 60 | super 61 | end 62 | end 63 | 64 | @[Crinja::Attributes] 65 | struct RevisionInfo 66 | include Crinja::Object::Auto 67 | end 68 | 69 | @[Crinja::Attributes] 70 | struct Commit 71 | include Crinja::Object::Auto 72 | end 73 | 74 | @[Crinja::Attributes] 75 | struct Tag 76 | include Crinja::Object::Auto 77 | end 78 | 79 | @[Crinja::Attributes] 80 | struct Signature 81 | include Crinja::Object::Auto 82 | end 83 | end 84 | 85 | @[Crinja::Attributes] 86 | class Repo 87 | include Crinja::Object::Auto 88 | 89 | def crinja_attribute(attr : Crinja::Value) 90 | if attr.to_string == "role" 91 | return Crinja::Value.new(role.to_s) 92 | end 93 | 94 | super 95 | end 96 | end 97 | 98 | @[Crinja::Attributes] 99 | struct Repo::Ref 100 | include Crinja::Object::Auto 101 | end 102 | 103 | @[Crinja::Attributes] 104 | struct Repo::Metadata 105 | include Crinja::Object::Auto 106 | end 107 | 108 | @[Crinja::Attributes] 109 | class Repo::Owner 110 | include Crinja::Object::Auto 111 | end 112 | 113 | @[Crinja::Attributes] 114 | struct Repo::Owner::Metrics 115 | include Crinja::Object::Auto 116 | end 117 | 118 | @[Crinja::Attributes] 119 | struct ShardsDB::Stats 120 | include Crinja::Object::Auto 121 | end 122 | 123 | @[Crinja::Attributes] 124 | struct ShardsDB::Metrics 125 | include Crinja::Object::Auto 126 | end 127 | 128 | @[Crinja::Attributes] 129 | struct Activity 130 | include Crinja::Object::Auto 131 | @[Crinja::Attribute(ignore: true)] 132 | def clone 133 | previous_def 134 | end 135 | end 136 | 137 | @[Crinja::Attributes] 138 | struct Author 139 | include Crinja::Object::Auto 140 | end 141 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include Makefile.local # for optional local options 2 | 3 | BUILD_TARGET := bin/app 4 | WORKER_TARGET := bin/worker 5 | BUILD_TARGETS := $(BUILD_TARGET) $(WORKER_TARGET) 6 | 7 | # The shards command to use 8 | SHARDS ?= shards 9 | # The crystal command to use 10 | CRYSTAL ?= crystal 11 | 12 | SRC_SOURCES := $(shell find src -name '*.cr' 2>/dev/null) 13 | LIB_SOURCES := $(shell find lib -name '*.cr' 2>/dev/null) 14 | SPEC_SOURCES := $(shell find spec -name '*.cr' 2>/dev/null) 15 | 16 | PG_USER := postgres 17 | DATABASE_NAME := $(shell echo $(DATABASE_URL) | grep -o -P '[^/]+$$') 18 | TEST_DATABASE_NAME := $(shell echo $(TEST_DATABASE_URL) | grep -o -P '[^/]+$$') 19 | 20 | .PHONY: build 21 | build: ## Build the application binary 22 | build: $(BUILD_TARGETS) 23 | 24 | $(BUILD_TARGET): $(SRC_SOURCES) $(LIB_SOURCES) lib 25 | mkdir -p $(@D) 26 | $(CRYSTAL) build src/cli.cr -o $(@) 27 | 28 | $(WORKER_TARGET): $(SRC_SOURCES) $(LIB_SOURCES) lib 29 | mkdir -p $(@D) 30 | $(CRYSTAL) build lib/shardbox-core/src/worker.cr -o $(@) 31 | 32 | .PHONY: test 33 | test: ## Run the test suite 34 | test: lib 35 | $(CRYSTAL) spec 36 | 37 | .PHONY: format 38 | format: ## Apply source code formatting 39 | format: $(SRC_SOURCES) $(SPEC_SOURCES) 40 | $(CRYSTAL) tool format src 41 | 42 | docs: ## Generate API docs 43 | docs: $(SRC_SOURCES) lib 44 | $(CRYSTAL) docs -o docs src/cli.cr 45 | 46 | lib: shard.lock 47 | $(SHARDS) install 48 | # Touch is necessary because `shards install` always touches shard.lock 49 | touch lib 50 | 51 | shard.lock: shard.yml 52 | $(SHARDS) update 53 | 54 | .PHONY: public/assets 55 | public/assets: $(BUILD_TARGET) 56 | $(BUILD_TARGET) assets:precompile 57 | 58 | .PHONY: DATABASE_URL 59 | DATABASE_URL: 60 | @test "${$@}" || (echo "$@ is undefined" && false) 61 | 62 | .PHONY: TEST_DATABASE_URL 63 | TEST_DATABASE_URL: 64 | @test "${$@}" || (echo "$@ is undefined" && false) 65 | 66 | .PHONY: deploy 67 | deploy: 68 | @test "$(DEPLOY_HOST)" || (echo "$$DEPLOY_HOST is undefined" && false) 69 | docker build . -t dokku/shardbox:latest 70 | docker save dokku/shardbox:latest | ssh $(DEPLOY_HOST) "sudo docker load && dokku git:from-image shardbox dokku/shardbox:latest && dokku ps:rebuild shardbox" 71 | 72 | .PHONY: clean 73 | clean: ## Remove application binary 74 | clean: 75 | @rm -rf $(BUILD_TARGETS) 76 | @rm -rf public/assets/css/style.css 77 | 78 | .PHONY: help 79 | help: ## Show this help 80 | @echo 81 | @printf '\033[34mtargets:\033[0m\n' 82 | @grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\ 83 | sort |\ 84 | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' 85 | @echo 86 | @printf '\033[34moptional variables:\033[0m\n' 87 | @grep -hE '^[a-zA-Z_-]+ \?=.*?## .*$$' $(MAKEFILE_LIST) |\ 88 | sort |\ 89 | awk 'BEGIN {FS = " \\?=.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' 90 | @echo 91 | @printf '\033[34mrecipes:\033[0m\n' 92 | @grep -hE '^##.*$$' $(MAKEFILE_LIST) |\ 93 | awk 'BEGIN {FS = "## "}; /^## [a-zA-Z_-]/ {printf " \033[36m%s\033[0m\n", $$2}; /^## / {printf " %s\n", $$2}' 94 | -------------------------------------------------------------------------------- /app/views/pages/imprint.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/links.j2" %} 3 | 4 | {% block page_title %}Shardbox: Imprint{% endblock %} 5 | {% block main %} 6 |
7 |

Legal disclosure

8 |
9 | 10 |
11 |
12 |

13 | Information in accordance with §5 TMG 14 | and responsible for contents: 15 |

16 |
17 | Johannes Werner Müller
18 | Ostendstraße 1
19 | 36103 Flieden 20 |
21 |

Contact

22 | straightshoota@gmail.com 23 |
24 |
25 |

Disclaimer

26 |

Accountability for content

27 |

28 |

We are responsible for our own content in accordance with § 7 Abs. 1 TMG. 29 | According to §§ 8 to 10 TMG, however, we as a service provider are not obliged 30 | to monitor transmitted or stored external information or to investigate 31 | circumstances that indicate an illegal activity. 32 | Thus the removal or blocking of the use of information under the general 33 | laws remain unaffected. 34 | A liability in this regard, however, is only possible from the date of 35 | knowledge of a specific infringement. Upon notification of appropriate 36 | violations, we will remove this content immediately. 37 |

38 | 39 |

Accountability for links

40 |

41 | Our offer contains links to external websites on whose content we have 42 | no influence. Therefore we can not assume any liability for these external 43 | contents. The content of the linked pages is always the responsibility 44 | of the respective provider or operator of the pages. The linked pages 45 | were checked the time of the link to possible leftover. Illegal content 46 | was not recognizable at the time of linking. A permanent content control 47 | of the linked pages is, however, unreasonable without concrete evidence 48 | of an infringement. Upon notification of violations, we will remove such 49 | links immediately. 50 |

51 | 52 |

Copyright

53 |

54 | The pages of the driver are in content and works in the pages include 55 | the copyright. The duplication, processing, distribution and any kind of 56 | exploitation outside the limits of copyright require the written consent 57 | of the respective author or creator. Downloads and copies of this site 58 | are for private, non-commercial use only. As far as the contents on this 59 | side were not created by the operator, the copyrights of third parties 60 | are considered. In particular contents of third parties are marked as 61 | such. If you want to be wary of a breach of original rights, please 62 | notify us accordingly. Upon notification of violations, we will thus 63 | remove content immediately. 64 |

65 |
66 |
67 | {% endblock %} 68 | -------------------------------------------------------------------------------- /app/views/releases/_layout.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/date.j2" %} 3 | {% import "macros/links.j2" %} 4 | 5 | {% block main %} 6 | {% include "releases/_header.html.j2" %} 7 | 8 |
9 |
10 | 16 |
17 | {% block release_main %} 18 | {% endblock %} 19 |
20 |
21 | 22 |
23 | {% include "releases/_infobox.html.j2" %} 24 | 25 |
26 |

Releases

27 | 47 | {% if remaining_releases_count > 0 %} 48 | Show all {{ all_releases | length }} releases 49 | {% endif %} 50 |
51 | 52 |
53 | {% if repo.synced_at %} 54 | Last synced {{ time_date(repo.synced_at, title = "Last synced on {{date}}") }}. 55 | {% endif %} 56 |
57 | {% set category = categories[0] %} 58 | {% if category %} 59 | 66 | {% endif %} 67 |
68 |
69 | 70 | 71 | 72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /src/api.cr: -------------------------------------------------------------------------------- 1 | get "/api/v1/search" do |context| 2 | context.response.content_type = "application/json" 3 | query = context.request.query_params["q"]? || "" 4 | 5 | ShardsDB.connect do |db| 6 | shards = db.search(query) 7 | 8 | JSON.build(context.response) do |json| 9 | json.object do 10 | json.field "query", query 11 | json.field "results" do 12 | json.array do 13 | shards.each do |entry| 14 | json.object do 15 | shard = entry[:shard] 16 | json.field "name", shard.name 17 | json.field "qualifier", shard.qualifier 18 | json.field "display_name", shard.display_name 19 | json.field "canonical_repo", entry[:repo].ref.to_uri.to_s 20 | json.field "description", shard.description 21 | json.field "latest_release" do 22 | json.object do 23 | json.field "version", entry[:version] 24 | json.field "released_at", entry[:released_at] 25 | end 26 | end 27 | json.field "details_url", "/shards/#{shard.slug}" 28 | json.field "categories" do 29 | json.array do 30 | entry[:categories].each do |category| 31 | category.slug.to_json(json) 32 | end 33 | end 34 | end 35 | json.field "archived_at", shard.archived_at if shard.archived_at 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | end 43 | end 44 | 45 | get "/api/v1/shards/:name" do |context| 46 | context.response.content_type = "application/json" 47 | ShardsDB.connect do |db| 48 | page = Page::Shard.new(db, context, "json") 49 | case page 50 | when String 51 | halt context, 404, page 52 | when Nil 53 | next 54 | when Page::Shard 55 | page.to_json(context.response) 56 | nil 57 | end 58 | end 59 | end 60 | 61 | get "/api/v1/shards/:name/releases" do |context| 62 | context.response.content_type = "application/json" 63 | ShardsDB.connect do |db| 64 | page = Page::Shard.new(db, context, "releases") 65 | case page 66 | when String 67 | halt context, 404, page 68 | when Nil 69 | next 70 | when Page::Shard 71 | JSON.build(context.response) do |json| 72 | json.object do 73 | shard = page.shard 74 | json.field "name", shard.name 75 | json.field "display_name", shard.display_name 76 | json.field "details_url", "/shards/#{shard.slug}" 77 | json.field "canonical_repo", page.canonical_repo.ref.to_uri.to_s 78 | 79 | json.field "releases" do 80 | json.array do 81 | page.all_releases.each do |release| 82 | json.object do 83 | json.field "version", release.version 84 | json.field "released_at", release.released_at 85 | json.field "commit_hash", release.commit_hash 86 | end 87 | end 88 | end 89 | end 90 | end 91 | end 92 | nil 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /app/views/home.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "layout.html.j2" %} 2 | {% import "macros/date.j2" %} 3 | {% import "macros/links.j2" %} 4 | {% import "macros/shard-card.j2" %} 5 | 6 | {% block page_title %}Shardbox: A shards database{% endblock %} 7 | {% block main %} 8 |
9 |

A Database of Crystal Shards

10 | 11 |
12 |

13 | Listing {{ stats.shards_count }} shards 14 | with {{ stats.dependencies_count }} dependencies. 15 |

16 | 17 |

18 | {% if stats.uncategorized_count > 0 %} 19 | {{ stats.uncategorized_count }} are uncategorized. 20 | {{ icon("tools") }} Help categorize them 21 | {% else %} 22 | You can help adding and improving the catalog. 23 | {{ icon("tools") }} Contribute 24 | {% endif %} 25 |

26 |
27 | 28 | 31 |
32 | 33 |
34 |
35 |

Most popular

36 |
37 | {% for item in popular_shards %} 38 | {{ shard_card(item.shard, item, item.categories, 39 | version = item.version, 40 | released_at = item.released_at, 41 | show_dev_dependents = false) }} 42 | {% endfor %} 43 |
44 |
45 | 46 |
47 |

Most depended upon

48 |
49 | {% for item in dependent_shards %} 50 | {{ shard_card(item.shard, item, item.categories, show_description = false, 51 | version = item.version, 52 | released_at = item.released_at, 53 | show_dev_dependents = false) }} 54 | {% endfor %} 55 |
56 |
57 | 58 |
59 |

Recently released

60 |
61 | {% for item in recent_shards %} 62 | {{ shard_card(item.shard, item, item.categories, 63 | show_dependents = false, 64 | show_dev_dependents = false, 65 | show_description = true, 66 | version = item.version, 67 | released_at = item.released_at) }} 68 | {% endfor %} 69 |
70 |
71 | 72 |
73 |

Most depended upon (development)

74 |
75 | {% for item in dev_dependent_shards %} 76 | {{ shard_card(item.shard, item, item.categories, 77 | show_transitive_dependents = false, 78 | show_dev_dependents = true, 79 | version = item.version, 80 | released_at = item.released_at) }} 81 | {% endfor %} 82 |
83 |
84 | 85 |
86 |

New shards

87 |
88 | {% for item in new_shards %} 89 | {{ shard_card(item.shard, item, item.categories, 90 | show_dependents = false, 91 | show_dev_dependents = false, 92 | show_description = true, 93 | show_repo = true, 94 | show_created_at = true, 95 | show_released_at = false) }} 96 | {% endfor %} 97 |
98 |
99 |
100 | {% endblock %} 101 | -------------------------------------------------------------------------------- /public/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.17.1 2 | https://prismjs.com/download.html#themes=prism-solarizedlight&languages=markup+css+clike+javascript+c+asciidoc+bash+ruby+diff+markup-templating+docker+lua+erb+go+graphql+json+crystal+liquid+makefile+sql+powershell+scss+python+rust+sass+shell-session+yaml+toml+regex */ 3 | /* 4 | Solarized Color Schemes originally by Ethan Schoonover 5 | http://ethanschoonover.com/solarized 6 | 7 | Ported for PrismJS by Hector Matos 8 | Website: https://krakendev.io 9 | Twitter Handle: https://twitter.com/allonsykraken) 10 | */ 11 | 12 | /* 13 | SOLARIZED HEX 14 | --------- ------- 15 | base03 #002b36 16 | base02 #073642 17 | base01 #586e75 18 | base00 #657b83 19 | base0 #839496 20 | base1 #93a1a1 21 | base2 #eee8d5 22 | base3 #fdf6e3 23 | yellow #b58900 24 | orange #cb4b16 25 | red #dc322f 26 | magenta #d33682 27 | violet #6c71c4 28 | blue #268bd2 29 | cyan #2aa198 30 | green #859900 31 | */ 32 | 33 | code[class*="language-"], 34 | pre[class*="language-"] { 35 | color: #657b83; /* base00 */ 36 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 37 | font-size: 1em; 38 | text-align: left; 39 | white-space: pre; 40 | word-spacing: normal; 41 | word-break: normal; 42 | word-wrap: normal; 43 | 44 | line-height: 1.5; 45 | 46 | -moz-tab-size: 4; 47 | -o-tab-size: 4; 48 | tab-size: 4; 49 | 50 | -webkit-hyphens: none; 51 | -moz-hyphens: none; 52 | -ms-hyphens: none; 53 | hyphens: none; 54 | } 55 | 56 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 57 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 58 | background: #073642; /* base02 */ 59 | } 60 | 61 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 62 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 63 | background: #073642; /* base02 */ 64 | } 65 | 66 | /* Code blocks */ 67 | pre[class*="language-"] { 68 | padding: 1em; 69 | margin: .5em 0; 70 | overflow: auto; 71 | border-radius: 0.3em; 72 | } 73 | 74 | :not(pre) > code[class*="language-"], 75 | pre[class*="language-"] { 76 | background-color: #fdf6e3; /* base3 */ 77 | } 78 | 79 | /* Inline code */ 80 | :not(pre) > code[class*="language-"] { 81 | padding: .1em; 82 | border-radius: .3em; 83 | } 84 | 85 | .token.comment, 86 | .token.prolog, 87 | .token.doctype, 88 | .token.cdata { 89 | color: #93a1a1; /* base1 */ 90 | } 91 | 92 | .token.punctuation { 93 | color: #586e75; /* base01 */ 94 | } 95 | 96 | .namespace { 97 | opacity: .7; 98 | } 99 | 100 | .token.property, 101 | .token.tag, 102 | .token.boolean, 103 | .token.number, 104 | .token.constant, 105 | .token.symbol, 106 | .token.deleted { 107 | color: #268bd2; /* blue */ 108 | } 109 | 110 | .token.selector, 111 | .token.attr-name, 112 | .token.string, 113 | .token.char, 114 | .token.builtin, 115 | .token.url, 116 | .token.inserted { 117 | color: #2aa198; /* cyan */ 118 | } 119 | 120 | .token.entity { 121 | color: #657b83; /* base00 */ 122 | background: #eee8d5; /* base2 */ 123 | } 124 | 125 | .token.atrule, 126 | .token.attr-value, 127 | .token.keyword { 128 | color: #859900; /* green */ 129 | } 130 | 131 | .token.function, 132 | .token.class-name { 133 | color: #b58900; /* yellow */ 134 | } 135 | 136 | .token.regex, 137 | .token.important, 138 | .token.variable { 139 | color: #cb4b16; /* orange */ 140 | } 141 | 142 | .token.important, 143 | .token.bold { 144 | font-weight: bold; 145 | } 146 | .token.italic { 147 | font-style: italic; 148 | } 149 | 150 | .token.entity { 151 | cursor: help; 152 | } 153 | 154 | -------------------------------------------------------------------------------- /app/views/macros/shard-card.j2: -------------------------------------------------------------------------------- 1 | {% macro shard_card(shard, data, categories) %} 2 |
3 | {{ shard_name(shard)}} 4 | 5 | {% if show_description | default(true) and shard.description %} 6 |
7 | {{ shard.description | markdown_inline }} 8 | {% if show_topics %} 9 | {{ list_topics(repo.metadata.topics) }} 10 | {% endif %} 11 |
12 | {% elif show_topics %} 13 |
14 | {{ list_topics(repo.metadata.topics) }} 15 |
16 | {% endif %} 17 | 18 |
19 |
20 | {% if released_at and version %} 21 | {{ icon("tag") }} 22 | {{ version }} 23 | {{ release_date(released_at) }} 24 | {% endif %} 25 | {% if show_created_at %} 26 | {{ icon("tag") }} 27 | {{ data.version }} 28 | {{ release_date(data.created_at) }} 29 | {% endif %} 30 |
31 |
32 | 33 | {% if repo_owner %} 34 | 35 | {{ owner_image(repo_owner) }} 36 | {{ repo_owner.name | default(repo_owner.slug) }} 37 | 38 | {% elif repo_ref %} 39 | 42 | {% endif %} 43 | 44 |
45 | {% for category in categories %} 46 | {{ category.name }} 47 | {% endfor %} 48 |
49 | 50 |
51 | {% if show_dependents | default(data.dependents_count is defined) %} 52 |
53 | dependents 54 | {{ icon("package-dependents") }} {{ data.dependents_count }} 55 |
56 | {% endif %} 57 | {% if show_transitive_dependents | default(data.transitive_dependents_count is defined) %} 58 |
59 | total dependents 60 | {{ icon("package-dependents") }}{{ icon("plus") }} {{ data.transitive_dependents_count }} 61 |
62 | {% endif %} 63 | {% if show_dev_dependents | default(data.dev_dependents_count is defined) %} 64 |
65 | dev dependents 66 | {{ icon("package-dependents") }}{{ icon("code") }} {{ data.dev_dependents_count }} 67 |
68 | {% endif %} 69 | {% if shard.archived %} 70 |
71 | Archived 72 | {{ time_date(shard.archived_at, title = "Archived on {{date}}")}} 73 |
74 | {% endif %} 75 | 76 | {% if repo and repo.sync_failed_at %} 77 |
78 | Sync failed 79 | {{ time_date(repo.sync_failed_at, title = "Sync failed on {{date}}")}} 80 |
81 | {% endif %} 82 |
83 |
84 | {% endmacro %} 85 | 86 | {% macro list_topics(topics) %} 87 | {% if topics %} 88 | 89 | {% for topic in topics if topic != 'crystal' and topic != 'crystal-lang' and topic != 'crystal-language' %} 90 | {{ topic }} 91 | {% endfor %} 92 | 93 | {% endif %} 94 | {% endmacro %} 95 | -------------------------------------------------------------------------------- /app/views/releases/dependencies.html.j2: -------------------------------------------------------------------------------- 1 | {% extends "releases/_layout.html.j2" %} 2 | fooo 3 | 4 | {% block page_title %}{{ shard.display_name }}@{{ release.version }} on Shardbox{% endblock %} 5 | 6 | {% block release_main %} 7 | {% set libraries = release.spec["libraries"] %} 8 | {% if libraries %} 9 |
10 |

Libraries {{ libraries | count }}

11 | 16 |
17 | {% endif %} 18 | 19 |
20 |

21 | {{ icon("package-dependencies") }} Dependencies 22 | {{ dependencies | length }} 23 |

24 | 44 |
45 | 46 |
47 |

48 | {{ icon("package-dependencies") }}{{ icon("code") }} Development Dependencies 49 | {{ dev_dependencies | length }} 50 |

51 | 82 |
83 | 84 |
85 |

86 | {{ icon("package-dependents") }} Dependents 87 | {{ all_dependents | length }} 88 |

89 | 106 |
107 | {% endblock %} 108 | -------------------------------------------------------------------------------- /app/views/releases/_dependencies.html.j2: -------------------------------------------------------------------------------- 1 | 2 | {% set libraries = release.spec["libraries"] %} 3 | {% if libraries %} 4 |
5 |

{{ icon("package") }} Libraries {{ libraries | count }}

6 | 11 |
12 | {% endif %} 13 | 14 |
15 |

{{ icon("package-dependencies") }} Dependencies {{ dependencies | length }}

16 | 33 |
34 | 35 |
36 |

{{ icon("package-dependencies") }}{{ icon("code") }} Development Dependencies {{ dev_dependencies | length }}

37 | 62 |
63 | 64 |
65 |

{{ icon("package-dependents") }} Dependents {{ all_dependents | length }}

66 | 78 | {% if remaining_dependents_count > 0 %} 79 | Show all {{ all_dependents | length }} dependents 80 | {% endif %} 81 |
82 | 83 | {% if mirrors | length > 0 %} 84 |
85 |

{{ icon("repo") }} Other repos {{ mirrors | length }}

86 | 102 |
103 | {% endif %} 104 | 105 | {% if homonymous_shards | length > 0 %} 106 |
107 |

Similar shards

108 | 116 |
117 | {% endif %} 118 | -------------------------------------------------------------------------------- /app/sass/_typography.sass: -------------------------------------------------------------------------------- 1 | * 2 | box-sizing: border-box 3 | 4 | html 5 | font-family: Roboto 6 | color: var(--color-body) 7 | font-size: 13pt 8 | background: #f8f8f8 9 | line-height: 1.4 10 | 11 | body 12 | margin: 0 13 | padding: 0 14 | 15 | $main-margin: 3rem 16 | 17 | %max-width 18 | max-width: 80rem 19 | 20 | main 21 | margin: 0 0 3rem 22 | @media(min-width: 1024px) 23 | margin: 0 $main-margin / 3 3rem 24 | @media(min-width: 1200px) 25 | margin: 0 $main-margin 3rem 26 | 27 | a 28 | text-decoration: none 29 | 30 | &:link, &:visited 31 | color: var(--color-link) 32 | 33 | &:hover, &:focus 34 | color: var(--color-link-hover) 35 | 36 | &.btn 37 | padding: .3em .5em 38 | display: inline-block 39 | border: 2px solid transparent 40 | border-radius: .3em 41 | background-color: var(--color-light) 42 | color: #fff 43 | font-weight: 100 44 | 45 | &:hover, &:focus 46 | background: #ccc 47 | color: var(--color-light) 48 | 49 | &-inline 50 | padding: .1em .2em 51 | font-size: 89% 52 | 53 | &-inverted 54 | background: #ccc 55 | color: var(--color-light) 56 | &:hover 57 | background-color: var(--color-light) 58 | color: #fff 59 | 60 | &.help 61 | display: inline-block 62 | border-radius: 50% 63 | font-weight: 700 64 | margin: .2em 65 | width: 1.4em 66 | line-height: 1.3 67 | text-align: center 68 | background-color: #ddd 69 | color: var(--color-lighter) 70 | border: 2px solid var(--color-lighter) 71 | transform: scale(0.8) 72 | 73 | &:hover, &:focus 74 | background-color: var(--color-lighter) 75 | color: #fff 76 | 77 | &.legal-link 78 | --color-link: var(--color-light-hover) 79 | --color-link-hover: var(--color-light-link) 80 | font-weight: 300 81 | 82 | &.external 83 | --color-link: var(--color-light-link) 84 | --color-link-hover: var(--color-light-hover) 85 | 86 | 87 | &.category-link 88 | display: inline-block 89 | padding: .1em .5em 90 | margin: 0 .5em .5em 0 91 | white-space: nowrap 92 | border-radius: .3em 93 | text-decoration: none 94 | font-weight: 500 95 | //background: hsl(230, 40%, 98%) 96 | background-color: hsl(245, 30%, 93%) 97 | color: hsl(248, 25%, 40%) 98 | transition: color .2s ease-in-out, background-color .2s ease-in-out 99 | 100 | &:hover, &:focus 101 | background: hsl(230, 40%, 95%) 102 | color: #fff 103 | --color-link-hover: hsl(245, 60%, 50%) 104 | background-color: #66c 105 | background-color: hsl(245, 30%, 70%) 106 | h1 107 | font-weight: 500 108 | font-size: 5em 109 | margin: 0 110 | 111 | pre 112 | white-space: pre-wrap 113 | background: var(--color-code-background) 114 | padding: .5em 115 | color: var(--color-code) 116 | 117 | header 118 | margin: 2rem 0 119 | 120 | h2 121 | > .count 122 | font-weight: normal 123 | color: var(--color-light) 124 | 125 | h2, 126 | .release-body h1 127 | font-size: 2rem 128 | font-weight: 300 129 | color: var(--color-light) 130 | 131 | margin-bottom: .5rem 132 | margin-top: 1rem 133 | 134 | .release-body h2 135 | font-size: 1.5rem 136 | font-weight: 300 137 | color: var(--color-light) 138 | 139 | margin-bottom: .5rem 140 | margin-top: 1rem 141 | 142 | h3 143 | font-size: 1.5rem 144 | 145 | .repo-ref 146 | --color-link: var(--color-light-link) 147 | --color-link-hover: var(--color-light-hover) 148 | font-weight: 400 149 | white-space: nowrap 150 | 151 | > .icon 152 | font-size: 80% 153 | margin-top: 0 154 | 155 | .avatar 156 | vertical-align: middle 157 | height: 1.4em 158 | border-radius: 50% 159 | 160 | .shard-name, .dependency-name 161 | font-weight: 500 162 | text-decoration: none 163 | 164 | .topic 165 | display: inline-block 166 | font-weight: 400 167 | &:before 168 | content: "#" 169 | 170 | .large 171 | font-size: 1.5rem 172 | 173 | .fat 174 | font-weight: 500 175 | 176 | .icon 177 | display: inline-block 178 | width: 1em 179 | height: 1em 180 | vertical-align: middle 181 | fill: currentColor 182 | margin-top: -.2em 183 | 184 | + .icon 185 | margin-left: -0.4em 186 | 187 | .shard-name 188 | --color-link: #333 189 | --color-link-hover: var(--color-light) 190 | white-space: nowrap 191 | font-weight: 500 192 | 193 | .qualifier 194 | font-weight: 300 195 | color: var(--color-light) 196 | font-size: 95% 197 | 198 | .description 199 | --color-link: var(--color-light-link) 200 | --color-link-hover: var(--color-light-hover) 201 | 202 | p 203 | margin: 0 204 | 205 | &:not(:empty) + p:not(:empty) 206 | margin-top: .7rem 207 | 208 | img 209 | max-width: 100% 210 | 211 | .percent 212 | color: var(--color-lighter) 213 | font-weight: 300 214 | 215 | .badge 216 | display: inline-block 217 | margin-top: -1px 218 | margin-bottom: -1px 219 | padding: 3px 4px 220 | line-height: 1 221 | border: 1px solid 222 | font-weight: 400 223 | border-radius: 2px 224 | 225 | %monospace 226 | font-family: "Roboto Mono", monospace 227 | -------------------------------------------------------------------------------- /app/views/releases/_header.html.j2: -------------------------------------------------------------------------------- 1 | {% import "macros/shard-card.j2" %} 2 | 3 |
4 |

5 | {{ shard_name(shard) }} 6 |

7 | 8 | {% set description = shard.description | default(release.description) | default(repo.metadata.description) %} 9 | {% if description %} 10 |
11 | {{description | markdown_inline }} 12 | 13 | {{ list_topics(repo.metadata.topics) }} 14 |
15 | {% endif %} 16 | 17 |
18 | {% for category in categories %} 19 | {{ category.name }} 20 | {% else %} 21 | Help categorize this shard! 22 | {% endfor %} 23 |
24 | 25 |
26 | 27 | {{ icon("tag") }} 28 | {{ release.version }} 29 | {% if release.latest %} 30 | Latest release 31 | {% endif %} 32 | {% if release.yanked_at %} 33 | Yanked release 34 | {% endif %} 35 | released {{ release_date(release.released_at) }} 36 | 37 |
38 | 39 |
40 |
41 | {{ repo_link(repo.ref) }} 42 | 43 | {% set latest_activity = repo.metadata.pushed_at %} 44 | {% if latest_activity %} 45 |
46 | {{ time_date(latest_activity, title="Latest activity on {{date}}") }} 47 |
48 | {% endif %} 49 |
50 | {% set stargazers_count = repo.metadata.stargazers_count %} 51 | {% if stargazers_count %} 52 | 53 | {{ stargazers_count }} {{ icon("star") }} 54 | 55 | {% endif %} 56 | 57 | {% set forks_count = repo.metadata.forks_count %} 58 | {% if forks_count %} 59 | 60 | {{ forks_count }} {{ icon("repo-forked" ) }} 61 | 62 | {% endif %} 63 | 64 | {% set open_issues_count = repo.metadata.open_issues_count %} 65 | {% if open_issues_count %} 66 | 67 | {{ open_issues_count }} {{ icon("issue") }} 68 | 69 | {% endif %} 70 |
71 |
72 | 73 | {% if repo_owner %} 74 | 75 | {{ owner_image(repo_owner) }} 76 | {{ repo_owner.name | default(repo_owner.slug) }} 77 | 78 | {% endif %} 79 |
80 | 81 | 102 |
103 | 104 |
105 | {% if shard.archived %} 106 |
107 |

Archived shard

108 |

109 | This shard has been archived {{ time_date(shard.archived_at, title = "Archived on {{date}}")}}. 110 | It is no longer maintained or has been discontinued for other reasons. 111 |

112 |
113 | {% endif %} 114 | 115 | {% if repo.sync_failed_at %} 116 |
117 |

118 | This repo seems to be {% if repo.synced_at %}no longer{% else %}not{% endif %} available at {{ repo_link(repo.ref) }}. 119 |

120 | 121 |

122 | {% if repo.synced_at %} 123 | Git synchronization failed {{ time_date(repo.sync_failed_at, title = "Synchronization failed on {{date}}") }}. 124 | Last successful sync was {{ time_date(repo.synced_at, title = "Last successful sync on {{date}}") }}. 125 | {% else %} 126 | This repo could never be reached at the given location. 127 | First unsuccessful sync was {{ time_date(repo.sync_failed_at, title = "Synchronization failed on {{date}}") }}. 128 | {% endif %} 129 |

130 | 131 | Help find it again! … or have it archived. 132 |
133 | {% endif %} 134 |
135 | -------------------------------------------------------------------------------- /src/app.cr: -------------------------------------------------------------------------------- 1 | require "kemal" 2 | require "crinja" 3 | require "baked_file_system" 4 | require "humanize_time" 5 | require "shardbox-core/db" 6 | require "shardbox-core/repo" 7 | require "./db" 8 | require "./raven" 9 | require "./crinja_models" 10 | require "./crinja_lib" 11 | require "./page" 12 | require "./page/*" 13 | require "./assets" 14 | require "./api" 15 | 16 | def crinja 17 | Page.crinja 18 | end 19 | 20 | get "/" do |context| 21 | ShardsDB.connect do |db| 22 | recent_shards = db.recent_shards 23 | dependent_shards = db.dependent_shards 24 | popular_shards = db.popular_shards 25 | dev_dependent_shards = db.dependent_shards(:development) 26 | 27 | template = crinja.get_template("home.html.j2") 28 | template.render({ 29 | "recent_shards" => recent_shards, 30 | "dependent_shards" => dependent_shards, 31 | "popular_shards" => popular_shards, 32 | "dev_dependent_shards" => dev_dependent_shards, 33 | "new_shards" => db.new_shards, 34 | "stats" => db.stats, 35 | }) 36 | end 37 | end 38 | 39 | get "/stats" do |context| 40 | ShardsDB.connect do |db| 41 | template = crinja.get_template("stats.html.j2") 42 | template.render({ 43 | "stats" => db.stats, 44 | }) 45 | end 46 | end 47 | 48 | get "/categories" do |context| 49 | ShardsDB.connect do |db| 50 | template = crinja.get_template("categories/index.html.j2") 51 | template.render({ 52 | "categories" => db.all_categories, 53 | "top_shards" => db.all_categories_top_shards, 54 | }) 55 | end 56 | end 57 | 58 | get "/categories/:slug" do |context| 59 | slug = context.params.url["slug"] 60 | ShardsDB.connect do |db| 61 | category = db.find_category(slug) 62 | 63 | unless category 64 | halt context, 404 65 | end 66 | 67 | page = Page::Category.new(db, category) 68 | page.render(context.response) 69 | nil 70 | end 71 | end 72 | 73 | get "/shards/:name" do |context| 74 | show_release(context) 75 | end 76 | 77 | # Redirect /shards/:name/:version to /shards/releases/:version 78 | get "/shards/:name/:version" do |context| 79 | # only redirect when version looks like a version 80 | unless context.params.url["version"] =~ /^\d+\.\d/ 81 | halt context, 404 82 | next 83 | end 84 | 85 | ShardsDB.connect do |db| 86 | release = Page::Shard.find_release(db, context) 87 | case release 88 | when String 89 | halt context, 404, release 90 | when Nil 91 | next 92 | else 93 | # Found release, redirect to /releases/:version path 94 | context.redirect "/shards/#{context.params.url["name"]}/releases/#{context.params.url["version"]}" 95 | end 96 | end 97 | end 98 | 99 | get "/shards/:name/releases/:version" do |context| 100 | show_release(context) 101 | end 102 | 103 | get "/shards/:name/releases" do |context| 104 | ShardsDB.connect do |db| 105 | page = Page::Shard.new(db, context, "releases") 106 | case page 107 | when String 108 | halt context, 404, page 109 | when Nil 110 | next 111 | when Page::Shard 112 | page.render(context.response) 113 | nil 114 | end 115 | end 116 | end 117 | 118 | get "/shards/:name/activity" do |context| 119 | ShardsDB.connect do |db| 120 | page = Page::Shard.new(db, context, "activity") 121 | case page 122 | when String 123 | halt context, 404, page 124 | when Nil 125 | next 126 | when Page::Shard 127 | page.render(context.response) 128 | nil 129 | end 130 | end 131 | end 132 | 133 | get "/shards/:name/releases/:version/dependencies" do |context| 134 | ShardsDB.connect do |db| 135 | page = Page::Shard.new(db, context, "dependencies") 136 | case page 137 | when String 138 | halt context, 404, page 139 | when Nil 140 | next 141 | when Page::Shard 142 | page.render(context.response) 143 | nil 144 | end 145 | end 146 | end 147 | 148 | get "/owners/" do |context| 149 | ShardsDB.connect do |db| 150 | template = crinja.get_template("owners/index.html.j2") 151 | template.render({ 152 | "owners" => db.get_owners, 153 | }) 154 | end 155 | end 156 | 157 | get "/owners/:resolver/:slug" do |context| 158 | ShardsDB.connect do |db| 159 | page = Page::Owner.new(db, context) 160 | case page 161 | when String 162 | halt context, 404, page 163 | when Nil 164 | next 165 | when Page 166 | page.render(context.response) 167 | nil 168 | end 169 | end 170 | end 171 | 172 | get "/deploy_status" do 173 | "OK" 174 | end 175 | 176 | get "/contribute" do 177 | template = crinja.get_template("pages/contribute.html.j2") 178 | template.render 179 | end 180 | 181 | get "/imprint" do 182 | template = crinja.get_template("pages/imprint.html.j2") 183 | template.render 184 | end 185 | 186 | get "/search" do |context| 187 | query = context.request.query_params["q"]? || "" 188 | 189 | ShardsDB.connect do |db| 190 | shards = db.search(query) 191 | 192 | template = crinja.get_template("search.html.j2") 193 | template.render({ 194 | "query" => query, 195 | "shards" => shards, 196 | }) 197 | end 198 | end 199 | 200 | get "/webhook/import_catalog" do |context| 201 | secret = ENV["SHARDBOX_SECRET"]? 202 | unless secret 203 | halt context, status_code: HTTP::Status::NOT_FOUND.value 204 | end 205 | 206 | auth = context.request.headers["Authorization"]? 207 | unless auth 208 | context.response.headers["WWW-Authenticate"] = %[Basic realm="Webhook Authentication"] 209 | halt context, status_code: HTTP::Status::UNAUTHORIZED.value 210 | end 211 | 212 | unless auth == "Basic #{secret}" 213 | halt context, status_code: HTTP::Status::FORBIDDEN.value 214 | end 215 | 216 | ShardsDB.connect do |db| 217 | db.send_job_notification("import_catalog") 218 | end 219 | end 220 | 221 | def show_release(context) 222 | ShardsDB.connect do |db| 223 | page = Page::Shard.new(db, context, "readme") 224 | case page 225 | when String 226 | halt context, 404, page 227 | when Nil 228 | next 229 | when Page::Shard 230 | page.context["readme"] = db.fetch_file(page.release.id, "README.md") 231 | page.render(context.response, "releases/show.html.j2") 232 | nil 233 | end 234 | end 235 | end 236 | -------------------------------------------------------------------------------- /app/sass/pages/release.sass: -------------------------------------------------------------------------------- 1 | .release-header, 2 | .release-notices, 3 | .release-wrapper 4 | @extend %max-width 5 | margin: 0 auto 6 | 7 | .release-header 8 | margin-bottom: 2rem 9 | padding: 2.1rem 2rem 1.5rem 10 | border-bottom: .2rem solid #333 11 | background: #fff 12 | display: grid 13 | grid-template-columns: auto max-content 14 | grid-template-areas: "categories repo-pane" "name repo-pane" "description repo-pane" "shard-info links" 15 | 16 | > .shard-title 17 | grid-area: name 18 | font-size: 4.2em 19 | 20 | > .shard-description 21 | grid-area: description 22 | margin-bottom: .5rem 23 | 24 | > p 25 | display: inline 26 | font-size: 1.15em 27 | 28 | > .categories 29 | grid-area: categories 30 | margin-bottom: .7rem 31 | 32 | > .repo-pane 33 | grid-area: repo-pane 34 | display: flex 35 | align-items: flex-end 36 | flex-direction: column 37 | max-width: 20em 38 | 39 | > .shard-info 40 | grid-area: shard-info 41 | font-size: 1.2rem 42 | margin-top: .5rem 43 | 44 | > .links 45 | grid-area: links 46 | display: flex 47 | align-items: flex-end 48 | justify-content: flex-end 49 | 50 | > a 51 | margin: 0 .1em 52 | white-space: nowrap 53 | 54 | .repo-info 55 | display: flex 56 | align-items: flex-end 57 | flex-direction: column 58 | 59 | .owner-link 60 | display: inline-flex 61 | margin-top: 1em 62 | align-items: center 63 | 64 | > .owner_image 65 | width: 1.5em 66 | border-radius: 50% 67 | margin-right: .4em 68 | flex-shrink: 0 69 | 70 | > .owner_name 71 | white-space: nowrap 72 | 73 | .release-wrapper 74 | display: flex 75 | 76 | .release-main 77 | width: 60% 78 | flex-grow: 1 79 | 80 | > .tabs 81 | @media(max-width: 1024px) 82 | padding-left: .3em 83 | 84 | .release-body 85 | background: #ffffff 86 | overflow: hidden 87 | 88 | > .container 89 | margin: 2em 2em 3em 90 | 91 | .release-sidebar 92 | margin-left: 2rem 93 | width: 32% 94 | max-width: 25em 95 | 96 | > .container 97 | margin-top: 1rem 98 | margin-bottom: 1rem 99 | 100 | &.category-edit 101 | margin-top: 3rem 102 | margin-left: 1rem 103 | margin-right: 1rem 104 | 105 | .shards-spec 106 | // background-color: #f8f8f6 107 | padding: 1em .8em 108 | border: 1px solid #eee 109 | white-space: pre 110 | font-weight: 600 111 | color: var(--color-light) 112 | word-break: break-word 113 | white-space: pre-wrap 114 | position: relative 115 | font-family: monospace 116 | 117 | > .copy-code 118 | position: absolute 119 | top: 0 120 | right: 0 121 | 122 | &:hover 123 | 124 | 125 | .infobox > & 126 | background-color: #e4e4e4 127 | margin-bottom: 0 128 | 129 | &::before 130 | position: absolute 131 | left: 0 132 | right: 0 133 | top: 0 134 | bottom: 0 135 | content: "COPIED" 136 | text-align: center 137 | line-height: 6rem 138 | font-size: 3em 139 | background: rgba(0,0,0,.5) 140 | color: #ddd 141 | opacity: 0 142 | transition: opacity .2s ease-in-out 143 | display: none 144 | 145 | &.copied::before 146 | opacity: 1 147 | display: block 148 | transition: opacity .4s ease-in-out 149 | 150 | .token.atrule 151 | color: #666 152 | 153 | .dependencies 154 | 155 | pre 156 | display: none 157 | 158 | span.dependency-name 159 | font-style: italic 160 | 161 | &:not(.dependencies--infobox) > li + li 162 | margin-top: .3em 163 | 164 | .shard-description 165 | font-size: 94% 166 | line-height: 1.4 167 | color: var(--color-light) 168 | font-weight: 300 169 | display: block 170 | 171 | .list-dependents 172 | list-style: none 173 | margin: 0 174 | padding: 0 175 | line-height: 1.6 176 | 177 | > li 178 | display: inline-block 179 | padding-right: .3em 180 | 181 | .infobox 182 | background-color: #ffffff 183 | padding-bottom: .7em 184 | 185 | > .container 186 | padding: .7rem 1rem .3rem 1.3rem 187 | 188 | > h3 189 | font-weight: 300 190 | font-size: 1rem 191 | margin: 0 192 | margin-left: -0.5rem 193 | 194 | > ul 195 | margin: 0 196 | padding: 0 197 | > li 198 | list-style: none 199 | > .category-link 200 | display: block 201 | > .metrics 202 | > .metric 203 | > .metric-label 204 | font-size: 90% 205 | 206 | .shard-name.shard-title 207 | .qualifier 208 | font-weight: 250 209 | font-size: 82% 210 | 211 | .list-releases 212 | list-style: none 213 | margin: 0 214 | padding: 0 215 | line-height: 1.5 216 | display: grid 217 | grid-template-columns: 1fr auto 2fr 218 | 219 | > li 220 | display: contents 221 | 222 | &.current > a > * 223 | background-color: #eee 224 | > a 225 | display: contents 226 | justify-content: space-between 227 | text-decoration: none 228 | line-height: 1.5rem 229 | 230 | > * 231 | border-bottom: 1px dashed #ccc 232 | padding: 0 .2rem 233 | 234 | > .release-version 235 | color: #333 236 | font-weight: 500 237 | > .release-date 238 | color: var(--color-light) 239 | @extend %monospace 240 | > .release-span 241 | color: var(--color-lighter) 242 | font-size: 90% 243 | 244 | &:hover, &:focus 245 | > * 246 | background-color: #F4F4F4 247 | 248 | .shard-notice 249 | margin: 1rem 250 | padding: 1rem 251 | &__archived 252 | background: var(--color-archived) 253 | &__sync_failed_at 254 | background: var(--color-sync-failed) 255 | 256 | > p:first-child 257 | margin-top: 0 258 | 259 | .dependency-version 260 | @extend %monospace 261 | font-size: 90% 262 | color: var(--color-light) 263 | 264 | .recursive_dependents 265 | font-size: 90% 266 | color: var(--color-light) 267 | 268 | .release-nav 269 | display: flex 270 | flex-direction: row 271 | margin-top: -3rem 272 | margin-bottom: 2rem 273 | 274 | > a 275 | display: block 276 | padding: .4em .3em .2em 277 | background: #eee 278 | margin: 0 .2em 279 | text-decoration: none 280 | --color-link: var(--color-lighter) 281 | --color-hover: var(--color-light-hover) 282 | 283 | &.active 284 | background-color: #fff 285 | color: var(--color-light) 286 | font-weight: bold 287 | 288 | .repo-activity, .community-stats 289 | font-size: 90% 290 | color: var(--color-lighter) 291 | 292 | .community-stats 293 | display: flex 294 | flex-direction: row 295 | justify-content: flex-end 296 | margin-top: .2em 297 | 298 | > span 299 | padding: .1em .3em 300 | white-space: nowrap 301 | line-height: 1em 302 | 303 | .homonymous-shard 304 | display: flex 305 | flex-direction: column 306 | align-items: flex-start 307 | 308 | .author 309 | @extend .with-image 310 | margin-top: .4em 311 | 312 | &_image 313 | border-radius: 50% 314 | 315 | &_mail 316 | font-size: 90% 317 | -------------------------------------------------------------------------------- /src/page/shard.cr: -------------------------------------------------------------------------------- 1 | @[Crinja::Attributes] 2 | struct Page::Shard 3 | include Page 4 | 5 | NUM_RELEASES_SHOWN = 8 6 | NUM_DEPENDENTS_SHOWN = 10 7 | 8 | def self.new(db, context, name) 9 | result = find_release(db, context) 10 | return result if result.nil? || result.is_a?(String) 11 | 12 | new db, *result, name 13 | end 14 | 15 | getter db : ShardsDB 16 | getter shard : ::Shard 17 | getter all_releases : Array(Release) 18 | getter name : String 19 | @release : Release? 20 | 21 | def initialize(@db, @shard, @all_releases, @release, @name) 22 | end 23 | 24 | def release? 25 | @release 26 | end 27 | 28 | def release 29 | release? || default_release || raise "Shard #{shard.slug} has no releases" 30 | end 31 | 32 | def default_release 33 | all_releases.find(&.latest?) || all_releases.last? 34 | end 35 | 36 | def canonical_repo 37 | db.find_canonical_repo(shard.id) 38 | end 39 | 40 | def mirrors 41 | db.find_mirror_repos(shard.id) 42 | end 43 | 44 | def categories 45 | db.find_categories(shard.id) 46 | end 47 | 48 | def dependencies 49 | db.dependencies(release.id, :runtime) 50 | end 51 | 52 | def dev_dependencies 53 | db.dependencies(release.id, :development) 54 | end 55 | 56 | def owner 57 | db.get_owner?(canonical_repo.ref) 58 | end 59 | 60 | def source_url 61 | canonical_repo.ref.base_url_source(release.revision_info.commit.sha) 62 | end 63 | 64 | def path 65 | "/shards/#{shard.slug}" 66 | end 67 | 68 | private def initialize_context(context) 69 | context["release"] = release 70 | context["shard"] = shard 71 | context["releases"] = all_releases.first(NUM_RELEASES_SHOWN) 72 | context["all_releases"] = all_releases 73 | context["remaining_releases_count"] = Math.max(0, all_releases.size - NUM_RELEASES_SHOWN) 74 | 75 | context["dependencies"] = dependencies 76 | context["dev_dependencies"] = dev_dependencies 77 | 78 | dependents = db.dependents(shard.id) 79 | context["all_dependents"] = dependents 80 | context["dependents"] = dependents.first(NUM_DEPENDENTS_SHOWN) 81 | context["remaining_dependents_count"] = Math.max(0, dependents.size - NUM_DEPENDENTS_SHOWN) 82 | 83 | repo = canonical_repo 84 | context["repo"] = repo 85 | context["repo_owner"] = owner 86 | context["source_url"] = source_url 87 | context["metrics"] = db.get_current_metrics(shard.id) 88 | context["mirrors"] = mirrors 89 | 90 | context["homonymous_shards"] = db.find_homonymous_shards(shard.name).reject { |s| s[:shard].id == shard.id } 91 | 92 | context["categories"] = categories 93 | 94 | case @name 95 | when "activity" 96 | context["activities"] = db.get_activity(shard.id).group_by(&.created_at).values.reverse 97 | else 98 | end 99 | end 100 | 101 | def render(io) 102 | render(io, "releases/#{name}.html.j2") 103 | end 104 | 105 | def to_json(json : JSON::Builder) 106 | json.object do 107 | json.field "name", shard.name 108 | json.field "qualifier", shard.qualifier 109 | json.field "display_name", shard.display_name 110 | json.field "canonical_repo", canonical_repo.ref.to_uri.to_s 111 | json.field "description", shard.description 112 | json.field "categories" do 113 | json.array do 114 | categories.each do |category| 115 | category.slug.to_json(json) 116 | end 117 | end 118 | end 119 | json.field "archived_at", shard.archived_at if shard.archived_at 120 | if owner = self.owner 121 | json.field "owner" do 122 | json.object do 123 | json.field "resolver", owner.resolver 124 | json.field "slug", owner.slug 125 | json.field "name", owner.name 126 | json.field "description", owner.description if owner.description.presence 127 | json.field "website_url", owner.website_url if owner.website_url 128 | end 129 | end 130 | end 131 | json.field "latest_release" do 132 | default_release.to_json(json) 133 | end 134 | json.field "source_url", source_url.to_s if source_url 135 | json.field "mirrors" do 136 | json.array do 137 | mirrors.each do |mirror| 138 | mirror.ref.to_uri.to_s.to_json(json) 139 | end 140 | end 141 | end 142 | end 143 | end 144 | 145 | def self.find_release(db, context) 146 | name = context.params.url["name"] 147 | name, _, qualifier = name.partition('~') 148 | 149 | shard = db.find_shard?(name, qualifier) 150 | 151 | unless shard 152 | unqualified_shard = db.find_shard?(name, "") 153 | 154 | if unqualified_shard 155 | context.redirect "/shards/#{name}", 301 156 | return 157 | else 158 | return "Shard not found" 159 | end 160 | end 161 | 162 | if merged_with = shard.merged_with 163 | main_shard = db.get_shard(merged_with) 164 | context.redirect "/shards/#{main_shard.display_name}", 301 165 | return 166 | end 167 | 168 | releases = db.all_releases(shard.id) 169 | 170 | version = context.params.url["version"]? 171 | if version 172 | release = releases.find { |r| r.version == version } 173 | 174 | unless release 175 | return "Release not available" 176 | end 177 | else 178 | release = nil 179 | 180 | if releases.empty? 181 | return "Shard has no releases" 182 | end 183 | end 184 | 185 | return shard, releases, release 186 | end 187 | end 188 | 189 | class Release 190 | def spec_authors 191 | authors = [] of Author 192 | specs = spec["authors"]?.try(&.as_a?) 193 | return authors unless specs 194 | 195 | specs.each do |s| 196 | authors << Author.new(s.as_s) 197 | end 198 | 199 | authors 200 | end 201 | 202 | def to_json(json : JSON::Builder) 203 | json.object do 204 | json.field "version", version 205 | json.field "released_at", released_at 206 | json.field "revision_identifier", revision_identifier 207 | json.field "commit_hash", commit_hash 208 | json.field "revision_info", revision_info 209 | json.field "spec" do 210 | json.object do 211 | json.field "description", description 212 | json.field "license", license 213 | json.field "crystal", crystal 214 | json.field "authors", spec_authors 215 | end 216 | end 217 | end 218 | end 219 | 220 | def commit_hash : String? 221 | revision_info.try(&.commit.sha) 222 | end 223 | end 224 | 225 | struct Author 226 | getter name : String 227 | getter email : String? 228 | 229 | def initialize(@name : String) 230 | if name =~ /\A\s*(.+?)\s*<+(\s*.+?\s*)>/ 231 | @name, @email = $1, $2 232 | else 233 | @name = name 234 | end 235 | end 236 | 237 | def to_json(json : JSON::Builder) 238 | json.object do 239 | json.field "name", name 240 | json.field "email", email if email 241 | end 242 | end 243 | end 244 | 245 | struct Repo::Ref 246 | def crystaldoc_info_url(release) 247 | uri = to_uri 248 | 249 | org_repo_path = self.class.extract_org_repo_url(uri) || return 250 | 251 | # This is the hostname transformation from crystaldoc.info 252 | # https://github.com/nobodywasishere/crystaldoc.info/blob/ee083b9b1f1800c63e73862ecafe44bb17fc6709/src/crystaldoc/vcs.cr#L72 253 | service = uri.host.try &.rchop(".com").sub('.', '-') || return 254 | 255 | "https://crystaldoc.info/#{service}/#{org_repo_path}/v#{release.version}" 256 | end 257 | 258 | def self.extract_org_repo_url(uri) 259 | path = uri.path.not_nil!.strip('/').rchop(".git") 260 | if path.count('/') == 1 261 | path 262 | end 263 | end 264 | end 265 | -------------------------------------------------------------------------------- /app/sass/_fonts.css: -------------------------------------------------------------------------------- 1 | /* roboto-300 - latin */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: normal; 5 | font-weight: 300; 6 | src: url('/assets/fonts/roboto-v20-latin-300.eot'); /* IE9 Compat Modes */ 7 | src: local('Roboto Light'), local('Roboto-Light'), 8 | url('/assets/fonts/roboto-v20-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 9 | url('/assets/fonts/roboto-v20-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ 10 | url('/assets/fonts/roboto-v20-latin-300.woff') format('woff'), /* Modern Browsers */ 11 | url('/assets/fonts/roboto-v20-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */ 12 | url('/assets/fonts/roboto-v20-latin-300.svg#Roboto') format('svg'); /* Legacy iOS */ 13 | } 14 | 15 | /* roboto-300italic - latin */ 16 | @font-face { 17 | font-family: 'Roboto'; 18 | font-style: italic; 19 | font-weight: 300; 20 | src: url('/assets/fonts/roboto-v20-latin-300italic.eot'); /* IE9 Compat Modes */ 21 | src: local('Roboto Light Italic'), local('Roboto-LightItalic'), 22 | url('/assets/fonts/roboto-v20-latin-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 23 | url('/assets/fonts/roboto-v20-latin-300italic.woff2') format('woff2'), /* Super Modern Browsers */ 24 | url('/assets/fonts/roboto-v20-latin-300italic.woff') format('woff'), /* Modern Browsers */ 25 | url('/assets/fonts/roboto-v20-latin-300italic.ttf') format('truetype'), /* Safari, Android, iOS */ 26 | url('/assets/fonts/roboto-v20-latin-300italic.svg#Roboto') format('svg'); /* Legacy iOS */ 27 | } 28 | 29 | /* roboto-regular - latin */ 30 | @font-face { 31 | font-family: 'Roboto'; 32 | font-style: normal; 33 | font-weight: 400; 34 | src: url('/assets/fonts/roboto-v20-latin-regular.eot'); /* IE9 Compat Modes */ 35 | src: local('Roboto'), local('Roboto-Regular'), 36 | url('/assets/fonts/roboto-v20-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 37 | url('/assets/fonts/roboto-v20-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ 38 | url('/assets/fonts/roboto-v20-latin-regular.woff') format('woff'), /* Modern Browsers */ 39 | url('/assets/fonts/roboto-v20-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ 40 | url('/assets/fonts/roboto-v20-latin-regular.svg#Roboto') format('svg'); /* Legacy iOS */ 41 | } 42 | 43 | /* roboto-italic - latin */ 44 | @font-face { 45 | font-family: 'Roboto'; 46 | font-style: italic; 47 | font-weight: 400; 48 | src: url('/assets/fonts/roboto-v20-latin-italic.eot'); /* IE9 Compat Modes */ 49 | src: local('Roboto Italic'), local('Roboto-Italic'), 50 | url('/assets/fonts/roboto-v20-latin-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 51 | url('/assets/fonts/roboto-v20-latin-italic.woff2') format('woff2'), /* Super Modern Browsers */ 52 | url('/assets/fonts/roboto-v20-latin-italic.woff') format('woff'), /* Modern Browsers */ 53 | url('/assets/fonts/roboto-v20-latin-italic.ttf') format('truetype'), /* Safari, Android, iOS */ 54 | url('/assets/fonts/roboto-v20-latin-italic.svg#Roboto') format('svg'); /* Legacy iOS */ 55 | } 56 | 57 | /* roboto-500 - latin */ 58 | @font-face { 59 | font-family: 'Roboto'; 60 | font-style: normal; 61 | font-weight: 500; 62 | src: url('/assets/fonts/roboto-v20-latin-500.eot'); /* IE9 Compat Modes */ 63 | src: local('Roboto Medium'), local('Roboto-Medium'), 64 | url('/assets/fonts/roboto-v20-latin-500.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 65 | url('/assets/fonts/roboto-v20-latin-500.woff2') format('woff2'), /* Super Modern Browsers */ 66 | url('/assets/fonts/roboto-v20-latin-500.woff') format('woff'), /* Modern Browsers */ 67 | url('/assets/fonts/roboto-v20-latin-500.ttf') format('truetype'), /* Safari, Android, iOS */ 68 | url('/assets/fonts/roboto-v20-latin-500.svg#Roboto') format('svg'); /* Legacy iOS */ 69 | } 70 | 71 | /* roboto-500italic - latin */ 72 | @font-face { 73 | font-family: 'Roboto'; 74 | font-style: italic; 75 | font-weight: 500; 76 | src: url('/assets/fonts/roboto-v20-latin-500italic.eot'); /* IE9 Compat Modes */ 77 | src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), 78 | url('/assets/fonts/roboto-v20-latin-500italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 79 | url('/assets/fonts/roboto-v20-latin-500italic.woff2') format('woff2'), /* Super Modern Browsers */ 80 | url('/assets/fonts/roboto-v20-latin-500italic.woff') format('woff'), /* Modern Browsers */ 81 | url('/assets/fonts/roboto-v20-latin-500italic.ttf') format('truetype'), /* Safari, Android, iOS */ 82 | url('/assets/fonts/roboto-v20-latin-500italic.svg#Roboto') format('svg'); /* Legacy iOS */ 83 | } 84 | 85 | /* roboto-700 - latin */ 86 | @font-face { 87 | font-family: 'Roboto'; 88 | font-style: normal; 89 | font-weight: 700; 90 | src: url('/assets/fonts/roboto-v20-latin-700.eot'); /* IE9 Compat Modes */ 91 | src: local('Roboto Bold'), local('Roboto-Bold'), 92 | url('/assets/fonts/roboto-v20-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 93 | url('/assets/fonts/roboto-v20-latin-700.woff2') format('woff2'), /* Super Modern Browsers */ 94 | url('/assets/fonts/roboto-v20-latin-700.woff') format('woff'), /* Modern Browsers */ 95 | url('/assets/fonts/roboto-v20-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */ 96 | url('/assets/fonts/roboto-v20-latin-700.svg#Roboto') format('svg'); /* Legacy iOS */ 97 | } 98 | 99 | /* roboto-700italic - latin */ 100 | @font-face { 101 | font-family: 'Roboto'; 102 | font-style: italic; 103 | font-weight: 700; 104 | src: url('/assets/fonts/roboto-v20-latin-700italic.eot'); /* IE9 Compat Modes */ 105 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), 106 | url('/assets/fonts/roboto-v20-latin-700italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 107 | url('/assets/fonts/roboto-v20-latin-700italic.woff2') format('woff2'), /* Super Modern Browsers */ 108 | url('/assets/fonts/roboto-v20-latin-700italic.woff') format('woff'), /* Modern Browsers */ 109 | url('/assets/fonts/roboto-v20-latin-700italic.ttf') format('truetype'), /* Safari, Android, iOS */ 110 | url('/assets/fonts/roboto-v20-latin-700italic.svg#Roboto') format('svg'); /* Legacy iOS */ 111 | } 112 | 113 | /* roboto-mono-100 - latin */ 114 | @font-face { 115 | font-family: 'Roboto Mono'; 116 | font-style: normal; 117 | font-weight: 100; 118 | src: url('/assets/fonts/roboto-mono-v7-latin-100.eot'); /* IE9 Compat Modes */ 119 | src: local('Roboto Mono Thin'), local('RobotoMono-Thin'), 120 | url('/assets/fonts/roboto-mono-v7-latin-100.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 121 | url('/assets/fonts/roboto-mono-v7-latin-100.woff2') format('woff2'), /* Super Modern Browsers */ 122 | url('/assets/fonts/roboto-mono-v7-latin-100.woff') format('woff'), /* Modern Browsers */ 123 | url('/assets/fonts/roboto-mono-v7-latin-100.ttf') format('truetype'), /* Safari, Android, iOS */ 124 | url('/assets/fonts/roboto-mono-v7-latin-100.svg#RobotoMono') format('svg'); /* Legacy iOS */ 125 | } 126 | 127 | /* roboto-mono-300 - latin */ 128 | @font-face { 129 | font-family: 'Roboto Mono'; 130 | font-style: normal; 131 | font-weight: 300; 132 | src: url('/assets/fonts/roboto-mono-v7-latin-300.eot'); /* IE9 Compat Modes */ 133 | src: local('Roboto Mono Light'), local('RobotoMono-Light'), 134 | url('/assets/fonts/roboto-mono-v7-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 135 | url('/assets/fonts/roboto-mono-v7-latin-300.woff2') format('woff2'), /* Super Modern Browsers */ 136 | url('/assets/fonts/roboto-mono-v7-latin-300.woff') format('woff'), /* Modern Browsers */ 137 | url('/assets/fonts/roboto-mono-v7-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */ 138 | url('/assets/fonts/roboto-mono-v7-latin-300.svg#RobotoMono') format('svg'); /* Legacy iOS */ 139 | } 140 | 141 | /* roboto-mono-500 - latin */ 142 | @font-face { 143 | font-family: 'Roboto Mono'; 144 | font-style: normal; 145 | font-weight: 500; 146 | src: url('/assets/fonts/roboto-mono-v7-latin-500.eot'); /* IE9 Compat Modes */ 147 | src: local('Roboto Mono Medium'), local('RobotoMono-Medium'), 148 | url('/assets/fonts/roboto-mono-v7-latin-500.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 149 | url('/assets/fonts/roboto-mono-v7-latin-500.woff2') format('woff2'), /* Super Modern Browsers */ 150 | url('/assets/fonts/roboto-mono-v7-latin-500.woff') format('woff'), /* Modern Browsers */ 151 | url('/assets/fonts/roboto-mono-v7-latin-500.ttf') format('truetype'), /* Safari, Android, iOS */ 152 | url('/assets/fonts/roboto-mono-v7-latin-500.svg#RobotoMono') format('svg'); /* Legacy iOS */ 153 | } 154 | 155 | /* roboto-mono-regular - latin */ 156 | @font-face { 157 | font-family: 'Roboto Mono'; 158 | font-style: normal; 159 | font-weight: 400; 160 | src: url('/assets/fonts/roboto-mono-v7-latin-regular.eot'); /* IE9 Compat Modes */ 161 | src: local('Roboto Mono'), local('RobotoMono-Regular'), 162 | url('/assets/fonts/roboto-mono-v7-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 163 | url('/assets/fonts/roboto-mono-v7-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ 164 | url('/assets/fonts/roboto-mono-v7-latin-regular.woff') format('woff'), /* Modern Browsers */ 165 | url('/assets/fonts/roboto-mono-v7-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ 166 | url('/assets/fonts/roboto-mono-v7-latin-regular.svg#RobotoMono') format('svg'); /* Legacy iOS */ 167 | } 168 | -------------------------------------------------------------------------------- /app/views/includes/icons.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | search 7 | 8 | 9 | 10 | fire 11 | 12 | 13 | 14 | star 15 | 16 | 17 | 18 | recently 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 74 | 75 | 76 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | -------------------------------------------------------------------------------- /src/db.cr: -------------------------------------------------------------------------------- 1 | record Activity, 2 | event : String, 3 | created_at : Time = Time.utc, 4 | metadata : JSON::Any? = nil, 5 | shard_id : Int64? = nil, 6 | repo_ref : Repo::Ref? = nil, 7 | id : Int64? = nil do 8 | end 9 | 10 | require "shardbox-core/db" 11 | 12 | class ShardsDB 13 | # self.statement_timeout = "30s" 14 | 15 | # HOME 16 | def recent_shards 17 | results = connection.query_all <<-SQL, as: {Int64, String, String, String?, Time?, String, Time, Array(Array(String))?} 18 | SELECT 19 | shards.id, name::text, qualifier::text, shards.description, archived_at, version, released_at, 20 | (SELECT array_agg(ARRAY[categories.slug::text, categories.name::text]) FROM categories WHERE shards.categories @> ARRAY[categories.id]) 21 | FROM shards 22 | JOIN releases ON releases.shard_id = shards.id 23 | WHERE releases.latest = true 24 | AND shards.archived_at IS NULL 25 | ORDER BY releases.released_at DESC 26 | LIMIT 11 27 | SQL 28 | 29 | results.map do |result| 30 | id, name, qualifier, description, archived_at, version, released_at, categories = result 31 | categories ||= [] of Array(String) 32 | categories = categories.map { |(name, slug)| Category.new(name, slug) } 33 | {shard: Shard.new(name, qualifier, description, archived_at, id: id), version: version, released_at: released_at, categories: categories} 34 | end 35 | end 36 | 37 | def new_shards 38 | results = connection.query_all <<-SQL, as: {Int64, String, String, String?, Time?, String, Time, String, String} 39 | WITH newest_shards AS ( 40 | SELECT shard_id, MIN(released_at) AS released_at FROM releases WHERE version <> 'HEAD' GROUP BY shard_id ORDER BY MIN(released_at) DESC LIMIT 10 41 | ) 42 | SELECT 43 | shards.id, name::text, qualifier::text, shards.description, archived_at, 44 | version, releases.released_at, 45 | repos.resolver::text, repos.url::text 46 | FROM 47 | shards 48 | JOIN 49 | releases ON releases.shard_id = shards.id 50 | JOIN 51 | repos ON repos.shard_id = shards.id AND repos.role = 'canonical' 52 | JOIN 53 | newest_shards ON newest_shards.shard_id = shards.id AND newest_shards.released_at = releases.released_at 54 | WHERE shards.archived_at IS NULL 55 | ORDER BY released_at DESC 56 | LIMIT 11 57 | SQL 58 | 59 | results.map do |result| 60 | id, name, qualifier, description, archived_at, version, created_at, resolver, url = result 61 | {shard: Shard.new(name, qualifier, description, archived_at, id: id), version: version, created_at: created_at, repo_ref: Repo::Ref.new(resolver, url)} 62 | end 63 | end 64 | 65 | def dependent_shards(scope : Dependency::Scope = :runtime) 66 | column_name = scope.development? ? "dev_dependents_count" : "dependents_count" 67 | results = connection.query_all <<-SQL % (), as: {Int64, String, String, String?, Time?, Int32, Int32, Int32, Array(Array(String))?, String, Time} 68 | SELECT 69 | shards.id, name::text, qualifier::text, shards.description, archived_at, 70 | metrics.dependents_count, metrics.dev_dependents_count, metrics.transitive_dependents_count, 71 | (SELECT array_agg(ARRAY[categories.slug::text, categories.name::text]) FROM categories WHERE shards.categories @> ARRAY[categories.id]), 72 | releases.version, releases.released_at 73 | FROM shards 74 | JOIN releases ON releases.shard_id = shards.id AND latest 75 | JOIN shard_metrics_current AS metrics ON metrics.shard_id = shards.id 76 | WHERE shards.archived_at IS NULL 77 | ORDER BY #{column_name} DESC 78 | LIMIT 11 79 | SQL 80 | 81 | results.map do |result| 82 | id, name, qualifier, description, archived_at, dependents_count, dev_dependents_count, transitive_dependents_count, categories, version, released_at = result 83 | categories ||= [] of Array(String) 84 | categories = categories.map { |(name, slug)| Category.new(name, slug) } 85 | { 86 | shard: Shard.new(name, qualifier, description, archived_at, id: id), 87 | dependents_count: dependents_count, 88 | dev_dependents_count: dev_dependents_count, 89 | transitive_dependents_count: transitive_dependents_count, 90 | categories: categories, 91 | version: version, 92 | released_at: released_at, 93 | } 94 | end 95 | end 96 | 97 | def popular_shards 98 | results = connection.query_all <<-SQL % (), as: {Int64, String, String, String?, Time?, Int32, Int32, Int32, Array(Array(String))?, String, Time} 99 | SELECT 100 | shards.id, name::text, qualifier::text, shards.description, archived_at, 101 | metrics.dependents_count, metrics.dev_dependents_count, metrics.transitive_dependents_count, 102 | (SELECT array_agg(ARRAY[categories.slug::text, categories.name::text]) FROM categories WHERE shards.categories @> ARRAY[categories.id]), 103 | releases.version, releases.released_at 104 | FROM shards 105 | JOIN releases ON releases.shard_id = shards.id AND latest 106 | JOIN shard_metrics_current AS metrics ON metrics.shard_id = shards.id 107 | WHERE shards.archived_at IS NULL 108 | ORDER BY popularity DESC 109 | LIMIT 11 110 | SQL 111 | 112 | results.map do |result| 113 | id, name, qualifier, description, archived_at, dependents_count, dev_dependents_count, transitive_dependents_count, categories, version, released_at = result 114 | categories ||= [] of Array(String) 115 | categories = categories.map { |(name, slug)| Category.new(name, slug) } 116 | { 117 | shard: Shard.new(name, qualifier, description, archived_at, id: id), 118 | dependents_count: dependents_count, 119 | dev_dependents_count: dev_dependents_count, 120 | transitive_dependents_count: transitive_dependents_count, 121 | categories: categories, 122 | version: version, 123 | released_at: released_at, 124 | } 125 | end 126 | end 127 | 128 | def shards_owned_by(owner_id : Int64) 129 | results = connection.query_all <<-SQL, owner_id, as: {Int64, String, String, String?, Time?, Int32, Int32, Int32, Array(Array(String))?, String, Time} 130 | SELECT 131 | shards.id, name::text, qualifier::text, shards.description, archived_at, 132 | metrics.dependents_count, metrics.dev_dependents_count, metrics.transitive_dependents_count, 133 | (SELECT array_agg(ARRAY[categories.slug::text, categories.name::text]) FROM categories WHERE shards.categories @> ARRAY[categories.id]), 134 | releases.version, releases.released_at 135 | FROM shards 136 | JOIN repos 137 | ON repos.shard_id = shards.id 138 | AND repos.role = 'canonical' 139 | JOIN releases 140 | ON releases.shard_id = shards.id AND latest 141 | JOIN shard_metrics_current AS metrics 142 | ON metrics.shard_id = shards.id 143 | WHERE owner_id = $1 144 | ORDER BY popularity DESC 145 | SQL 146 | 147 | results.map do |result| 148 | id, name, qualifier, description, archived_at, dependents_count, dev_dependents_count, transitive_dependents_count, categories, version, released_at = result 149 | categories ||= [] of Array(String) 150 | categories = categories.map { |(name, slug)| Category.new(name, slug) } 151 | { 152 | shard: Shard.new(name, qualifier, description, archived_at, id: id), 153 | dependents_count: dependents_count, 154 | dev_dependents_count: dev_dependents_count, 155 | transitive_dependents_count: transitive_dependents_count, 156 | version: version, 157 | released_at: released_at, 158 | categories: categories, 159 | } 160 | end 161 | end 162 | 163 | def get_owner_metrics(owner_id : Int64) 164 | result = connection.query_one? <<-SQL, owner_id, as: {Int32, Int32, Int32, Int32, Int32, Int32, Int32, Float32} 165 | SELECT 166 | shards_count, 167 | dependents_count, 168 | transitive_dependents_count, 169 | dev_dependents_count, 170 | transitive_dependencies_count, 171 | dev_dependencies_count, 172 | dependencies_count, 173 | popularity 174 | FROM 175 | owners 176 | WHERE id = $1 177 | AND dependents_count IS NOT NULL 178 | SQL 179 | 180 | return unless result 181 | Repo::Owner::Metrics.new(*result, nil) 182 | end 183 | 184 | def get_owners 185 | results = connection.query_all <<-SQL, as: {String, String, String?, String?, JSON::Any, Int64, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Float32} 186 | SELECT 187 | owners.resolver::text, 188 | owners.slug::text, 189 | owners.name, 190 | owners.description, 191 | owners.extra, 192 | owners.id, 193 | shards_count, 194 | dependents_count, 195 | transitive_dependents_count, 196 | dev_dependents_count, 197 | transitive_dependencies_count, 198 | dev_dependencies_count, 199 | dependencies_count, 200 | popularity 201 | FROM owners 202 | WHERE popularity IS NOT NULL 203 | ORDER BY popularity DESC 204 | LIMIT 100 205 | SQL 206 | 207 | results.map do |result| 208 | resolver, slug, name, description, extra, id, shards_count, dependents_count, transitive_dependents_count, dev_dependents_count, transitive_dependencies_count, dev_dependencies_count, dependencies_count, popularity = result 209 | 210 | { 211 | owner: Repo::Owner.new(resolver, slug, name, description, extra.as_h, id: id), 212 | metrics: Repo::Owner::Metrics.new(shards_count, dependents_count, transitive_dependents_count, dev_dependents_count, transitive_dependencies_count, dev_dependencies_count, dependencies_count, popularity), 213 | } 214 | end 215 | end 216 | 217 | # SHARD 218 | 219 | def find_shard?(name : String, qualifier : String) 220 | result = connection.query_one? <<-SQL, name, qualifier, as: {Int64, String, String, String?, Time?, Int64?} 221 | SELECT id, name::text, qualifier::text, description, archived_at, merged_with 222 | FROM shards 223 | WHERE 224 | name = $1 AND qualifier = $2; 225 | SQL 226 | 227 | return unless result 228 | 229 | id, name, qualifier, description, archived_at, merged_with = result 230 | Shard.new(name, qualifier, description, archived_at, merged_with, id: id) 231 | end 232 | 233 | def find_homonymous_shards(name : String) 234 | results = [] of {shard: Shard, repo_ref: Repo::Ref, category: String?} 235 | connection.query_all <<-SQL, name do |result| 236 | SELECT shards.id, shards.name::text, qualifier::text, shards.description, archived_at, 237 | resolver::text, url::text, categories.slug::text 238 | FROM shards 239 | JOIN repos 240 | ON repos.shard_id = shards.id 241 | AND repos.role = 'canonical' 242 | LEFT JOIN categories 243 | ON categories.id = shards.categories[1] 244 | WHERE 245 | shards.name = $1; 246 | SQL 247 | id, name, qualifier, description, archived_at, resolver, url, category = result.read Int64, String, String, String?, Time?, String, String, String? 248 | results << {shard: Shard.new(name, qualifier, description, archived_at, id: id), repo_ref: Repo::Ref.new(resolver, url), category: category} 249 | end 250 | results 251 | end 252 | 253 | def find_homonymous_shards(names) 254 | results = {} of String => Array(Shard) 255 | connection.query_all <<-SQL, names do |result| 256 | SELECT 257 | id, 258 | name::text, 259 | qualifier::text, 260 | description, 261 | archived_at 262 | FROM 263 | shards 264 | WHERE 265 | name = ANY($1) 266 | ORDER BY 267 | name, 268 | qualifier 269 | SQL 270 | id, name, qualifier, description, archived_at = result.read Int64, String, String, String?, Time? 271 | list = results[name] ||= [] of Shard 272 | list << Shard.new(name, qualifier, description, archived_at, id: id) 273 | end 274 | results 275 | end 276 | 277 | def dependencies(release_id : Int64, scope : Dependency::Scope) 278 | results = connection.query_all <<-SQL, release_id, scope, as: {String, JSON::Any, String, Int64?, String?, String?, String?, Time?} 279 | SELECT 280 | dependencies.name::text, dependencies.spec, dependencies.scope::text, 281 | shards.id, shards.name::text, shards.qualifier::text, description::text, archived_at 282 | FROM 283 | dependencies 284 | LEFT JOIN 285 | repos ON dependencies.repo_id = repos.id 286 | JOIN 287 | shards ON repos.shard_id = shards.id 288 | WHERE 289 | dependencies.release_id = $1 AND dependencies.scope = $2 290 | SQL 291 | 292 | results.map do |result| 293 | name, spec, scope, shard_id, shard_name, qualifier, description, archived_at = result 294 | scope = Dependency::Scope.parse(scope) 295 | 296 | if shard_id 297 | shard = Shard.new(shard_name.not_nil!, qualifier.not_nil!, description, archived_at, id: shard_id) 298 | end 299 | 300 | {dependency: Dependency.new(name, spec, scope), shard: shard} 301 | end 302 | end 303 | 304 | def dependents(shard_id : Int64) 305 | results = connection.query_all <<-SQL, shard_id, as: {Int64, String, String, String?, Time?, Int32} 306 | SELECT 307 | shards.id, shards.name::text, shards.qualifier::text, description::text, archived_at, 308 | metrics.dependents_count 309 | FROM 310 | shards 311 | JOIN 312 | shard_dependencies ON shard_dependencies.shard_id = shards.id 313 | JOIN 314 | shard_metrics_current AS metrics ON metrics.shard_id = shards.id 315 | WHERE 316 | shard_dependencies.depends_on = $1 317 | ORDER BY 318 | metrics.dependents_count DESC, metrics.transitive_dependents_count DESC, metrics.dev_dependents_count DESC, shards.name ASC 319 | SQL 320 | 321 | results.map do |result| 322 | shard_id, shard_name, qualifier, description, archived_at, dependents_count = result 323 | 324 | {shard: Shard.new(shard_name, qualifier, description, archived_at, id: shard_id), dependents_count: dependents_count} 325 | end 326 | end 327 | 328 | def find_categories(shard_id : Int64) 329 | results = connection.query_all <<-SQL, shard_id, as: {Int64, String, String, String?, Int32} 330 | SELECT 331 | categories.id, categories.slug::text, categories.name::text, categories.description::text, categories.entries_count 332 | FROM 333 | categories 334 | JOIN 335 | shards ON shards.categories @> ARRAY[categories.id] 336 | WHERE shards.id = $1 337 | SQL 338 | 339 | results.map do |result| 340 | id, slug, name, description, entries_count = result 341 | 342 | Category.new(slug, name, description, entries_count, id: id) 343 | end 344 | end 345 | 346 | record Metrics, shard_id : Int64, popularity : Float32?, likes_count : Int32?, watchers_count : Int32?, forks_count : Int32?, 347 | clones_count : Int32?, dependents_count : Int32?, transitive_dependents_count : Int32?, dev_dependents_count : Int32?, 348 | transitive_dependencies_count : Int32?, dev_dependencies_count : Int32?, dependencies_count : Int32?, created_at : Time 349 | 350 | def get_current_metrics(shard_id : Int64) 351 | result = connection.query_one? <<-SQL, shard_id, as: {Float32?, Int32?, Int32?, Int32?, Int32?, Int32?, Int32?, Int32?, Int32?, Int32?, Int32?, Time} 352 | SELECT 353 | popularity, likes_count, watchers_count, forks_count, 354 | clones_count, dependents_count, transitive_dependents_count, dev_dependents_count, 355 | transitive_dependencies_count, dev_dependencies_count, dependencies_count, created_at 356 | FROM 357 | shard_metrics_current 358 | WHERE 359 | shard_id = $1 360 | SQL 361 | return unless result 362 | Metrics.new(shard_id, *result) 363 | end 364 | 365 | # CATEGORY 366 | 367 | def all_categories_top_shards 368 | results = {} of Int64 => Array(Shard) 369 | connection.query_all <<-SQL do |rs| 370 | SELECT 371 | * 372 | FROM 373 | ( 374 | SELECT 375 | s.*, 376 | ROW_NUMBER() OVER ( 377 | PARTITION BY 378 | category_id 379 | ORDER BY 380 | transitive_dependents_count DESC 381 | ) AS r 382 | FROM 383 | ( 384 | SELECT 385 | unnest(categories) AS category_id, 386 | name::text, qualifier::text, 387 | shards.id 388 | FROM 389 | shards 390 | WHERE 391 | archived_at IS NULL 392 | ) s 393 | JOIN 394 | shard_metrics_current AS metrics ON s.id = metrics.shard_id 395 | ) x 396 | WHERE 397 | x.r <= 5 398 | SQL 399 | category_id, name, qualifier = rs.read Int64, String, String, Int64 400 | 401 | list = results[category_id] ||= [] of Shard 402 | list << Shard.new(name, qualifier) 403 | end 404 | results 405 | end 406 | 407 | def find_category(slug : String) 408 | result = connection.query_one? <<-SQL, slug, as: {Int64, String, String, String?, Int32} 409 | SELECT 410 | id, slug::text, name::text, description::text, entries_count 411 | FROM 412 | categories 413 | WHERE 414 | slug = $1 415 | SQL 416 | 417 | return unless result 418 | 419 | id, slug, name, description, entries_count = result 420 | 421 | Category.new(slug, name, description, entries_count, id: id) 422 | end 423 | 424 | record CategoryResult, shard : Shard, repo : Repo, release : Release, 425 | dependents_count : Int32?, 426 | dev_dependents_count : Int32?, 427 | transitive_dependents_count : Int32? 428 | 429 | def shards_in_category_with_releases(category_id : Int64?) 430 | if category_id 431 | args = [category_id] 432 | where = "$1 = ANY(categories)" 433 | else 434 | args = [] of Int64 435 | where = "categories = '{}'::bigint[]" 436 | end 437 | 438 | results = connection.query_all <<-SQL, args: args, as: {Int64, String, String, String?, Time?, String, Time, Int32?, Int32?, Int32?, String, String, String, Time?, Time?, Int64} 439 | SELECT 440 | shards.id, name::text, qualifier::text, shards.description, archived_at, 441 | releases.version, releases.released_at, 442 | metrics.dependents_count, metrics.dev_dependents_count, metrics.transitive_dependents_count, 443 | repos.resolver::text, repos.url::text, repos.metadata::text, repos.synced_at, repos.sync_failed_at, repos.id 444 | FROM 445 | shards 446 | JOIN 447 | releases ON releases.shard_id = shards.id AND releases.latest = true 448 | JOIN 449 | repos ON repos.shard_id = shards.id AND repos.role = 'canonical' 450 | LEFT JOIN 451 | shard_metrics_current AS metrics ON metrics.shard_id = shards.id 452 | WHERE 453 | #{where} 454 | ORDER BY 455 | metrics.popularity DESC 456 | SQL 457 | 458 | results.map do |result| 459 | shard_id, name, qualifier, description, archived_at, version, released_at, dependents_count, dev_dependents_count, transitive_dependents_count, resolver, url, metadata, synced_at, sync_failed_at, repo_id = result 460 | CategoryResult.new( 461 | shard: Shard.new(name, qualifier, description, archived_at, id: shard_id), 462 | repo: Repo.new(resolver, url, shard_id, 463 | metadata: Repo::Metadata.from_json(metadata), 464 | synced_at: synced_at, 465 | sync_failed_at: sync_failed_at, 466 | id: repo_id), 467 | release: Release.new(version, released_at), 468 | dependents_count: dependents_count, 469 | dev_dependents_count: dev_dependents_count, 470 | transitive_dependents_count: transitive_dependents_count, 471 | ) 472 | end 473 | end 474 | 475 | def duplicate_shard_names 476 | results = Hash(String, Array(String)).new { [] of String } 477 | connection.query_all <<-SQL do |rs| 478 | WITH qualified_shards AS ( 479 | SELECT DISTINCT name FROM shards WHERE qualifier != '' 480 | ) 481 | SELECT 482 | shards.name::text, qualifier::text 483 | FROM 484 | shards 485 | JOIN 486 | qualified_shards ON qualified_shards.name = shards.name 487 | ORDER BY 488 | name, qualifier 489 | SQL 490 | name, qualifier = rs.read String, String 491 | results[name] << qualifier 492 | end 493 | results 494 | end 495 | 496 | def search(query) 497 | query = "%#{query}%" 498 | results = connection.query_all <<-SQL, query, as: {Int64, String, String, String?, Time?, String, Time, String, String, String, Array(Array(String))?} 499 | SELECT 500 | shards.id, name::text, qualifier::text, shards.description, archived_at, 501 | releases.version, releases.released_at, 502 | repos.resolver::text, repos.url::text, repos.metadata::text, 503 | (SELECT array_agg(ARRAY[categories.slug::text, categories.name::text]) FROM categories WHERE shards.categories @> ARRAY[categories.id]) 504 | FROM 505 | shards 506 | JOIN 507 | releases ON releases.shard_id = shards.id AND releases.id = (SELECT id FROM releases WHERE shard_id = shards.id ORDER BY latest, position LIMIT 1) 508 | JOIN 509 | repos ON repos.shard_id = shards.id AND repos.role = 'canonical' 510 | LEFT JOIN 511 | shard_metrics_current AS metrics ON metrics.shard_id = shards.id 512 | WHERE 513 | name ILIKE $1 OR qualifier ILIKE $1 OR shards.description ILIKE $1 OR releases.spec->>'description' = $1 OR repos.metadata->>'description' = $1 514 | ORDER BY 515 | metrics.popularity DESC 516 | LIMIT 100 517 | SQL 518 | 519 | results.map do |result| 520 | shard_id, name, qualifier, description, archived_at, version, released_at, resolver, url, metadata, categories = result 521 | categories ||= [] of Array(String) 522 | categories = categories.map { |(name, slug)| Category.new(name, slug) } 523 | {shard: Shard.new(name, qualifier, description, archived_at, id: shard_id), repo: Repo.new(resolver, url, shard_id, metadata: Repo::Metadata.from_json(metadata)), version: version, released_at: released_at, categories: categories} 524 | end 525 | end 526 | 527 | # ACTIVITY 528 | def get_activity(shard_id : Int64) 529 | results = connection.query_all(<<-SQL, args: [shard_id], as: {Int64, Int64?, String, JSON::Any?, Time, Int64?, String?, String?}) 530 | SELECT log.id, log.repo_id, log.event, log.metadata, log.created_at, log.shard_id, 531 | repos.url::text, repos.resolver::text 532 | FROM activity_log log 533 | LEFT JOIN repos 534 | ON repos.id = log.repo_id 535 | WHERE 536 | log.shard_id = $1 537 | ORDER BY log.created_at DESC 538 | SQL 539 | results.map do |result| 540 | id, repo_id, event, metadata, created_at, shard_id, url, resolver = result 541 | if resolver && url 542 | repo_ref = Repo::Ref.new(resolver, url) 543 | else 544 | repo_ref = nil 545 | end 546 | Activity.new(event, created_at, metadata, shard_id, repo_ref, id: id) 547 | end 548 | end 549 | 550 | # STATS 551 | 552 | def stats 553 | Stats.new( 554 | shards_count: connection.query_one("SELECT COUNT(*) FROM shards", as: Int64), 555 | repos_count: connection.query_one("SELECT COUNT(*) FROM repos WHERE role <> 'obsolete'", as: Int64), 556 | dependencies_count: connection.query_one( 557 | "SELECT COUNT(*) FROM dependencies JOIN releases ON release_id = releases.id WHERE releases.latest = true AND scope = 'runtime'", as: Int64), 558 | dev_dependencies_count: connection.query_one( 559 | "SELECT COUNT(*) FROM dependencies JOIN releases ON release_id = releases.id WHERE releases.latest = true AND scope = 'development'", as: Int64), 560 | resolver_counts: count_table("SELECT resolver::text, COUNT(*) AS count FROM repos GROUP BY resolver ORDER BY count DESC"), 561 | crystal_version_counts: count_table("SELECT spec->>'crystal' AS version, COUNT(*) AS count FROM releases WHERE latest = true GROUP BY spec->>'crystal' ORDER BY count DESC"), 562 | license_counts: count_table("SELECT spec->>'license', COUNT(*) FROM releases WHERE latest = true GROUP BY spec->>'license' ORDER BY count DESC"), 563 | uncategorized_count: uncategorized_count, 564 | shards_without_dependencies_count: connection.query_one("SELECT COUNT(*) FROM shards LEFT JOIN shard_dependencies ON shard_id = shards.id WHERE shard_dependencies.depends_on_repo_id IS NULL", as: Int64), 565 | shard_yml_keys_counts: count_table("SELECT jsonb_object_keys(spec) AS key, COUNT(*) AS count FROM releases WHERE latest GROUP BY key ORDER BY count DESC"), 566 | ) 567 | end 568 | 569 | def uncategorized_count 570 | connection.query_one("SELECT COUNT(*) FROM shards WHERE categories = '{}'::bigint[]", as: Int64) 571 | end 572 | 573 | private def count_table(query) 574 | counts = {} of String => Int64 575 | connection.query(query) do |rs| 576 | rs.each do 577 | counts[rs.read(String?) || "none"] = rs.read(Int64) 578 | end 579 | end 580 | counts 581 | end 582 | 583 | record Stats, 584 | shards_count : Int64, 585 | repos_count : Int64, 586 | dependencies_count : Int64, 587 | dev_dependencies_count : Int64, 588 | resolver_counts : Hash(String, Int64), 589 | crystal_version_counts : Hash(String, Int64), 590 | license_counts : Hash(String, Int64), 591 | uncategorized_count : Int64, 592 | shards_without_dependencies_count : Int64, 593 | shard_yml_keys_counts : Hash(String, Int64) 594 | end 595 | --------------------------------------------------------------------------------