mirror of
https://github.com/daylinmorgan/nimpkgs.git
synced 2024-11-14 15:47:53 -06:00
Compare commits
8 commits
39bf6f2d05
...
b5dfc04040
Author | SHA1 | Date | |
---|---|---|---|
b5dfc04040 | |||
fa1e181f4c | |||
9c45cf8983 | |||
0175ac9548 | |||
4bcd6b0f4e | |||
633bbf5e3e | |||
4b92961c77 | |||
ccb303134b |
29 changed files with 1097 additions and 512 deletions
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
|
@ -3,8 +3,6 @@ name: 📄 Build Docs
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
schedule:
|
|
||||||
- cron: '0 2 * * *'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -133,7 +133,10 @@ dist
|
||||||
##
|
##
|
||||||
|
|
||||||
*.workspace
|
*.workspace
|
||||||
|
nim.cfg
|
||||||
site/uno.css
|
site/uno.css
|
||||||
site/nimpkgs.js
|
site/app.js
|
||||||
src/nim.cfg
|
src/*.js
|
||||||
src/packages
|
|
||||||
|
# for debugging
|
||||||
|
site/nimpkgs.json
|
||||||
|
|
11
README.md
11
README.md
|
@ -12,19 +12,8 @@ A web UI is available at [nimble.directory](https://nimble.directory)([repo](htt
|
||||||
But, there are some outstanding [issues](https://github.com/FedericoCeratto/nim-package-directory/issues/53) that have affected even my own packages.
|
But, there are some outstanding [issues](https://github.com/FedericoCeratto/nim-package-directory/issues/53) that have affected even my own packages.
|
||||||
|
|
||||||
This site is client-only, powered by [karax](https://github.com/karaxnim/karax) and styled with [unocss](https://github.com/unocss/unocss).
|
This site is client-only, powered by [karax](https://github.com/karaxnim/karax) and styled with [unocss](https://github.com/unocss/unocss).
|
||||||
It provide a single page search UI over `nim-lang/packages`.
|
|
||||||
This makes it trivial to deploy with Github Actions.
|
This makes it trivial to deploy with Github Actions.
|
||||||
|
|
||||||
## usage
|
|
||||||
|
|
||||||
On page load 10 random packages and a set of tags will be selected.
|
|
||||||
Search can be modified by specifying fields.
|
|
||||||
|
|
||||||
examples:
|
|
||||||
- `tag:database sqlite`
|
|
||||||
- `license:MIT web`
|
|
||||||
|
|
||||||
|
|
||||||
## license
|
## license
|
||||||
|
|
||||||
Logos in [site/img](./site/img/) by [The Nim Programming language](https://nim-lang.org) used under [CC BY 3.0](https://github.com/nim-lang/website/blob/master/LICENSE.md).
|
Logos in [site/img](./site/img/) by [The Nim Programming language](https://nim-lang.org) used under [CC BY 3.0](https://github.com/nim-lang/website/blob/master/LICENSE.md).
|
||||||
|
|
16
config.nims
16
config.nims
|
@ -1,22 +1,12 @@
|
||||||
import std/[strutils, strformat]
|
switch("backend","js")
|
||||||
|
|
||||||
--backend:js
|
|
||||||
|
|
||||||
proc getCommitInfo*(): (string, string) =
|
|
||||||
if not dirExists "src/packages":
|
|
||||||
echo "cloning nim-lang/packages"
|
|
||||||
discard staticExec "git clone https://github.com/nim-lang/packages.git src/packages"
|
|
||||||
let output = (staticExec "git -C src/packages show -q --format='%h %H'").split()
|
|
||||||
return (output[0], output[1])
|
|
||||||
|
|
||||||
task setup, "run atlas init":
|
task setup, "run atlas init":
|
||||||
exec "atlas init --deps=.workspace"
|
exec "atlas init --deps=.workspace"
|
||||||
exec "atlas install"
|
exec "atlas install"
|
||||||
|
|
||||||
task build, "build":
|
task build, "build":
|
||||||
let (short,long) = getCommitInfo()
|
selfExec "js -o:site/app.js -d:release src/app.nim"
|
||||||
selfExec fmt"js -o:site/nimpkgs.js -d:packagesHash:{long} -d:packagesHashAbbr:{short} -d:release src/nimpkgs.nim"
|
|
||||||
exec "pnpm run build"
|
exec "pnpm run build"
|
||||||
|
|
||||||
task watch, "rebuild on change":
|
task watch, "rebuild on change":
|
||||||
exec "watchexec -w src nim js -d:packagesHash:master -o:site/nimpkgs.js src/nimpkgs.nim"
|
exec "watchexec -w src nim js -d:packagesHash:master -o:site/app.js src/app.nim"
|
||||||
|
|
|
@ -1,17 +1,7 @@
|
||||||
# Package
|
|
||||||
|
|
||||||
version = "2023.1001"
|
|
||||||
author = "Daylin Morgan"
|
|
||||||
description = "nim-lang packages alternate ui"
|
|
||||||
license = "MIT"
|
|
||||||
srcDir = "src"
|
|
||||||
bin = @["nimpkgs"]
|
|
||||||
|
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|
||||||
requires "nim >= 2.0.0"
|
requires "nim >= 2.0.0"
|
||||||
requires "karax"
|
requires "karax"
|
||||||
requires "jsony"
|
requires "jsony"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
13
package.json
13
package.json
|
@ -4,22 +4,23 @@
|
||||||
"server": "http-server ./site",
|
"server": "http-server ./site",
|
||||||
"watch": "nim watch",
|
"watch": "nim watch",
|
||||||
"build": "pnpm run uno:prd && pnpm run minify",
|
"build": "pnpm run uno:prd && pnpm run minify",
|
||||||
"minify": "esbuild --minify --outdir=site --allow-overwrite site/nimpkgs.js site/uno.css",
|
"minify": "esbuild --minify --outdir=site --allow-overwrite site/app.js site/uno.css",
|
||||||
"uno:dev": "unocss \"./site/**/*.html\" \"./src/**/*.nim\" --out-file site/uno.css -w",
|
"uno:dev": "unocss \"./site/**/*.html\" \"./src/**/*.nim\" --out-file site/uno.css -w",
|
||||||
"uno:prd": "unocss \"./site/**/*.html\" \"./src/**/*.nim\" --out-file site/uno.css"
|
"uno:prd": "unocss \"./site/**/*.html\" \"./src/**/*.nim\" --out-file site/uno.css"
|
||||||
},
|
},
|
||||||
"author": "Daylin Morgan",
|
"author": "Daylin Morgan",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"@catppuccin/palette": "^0.2.0",
|
"@catppuccin/palette": "^0.2.0",
|
||||||
|
"@iconify-json/mdi": "^1.1.55",
|
||||||
|
"@iconify-json/simple-icons": "^1.1.79",
|
||||||
"@types/promise-fs": "^2.1.5",
|
"@types/promise-fs": "^2.1.5",
|
||||||
"@unocss/cli": "^0.57.3",
|
"@unocss/cli": "^0.57.3",
|
||||||
|
"@unocss/preset-icons": "^0.57.7",
|
||||||
"@unocss/reset": "^0.57.3",
|
"@unocss/reset": "^0.57.3",
|
||||||
"unocss": "^0.57.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"esbuild": "^0.19.5",
|
"esbuild": "^0.19.5",
|
||||||
"http-server": "^14.1.1"
|
"http-server": "^14.1.1",
|
||||||
|
"unocss": "^0.57.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
339
pnpm-lock.yaml
339
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
82
site/img/logo-wide.svg
Normal file
82
site/img/logo-wide.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9 KiB |
|
@ -3,7 +3,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||||
<link rel="icon" href="img/logo-crown.svg">
|
<link rel="icon" href="img/logo-crown.svg" type="image/svg+xml">
|
||||||
<title>nimpkgs</title>
|
<title>nimpkgs</title>
|
||||||
<link href="uno.css" rel="stylesheet" type="text/css">
|
<link href="uno.css" rel="stylesheet" type="text/css">
|
||||||
<!--
|
<!--
|
||||||
|
@ -11,8 +11,8 @@
|
||||||
-->
|
-->
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..1000,0..1,1&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Recursive:wght,CASL,MONO@300..1000,0..1,1&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body id="body" class="text-ctp-text max-w-screen bg-ctp-base flex items-center justify-center">
|
<body id="body" class="text-ctp-text max-w-screen bg-ctp-base">
|
||||||
<div id="ROOT"></div>
|
<div id="ROOT"></div>
|
||||||
<script type="text/javascript" src="nimpkgs.js"></script>
|
<script type="text/javascript" src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
30
src/app.nim
Normal file
30
src/app.nim
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import std/strutils
|
||||||
|
import karax/[karax, karaxdsl, vdom]
|
||||||
|
|
||||||
|
import components/[header, button, footer]
|
||||||
|
import pages/pages
|
||||||
|
import context
|
||||||
|
import jsconsole
|
||||||
|
|
||||||
|
proc render(data: RouterData): VNode =
|
||||||
|
console.log ctx
|
||||||
|
result = buildHtml(tdiv(class = "lg:w-3/4 max-w-[90%] mx-auto md:text-lg text-sm min-h-screen flex flex-col")):
|
||||||
|
headerBar()
|
||||||
|
tdiv(class = "mb-5"):
|
||||||
|
if not ctx.loaded:
|
||||||
|
tdiv(class = "flex h-50"):
|
||||||
|
tdiv(class = "mx-auto my-auto lds-dual-ring")
|
||||||
|
else:
|
||||||
|
case data.hashPart
|
||||||
|
of "#/index", "": index.render()
|
||||||
|
of "#/search": search.render()
|
||||||
|
of "#/metrics": metrics.render()
|
||||||
|
else:
|
||||||
|
if ($data.hashPart).startswith("#/pkg/"):
|
||||||
|
package.render(($data.hashPart).replace("#/pkg/", ""))
|
||||||
|
else:
|
||||||
|
notfound.render()
|
||||||
|
footerBar()
|
||||||
|
scrollToTopButton()
|
||||||
|
|
||||||
|
setRenderer render
|
1
src/app.nim.cfg
Normal file
1
src/app.nim.cfg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
--backend:js
|
|
@ -1,7 +1,5 @@
|
||||||
import std/[dom, sugar]
|
import std/[dom, sugar]
|
||||||
|
import karax/[karax, karaxdsl, vdom, vstyles]
|
||||||
include karax/prelude
|
|
||||||
import karax/vstyles
|
|
||||||
|
|
||||||
proc showScrollToTop() =
|
proc showScrollToTop() =
|
||||||
# TODO: only show button when scrolling up
|
# TODO: only show button when scrolling up
|
||||||
|
@ -18,14 +16,14 @@ proc scrollToTop*() =
|
||||||
document.body.scrollTop = 0
|
document.body.scrollTop = 0
|
||||||
document.documentElement.scrollTop = 0
|
document.documentElement.scrollTop = 0
|
||||||
|
|
||||||
document.addEventListener("scroll", (e: Event) => showScrollToTop())
|
document.addEventListener("scroll", (e: dom.Event) => showScrollToTop())
|
||||||
|
|
||||||
proc scrollToTopButton*(): VNode =
|
proc scrollToTopButton*(): VNode =
|
||||||
|
|
||||||
result = buildHtml(tdiv):
|
result = buildHtml(tdiv):
|
||||||
button(
|
button(
|
||||||
class =
|
class =
|
||||||
" absolute fixed md:bottom-10 md:right-10 bottom-2 right-2 " &
|
" absolute fixed md:bottom-10 right-10 bottom-2 " &
|
||||||
" md:p-5 p-2 cursor-pointer z-99 rounded " &
|
" md:p-5 p-2 cursor-pointer z-99 rounded " &
|
||||||
" bg-ctp-rosewater hover:bg-ctp-mauve text-ctp-mantle ",
|
" bg-ctp-rosewater hover:bg-ctp-mauve text-ctp-mantle ",
|
||||||
`id` = "scrollBtn",
|
`id` = "scrollBtn",
|
29
src/components/footer.nim
Normal file
29
src/components/footer.nim
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import std/[times]
|
||||||
|
import karax/[kbase, karaxdsl, vdom, jstrutils]
|
||||||
|
|
||||||
|
import ../[context, style]
|
||||||
|
|
||||||
|
const packagesGitUrlBase = "https://github.com/nim-lang/packages/blob/".kstring
|
||||||
|
|
||||||
|
proc footerBar*(): VNode =
|
||||||
|
var links: seq[(kstring, kstring)]
|
||||||
|
if ctx.loaded:
|
||||||
|
let packagesAbbr = ($ctx.nimpkgs.packagesHash)[0..8].kstring
|
||||||
|
links.add (
|
||||||
|
packagesGitUrlBase & ctx.nimpkgs.packagesHash & "/packages.json".kstring,
|
||||||
|
"nim-lang/packages:" & packagesAbbr
|
||||||
|
)
|
||||||
|
links.add ("http://github.com/daylinmorgan/nimpkgs".kstring, "source".kstring)
|
||||||
|
result = buildHtml(footer(class = "mt-auto md:mx-10 flex flex-col md:flex-row md:justify-between md:items-center mb-5")):
|
||||||
|
if ctx.loaded:
|
||||||
|
tdiv(class = "text-xs text-ctp-subtextzero px-1"):
|
||||||
|
text "updated: " & ctx.nimpkgs.updated.format("yyyy-MM-ddZZZ")
|
||||||
|
tdiv():
|
||||||
|
ul(class = "md:flex items-center"):
|
||||||
|
for (url, msg) in links:
|
||||||
|
li(class = "px-1 hover:bg-ctp-mantle rounded text-sm flex items-center space-x-1"):
|
||||||
|
tdiv(class = "i-mdi-github")
|
||||||
|
a(href = url, class = accent):
|
||||||
|
text msg
|
||||||
|
|
||||||
|
|
27
src/components/header.nim
Normal file
27
src/components/header.nim
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import karax/[kbase, karaxdsl, vdom]
|
||||||
|
|
||||||
|
import ../style
|
||||||
|
|
||||||
|
proc headerBar*(): VNode =
|
||||||
|
result = buildHtml(tdiv(class = "md:m-5 m-1 flex flex-wrap")):
|
||||||
|
a(href = "/#", class = " no-underline"):
|
||||||
|
img(src = "img/logo-wide.svg", class = "inline md:h-4rem h-3rem px-1")
|
||||||
|
tdiv(class = "grow")
|
||||||
|
label(`for` = "menu-toggle",
|
||||||
|
class = "cursor-pointer lg:hidden flex items-center px-3 py-2"
|
||||||
|
):
|
||||||
|
text "menu"
|
||||||
|
input(class = "hidden", type = "checkbox", `id` = "menu-toggle")
|
||||||
|
tdiv(class = "lg:flex lg:items-center lg:justify-between hidden w-full lg:w-auto justify-end",
|
||||||
|
`id` = "menu"):
|
||||||
|
nav(class = "flex justify-end"):
|
||||||
|
ul(class = "lg:flex items-center"):
|
||||||
|
for (url, msg) in [
|
||||||
|
("/#/search", "search"),
|
||||||
|
("/#/metrics", "metrics"),
|
||||||
|
]:
|
||||||
|
li(class = "p-2 hover:bg-ctp-mantle rounded text-sm md:text-lg"):
|
||||||
|
a(href = url.kstring, class = accent):
|
||||||
|
text msg
|
||||||
|
|
||||||
|
|
87
src/components/package.nim
Normal file
87
src/components/package.nim
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import std/[algorithm, strutils, sequtils, jsconsole, uri, random]
|
||||||
|
|
||||||
|
import karax/[kbase, karax, karaxdsl, vdom, jstrutils, ]
|
||||||
|
|
||||||
|
import ../[packages, style, context]
|
||||||
|
import ../components/tag
|
||||||
|
import ../utils
|
||||||
|
|
||||||
|
randomize()
|
||||||
|
proc authorRepo(uri: Uri, hostname = false): kstring =
|
||||||
|
var name =
|
||||||
|
if hostname: uri.hostname & uri.path.replace(".git")
|
||||||
|
else: uri.path[1..^1].replace(".git")
|
||||||
|
if name[^1] == '/':
|
||||||
|
name = name[0..^2]
|
||||||
|
return name.jss
|
||||||
|
|
||||||
|
proc projectUrl*(pkg: NimPackage): VNode =
|
||||||
|
let uri = parseUri($pkg.url)
|
||||||
|
let icon =
|
||||||
|
case uri.hostname:
|
||||||
|
of "github.com": "i-mdi-github"
|
||||||
|
of "gitlab.com": "i-mdi-gitlab"
|
||||||
|
of "git.sr.ht": "i-simple-icons-sourcehut"
|
||||||
|
of "codeberg.org": "i-simple-icons-codeberg"
|
||||||
|
of "bitbucket.org": "i-simple-icons-bitbucket"
|
||||||
|
else: "i-mdi-git"
|
||||||
|
let repoName = uri.authorRepo(hostname = (icon == "i-mdi-git"))
|
||||||
|
|
||||||
|
buildHtml:
|
||||||
|
tdiv(class = "flex items-center space-x-2"):
|
||||||
|
tdiv(class = icon.jss & " shrink-0")
|
||||||
|
a(href = pkg.url, class = if pkg.deleted: "line-through text-ctp-red" else: ""):
|
||||||
|
text repoName.jss
|
||||||
|
|
||||||
|
|
||||||
|
proc card*(pkg: NimPackage): VNode =
|
||||||
|
result = buildHtml(tdiv(class = "flex flex-col bg-ctp-crust rounded-xl my-5 p-5")):
|
||||||
|
tdiv(class = "flex flex-col md:flex-row md:justify-between"):
|
||||||
|
a(href = "/#/pkg/" & pkg.name):
|
||||||
|
h2(class = (textStyle & "font-black md:text-2xl text-lg font-casual").kstring):
|
||||||
|
text pkg.name
|
||||||
|
if not pkg.isAlias:
|
||||||
|
pkg.projectUrl
|
||||||
|
if pkg.isAlias:
|
||||||
|
tdiv:
|
||||||
|
text "alias for: "
|
||||||
|
span(onClick = setSearchUrl("name:" & pkg.alias),
|
||||||
|
class = "link"):
|
||||||
|
text pkg.alias
|
||||||
|
else:
|
||||||
|
span(class = "md:text-xl my-2"): text pkg.description
|
||||||
|
tdiv(class = "flex flex-col text-xs md:text-lg overflow-x-scroll"):
|
||||||
|
tdiv(class = "flex flex-wrap"):
|
||||||
|
for t in pkg.tags:
|
||||||
|
tdiv(
|
||||||
|
onClick = setSearchUrl("tag:" & t.replace(" ", "-")),
|
||||||
|
class = "link"):
|
||||||
|
t.renderTag
|
||||||
|
|
||||||
|
proc getRecentReleases(ctx: Context): seq[NimPackage] =
|
||||||
|
var pkgs: seq[NimPackage]
|
||||||
|
for pkg in ctx.nimpkgs.packages.values():
|
||||||
|
if pkg.versions.len > 0:
|
||||||
|
pkgs.add pkg
|
||||||
|
|
||||||
|
pkgs.sort(sortVersion, order = Descending)
|
||||||
|
return pkgs[0..20]
|
||||||
|
|
||||||
|
proc recentPackageVersionList*(ctx: Context): VNode =
|
||||||
|
let pkgs = ctx.getRecentReleases
|
||||||
|
result = buildHtml(tdiv(class = "flex flex-wrap")):
|
||||||
|
for pkg in pkgs:
|
||||||
|
a(class = borderStyle & "p-2 m-1 space-x-1 no-underline text-ctp-text",
|
||||||
|
href = "/#/pkg/" & pkg.name):
|
||||||
|
span(class = textStyle & "font-bold font-mono-casual"): text pkg.name
|
||||||
|
span(class = "italic"): text pkg.versions[0].tag
|
||||||
|
span:
|
||||||
|
text " (" & (getTime() - pkg.versions[0].time).inDays.jss & " days ago)"
|
||||||
|
|
||||||
|
proc randomPackage*(ctx: Context): VNode =
|
||||||
|
let pkgName = ctx.nimpkgs.packages.keys().toSeq().sample()
|
||||||
|
console.log pkgName.jss
|
||||||
|
result = buildHtml(tdiv(class = borderStyle & "my-2 m-1 p-2")):
|
||||||
|
a(href = "/#/pkg/" & pkgName.jss, class = "flex items-center text-ctp-text no-underline"):
|
||||||
|
tdiv(class = "i-mdi-dice-6")
|
||||||
|
span(class = "font-ctp-text"): text "random"
|
73
src/components/search.nim
Normal file
73
src/components/search.nim
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import std/[strutils, sequtils, dom, uri]
|
||||||
|
|
||||||
|
import karax/[kbase, karax, karaxdsl, vdom, jstrutils, kdom]
|
||||||
|
|
||||||
|
import ../[packages, style, context]
|
||||||
|
# import ../components/package
|
||||||
|
import ../utils
|
||||||
|
|
||||||
|
type
|
||||||
|
Query* = object
|
||||||
|
all, name, tag, license = "".kstring
|
||||||
|
|
||||||
|
proc parseQuery*(s: kstring): Query =
|
||||||
|
result = Query()
|
||||||
|
if ":" notin s:
|
||||||
|
result.all = s; return
|
||||||
|
|
||||||
|
let parts = s.split(" ")
|
||||||
|
for part in parts:
|
||||||
|
if ":" in part:
|
||||||
|
let
|
||||||
|
subparts = part.split(":")
|
||||||
|
k = subparts[0]
|
||||||
|
v = subparts[1]
|
||||||
|
case k:
|
||||||
|
of "name":
|
||||||
|
result.name = v
|
||||||
|
of "tag":
|
||||||
|
result.tag = v.replace("-")
|
||||||
|
of "license":
|
||||||
|
result.license = v
|
||||||
|
else: discard
|
||||||
|
else:
|
||||||
|
result.all &= part
|
||||||
|
|
||||||
|
|
||||||
|
proc searchPackages*(q: Query): seq[NimPackage] =
|
||||||
|
if q == Query():
|
||||||
|
result = ctx.nimpkgs.packages.values.toSeq()
|
||||||
|
return
|
||||||
|
|
||||||
|
for name, pkg in ctx.nimpkgs.packages:
|
||||||
|
let searchStr = ((pkg.url & " " & pkg.name & " " & pkg.description & " " & (
|
||||||
|
pkg.tags).join(" ").kstring))
|
||||||
|
if (q.name notin pkg.name) or
|
||||||
|
(q.license notin pkg.license) or
|
||||||
|
(q.tag != "".kstring and (q.tag notin pkg.tags)): continue
|
||||||
|
|
||||||
|
if q.all in searchStr:
|
||||||
|
result.add pkg
|
||||||
|
|
||||||
|
proc getSearchFromUri*(): kstring =
|
||||||
|
var url = initUri()
|
||||||
|
parseUri($window.location.href, url)
|
||||||
|
if url.query == "": return ""
|
||||||
|
for k, v in decodeQuery(url.query):
|
||||||
|
if k == "query":
|
||||||
|
return v.kstring
|
||||||
|
|
||||||
|
proc getSearchInput*() =
|
||||||
|
let searchInput = getVNodeById("search").getInputText
|
||||||
|
setSearchUrl(searchInput)()
|
||||||
|
|
||||||
|
proc searchBar*(value = jss""): Vnode =
|
||||||
|
buildHtml(tdiv(class = "flex flex-row my-2 grow")):
|
||||||
|
input(`type` = "text", class = "bg-ctp-crust md:mx-3 mx-1 p-2 grow".kstring & borderStyle, `id` = "search",
|
||||||
|
placeholder = "query", value = value,
|
||||||
|
onChange = getSearchInput)
|
||||||
|
button(`type` = "button", class = borderStyle & "p-2 flex items-center",
|
||||||
|
onClick = getSearchInput):
|
||||||
|
tdiv(class = "i-mdi-magnify")
|
||||||
|
text "search"
|
||||||
|
|
46
src/components/tag.nim
Normal file
46
src/components/tag.nim
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import std/[uri, tables, random]
|
||||||
|
import karax/[kbase, karaxdsl, vdom, jstrutils]
|
||||||
|
|
||||||
|
import ../[packages, style, context, utils]
|
||||||
|
|
||||||
|
randomize()
|
||||||
|
|
||||||
|
proc renderTag*(tag: kstring): VNode =
|
||||||
|
buildHtml:
|
||||||
|
tdiv(class = "link md:p-2 p-1 m-1" & borderStyle):
|
||||||
|
text tag
|
||||||
|
|
||||||
|
proc renderTags*(tags: seq[kstring]): VNode =
|
||||||
|
buildHtml:
|
||||||
|
tdiv(class = "flex flex-wrap"):
|
||||||
|
for i, tag in tags:
|
||||||
|
let query = encodeQuery({"query": $("tag:" & tag)})
|
||||||
|
a(
|
||||||
|
href = ("/?" & query & "#/search").jss,
|
||||||
|
class = "no-underline"
|
||||||
|
):
|
||||||
|
tag.renderTag
|
||||||
|
|
||||||
|
proc selectRandomTags*(ctx: Context): seq[kstring] =
|
||||||
|
var tagCounts: CountTable[kstring]
|
||||||
|
for pkg in ctx.nimpkgs.packages.values():
|
||||||
|
for tag in pkg.tags:
|
||||||
|
tagCounts.inc tag
|
||||||
|
|
||||||
|
var tags: seq[kstring]
|
||||||
|
for tag, cnt in tagCounts:
|
||||||
|
if cnt > 3: tags.add tag
|
||||||
|
|
||||||
|
while result.len < 5:
|
||||||
|
let tag = tags.sample()
|
||||||
|
if tag notin result:
|
||||||
|
result.add tag
|
||||||
|
|
||||||
|
proc randomTags*(ctx: Context): VNode =
|
||||||
|
let tags = ctx.selectRandomTags()
|
||||||
|
buildHtml(tdiv):
|
||||||
|
tags.renderTags
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
33
src/context.nim
Normal file
33
src/context.nim
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import std/[
|
||||||
|
asyncjs, jsconsole, jsfetch, sugar, tables
|
||||||
|
]
|
||||||
|
|
||||||
|
import karax/[kbase, karax]
|
||||||
|
import jsony
|
||||||
|
|
||||||
|
import packages, utils
|
||||||
|
|
||||||
|
export tables
|
||||||
|
|
||||||
|
type
|
||||||
|
Context* = object
|
||||||
|
nimpkgs*: NimPkgs
|
||||||
|
loaded*: bool
|
||||||
|
|
||||||
|
let nimpkgsUrl =
|
||||||
|
when defined(debug): "http://localhost:8080/nimpkgs.json"
|
||||||
|
else: "https://raw.githubusercontent.com/nimpkgs/nimpkgs/main/nimpkgs.json"
|
||||||
|
|
||||||
|
|
||||||
|
proc fetchPackages*(ctx: var Context){.async.} =
|
||||||
|
await fetch(nimpkgsUrl.jss)
|
||||||
|
.then((r: Response) => r.text())
|
||||||
|
.then(proc(txt: kstring) =
|
||||||
|
ctx.nimpkgs = fromJson($txt, NimPkgs)
|
||||||
|
ctx.loaded = true
|
||||||
|
redraw()
|
||||||
|
)
|
||||||
|
.catch((err: Error) => console.log err
|
||||||
|
)
|
||||||
|
var ctx* = Context()
|
||||||
|
discard ctx.fetchPackages
|
250
src/nimpkgs.nim
250
src/nimpkgs.nim
|
@ -1,250 +0,0 @@
|
||||||
import std/[strutils, sets, sequtils, random]
|
|
||||||
include karax / prelude
|
|
||||||
|
|
||||||
import packages, button
|
|
||||||
|
|
||||||
type
|
|
||||||
Query = object
|
|
||||||
all, name, tag, license = "".kstring
|
|
||||||
|
|
||||||
randomize()
|
|
||||||
|
|
||||||
|
|
||||||
var
|
|
||||||
filteredPackages: seq[Package] = allPackages
|
|
||||||
searchInput: kstring = "".kstring
|
|
||||||
const
|
|
||||||
packagesGitUrl = "https://github.com/nim-lang/packages/blob/" & packagesHash & "/packages.json"
|
|
||||||
numPackages = allPackages.len
|
|
||||||
numTags = allTags.len
|
|
||||||
colors = [
|
|
||||||
"flamingo",
|
|
||||||
"pink",
|
|
||||||
"mauve",
|
|
||||||
"red",
|
|
||||||
"maroon",
|
|
||||||
"peach",
|
|
||||||
"yellow",
|
|
||||||
"green",
|
|
||||||
"teal",
|
|
||||||
"sky",
|
|
||||||
"sapphire",
|
|
||||||
"blue",
|
|
||||||
"lavender"
|
|
||||||
]
|
|
||||||
let
|
|
||||||
accent = (" " & colors.sample() & " ").kstring
|
|
||||||
textStyle = (" text-ctp-" & accent & " ").kstring
|
|
||||||
borderStyle = (" b-ctp-" & accent & " ").kstring
|
|
||||||
randomPkgIndices = [
|
|
||||||
rand(numPackages-1), rand(numPackages-1), rand(numPackages-1),
|
|
||||||
rand(numPackages-1), rand(numPackages-1), rand(numPackages-1),
|
|
||||||
rand(numPackages-1), rand(numPackages-1), rand(numPackages-1),
|
|
||||||
rand(numPackages-1)]
|
|
||||||
randomTagIndices = [
|
|
||||||
rand(numTags-1), rand(numTags-1), rand(numTags-1),
|
|
||||||
rand(numTags-1), rand(numTags-1), rand(numTags-1)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
proc parseQuery(s: kstring): Query =
|
|
||||||
result = Query()
|
|
||||||
if ":" notin s:
|
|
||||||
result.all = s; return
|
|
||||||
|
|
||||||
let parts = s.split(" ")
|
|
||||||
for part in parts:
|
|
||||||
if ":" in part:
|
|
||||||
let
|
|
||||||
subparts = part.split(":")
|
|
||||||
k = subparts[0]
|
|
||||||
v = subparts[1]
|
|
||||||
case k:
|
|
||||||
of "name":
|
|
||||||
result.name = v
|
|
||||||
of "tag":
|
|
||||||
result.tag = v
|
|
||||||
of "license":
|
|
||||||
result.license = v
|
|
||||||
else: discard
|
|
||||||
else:
|
|
||||||
result.all &= part
|
|
||||||
|
|
||||||
proc searchPackages(q: Query) =
|
|
||||||
filteredPackages = @[]
|
|
||||||
if q == Query():
|
|
||||||
filteredPackages = allPackages
|
|
||||||
return
|
|
||||||
for pkg in allPackages:
|
|
||||||
let searchStr = ((pkg.name & " " & pkg.description & " " & (pkg.tags).join(" ").kstring))
|
|
||||||
if (q.name notin pkg.name) or
|
|
||||||
(q.license notin pkg.license) or
|
|
||||||
(q.tag != "".kstring and (q.tag notin pkg.tags)): continue
|
|
||||||
|
|
||||||
if q.all in searchStr:
|
|
||||||
|
|
||||||
filteredPackages.add pkg
|
|
||||||
|
|
||||||
proc setSearch(v: kstring): proc () =
|
|
||||||
result = proc() =
|
|
||||||
searchInput = v
|
|
||||||
searchPackages(parseQuery(v))
|
|
||||||
redraw()
|
|
||||||
|
|
||||||
proc fieldToDom(s: kstring): VNode =
|
|
||||||
result = buildHtml(tdiv(class = "font-black basis-1/4 sm:basis-1/6 shrink-0")):
|
|
||||||
text s & ":"
|
|
||||||
|
|
||||||
proc noProtocol(s: kstring): kstring = kstring(($s).replace("http://",
|
|
||||||
"").replace("https://", ""))
|
|
||||||
|
|
||||||
proc toDom(pkg: Package): VNode =
|
|
||||||
result = buildHtml(tdiv(class = "flex flex-col bg-ctp-crust rounded-xl my-5 p-5")):
|
|
||||||
h2(class = (textStyle & "font-black md:text-2xl text-lg font-casual").kstring):
|
|
||||||
text ("# " & pkg.name).kstring
|
|
||||||
if pkg.alias != "":
|
|
||||||
tdiv:
|
|
||||||
text "alias for: "
|
|
||||||
span(onClick = setSearch("name:" & pkg.alias),
|
|
||||||
class = "hover:text-ctp-mauve"):
|
|
||||||
text pkg.alias
|
|
||||||
else:
|
|
||||||
text pkg.description
|
|
||||||
tdiv(class = "flex flex-col text-xs md:text-lg overflow-x-scroll"):
|
|
||||||
tdiv(class = "flex flex-row"):
|
|
||||||
fieldToDom("project")
|
|
||||||
a(href = pkg.url):
|
|
||||||
text pkg.url.noProtocol
|
|
||||||
tdiv(class = "flex flex-row"):
|
|
||||||
fieldToDom("web")
|
|
||||||
a(href = pkg.web): text pkg.web.noProtocol
|
|
||||||
if pkg.doc != "":
|
|
||||||
tdiv(class = "flex flex-row"):
|
|
||||||
fieldToDom("doc")
|
|
||||||
a(href = pkg.doc): text pkg.doc.noProtocol
|
|
||||||
tdiv(class = "flex flex-row"):
|
|
||||||
fieldToDom("license")
|
|
||||||
span: text pkg.license
|
|
||||||
tdiv(class = "flex flex-row"):
|
|
||||||
fieldToDom("tags")
|
|
||||||
tdiv():
|
|
||||||
for t in pkg.tags:
|
|
||||||
span(onClick = setSearch("tag:" & t),
|
|
||||||
class = "hover:text-ctp-mauve"):
|
|
||||||
text t
|
|
||||||
text "; "
|
|
||||||
|
|
||||||
# tdiv(class="bg-ctp-mantle rounded my-2 p-2"):
|
|
||||||
# text "nimble install " & p.name
|
|
||||||
# br()
|
|
||||||
# text "atlas use " & p.name
|
|
||||||
|
|
||||||
proc startChar(p: Package): char = p.name[0].toLowerAscii
|
|
||||||
|
|
||||||
proc toDom(pkgs: seq[Package]): VNode =
|
|
||||||
var l = 'a'
|
|
||||||
result = buildHtml(tdiv):
|
|
||||||
if pkgs[0].startChar == l: tdiv(id = ($l).kstring)
|
|
||||||
for pkg in pkgs:
|
|
||||||
let startC = pkg.name[0].toLowerAscii
|
|
||||||
if l != startC:
|
|
||||||
while l != startC: inc l
|
|
||||||
tdiv(id = ($l).kstring)
|
|
||||||
pkg.toDom
|
|
||||||
|
|
||||||
proc getSearchInput() =
|
|
||||||
searchInput = getVNodeById("search").getInputText
|
|
||||||
searchPackages(parseQuery(searchInput))
|
|
||||||
|
|
||||||
proc render(t: Tag): VNode =
|
|
||||||
result = buildHtml(tdiv(class = "bg-ctp-mantle m-1 p-1 rounded hover:text-ctp-mauve")):
|
|
||||||
tdiv(onClick = setSearch("tag:" & t.name)):
|
|
||||||
text t.name & "|" & kstring($t.packages)
|
|
||||||
|
|
||||||
proc searchBar(): Vnode =
|
|
||||||
result = buildHtml(tdiv(class = "flex flex-col md:flex-row md:items-center md:my-5")):
|
|
||||||
tdiv(class = "flex flex-row my-2"):
|
|
||||||
input(`type` = "text", class = "border-1 bg-ctp-crust rounded mx-3 p-2".kstring & borderStyle, `id` = "search",
|
|
||||||
placeholder = "query", value = searchInput,
|
|
||||||
onChange = getSearchInput)
|
|
||||||
button(`type` = "button", class = "border-1 rounded p-2".kstring &
|
|
||||||
borderStyle, onClick = getSearchInput):
|
|
||||||
text "search"
|
|
||||||
#[
|
|
||||||
tdiv(class = "md:mx-5 flex flex-col items-center"):
|
|
||||||
tdiv: text "examples: "
|
|
||||||
tdiv(class="flex flex-col"):
|
|
||||||
for msg in ["tag:database sqlite","license:MIT javascript"]:
|
|
||||||
span(class = "bg-ctp-mantle rounded text-ctp-subtextone m-1 p-1 text-xs"):
|
|
||||||
text msg
|
|
||||||
]#
|
|
||||||
tdiv(class = "flex flex-col mx-5"):
|
|
||||||
tdiv: text "explore tags:"
|
|
||||||
tdiv(class = "flex flex-wrap text-sm"):
|
|
||||||
for idx in randomTagIndices:
|
|
||||||
allTags[idx].render
|
|
||||||
|
|
||||||
|
|
||||||
proc headerBar(): VNode =
|
|
||||||
result = buildHtml(tdiv(class = "mt-5 mx-5 flex flex-wrap")):
|
|
||||||
tdiv(class = "flex items-center my-3 grow"):
|
|
||||||
img(src = "img/logo.svg", class = "inline h-1em md:h-2em px-1")
|
|
||||||
span(class = "font-bold md:text-4xl text-lg font-casual"):
|
|
||||||
text "pkgs"
|
|
||||||
label(`for` = "menu-toggle",
|
|
||||||
class = "cursor-pointer lg:hidden flex items-center px-3 py-2"
|
|
||||||
):
|
|
||||||
text "menu"
|
|
||||||
input(class = "hidden", type = "checkbox", `id` = "menu-toggle")
|
|
||||||
tdiv(class = "lg:flex lg:items-center justify-between hidden w-full lg:w-auto",
|
|
||||||
`id` = "menu"):
|
|
||||||
nav:
|
|
||||||
ul(class = "md:flex items-center"):
|
|
||||||
for (url, msg) in [
|
|
||||||
(packagesGitUrl, "nim-lang/packages:" & packagesHashAbbr),
|
|
||||||
("http://github.com/daylinmorgan/nimpkgs", "source")
|
|
||||||
]:
|
|
||||||
li(class = "p-2 hover:bg-ctp-mantle rounded text-sm"):
|
|
||||||
a(href = url.kstring, class = accent):
|
|
||||||
text msg
|
|
||||||
|
|
||||||
proc includedLinks(pkgs: seq[Package]): HashSet[char] =
|
|
||||||
pkgs.mapIt(it.startChar).toHashSet
|
|
||||||
|
|
||||||
proc letterlink(): VNode =
|
|
||||||
let activeLinks = includedLinks(filteredPackages)
|
|
||||||
result = buildHtml(tdiv(class = "flex flex-wrap md:text-xl text-lg capitalize w-full justify-evenly gap-x-2 md:gap-x-auto")):
|
|
||||||
for l in LowercaseLetters:
|
|
||||||
tdiv(class = "w-5"):
|
|
||||||
if l in activeLinks:
|
|
||||||
a(href = "#" & ($l).kstring):
|
|
||||||
text $l
|
|
||||||
else:
|
|
||||||
span(class = "text-ctp-crust"):
|
|
||||||
text $l
|
|
||||||
|
|
||||||
proc filteredPackagesDom(): VNode =
|
|
||||||
if filteredPackages.len > 0:
|
|
||||||
result = filteredPackages.toDom
|
|
||||||
else:
|
|
||||||
result = buildHtml():
|
|
||||||
text "no match...try a different query"
|
|
||||||
|
|
||||||
|
|
||||||
proc createDom(): VNode =
|
|
||||||
result = buildHtml(tdiv(class = "md:w-3/4 max-w-[95%] md:mx-auto mx-5 md:text-lg text-sm")):
|
|
||||||
headerBar()
|
|
||||||
searchBar()
|
|
||||||
letterlink()
|
|
||||||
tdiv(class = "text-ctp-surfacetwo"):
|
|
||||||
text ($filteredPackages.len & "/" & $allPackages.len) & " packages"
|
|
||||||
if searchInput == "":
|
|
||||||
tdiv():
|
|
||||||
for idx in randomPkgIndices:
|
|
||||||
allPackages[idx].toDom
|
|
||||||
hr()
|
|
||||||
filteredPackagesDom()
|
|
||||||
scrollToTopButton()
|
|
||||||
|
|
||||||
setRenderer createDom
|
|
105
src/packages.nim
105
src/packages.nim
|
@ -1,56 +1,71 @@
|
||||||
import std/[algorithm, strutils, tables]
|
import std/[
|
||||||
import karax/kbase
|
algorithm, asyncjs,
|
||||||
|
strutils, sugar, tables, times
|
||||||
|
]
|
||||||
|
import karax/[kbase]
|
||||||
import jsony
|
import jsony
|
||||||
|
|
||||||
type
|
export algorithm, tables, times, asyncjs, sugar
|
||||||
Package* = object
|
|
||||||
name*, url*, `method`*, description*, license*, web*, doc*, alias*: kstring
|
|
||||||
tags*: seq[kstring]
|
|
||||||
Tag* = object
|
|
||||||
name*: kstring
|
|
||||||
packages*: int
|
|
||||||
|
|
||||||
proc parseHook*(s: string, i: var int, v: var kstring) =
|
proc parseHook*(s: string, i: var int, v: var kstring) =
|
||||||
var str: string
|
var str: string
|
||||||
parseHook(s, i, str)
|
parseHook(s, i, str)
|
||||||
v = cstring(str)
|
v = cstring(str)
|
||||||
|
|
||||||
proc cmpPkgs(a, b: Package): int =
|
type
|
||||||
cmp(toLowerAscii($a.name), toLowerAscii($b.name))
|
Version* = object
|
||||||
|
tag*, hash*: kstring
|
||||||
|
time*: Time
|
||||||
|
|
||||||
proc getPackages(): seq[Package] =
|
NimPackage* = object
|
||||||
const packagesJsonStr = slurp "./packages/packages.json"
|
name*, url*, `method`*, description*,
|
||||||
result = packagesJsonStr.fromJson(seq[Package])
|
license*, web*, doc*, alias*: kstring
|
||||||
result.sort(cmpPkgs)
|
lastCommitHash*: kstring
|
||||||
|
lastCommitTime*: Time
|
||||||
|
versions*: seq[Version]
|
||||||
|
tags*: seq[kstring]
|
||||||
|
deleted*: bool
|
||||||
|
|
||||||
|
NimPkgs* = object
|
||||||
|
updated*: Time
|
||||||
|
packagesHash*: kstring
|
||||||
|
packages*: OrderedTable[string, NimPackage]
|
||||||
|
|
||||||
|
proc newHook*(p: var NimPackage) =
|
||||||
|
p.url = ""
|
||||||
|
p.alias = ""
|
||||||
|
p.`method` = ""
|
||||||
|
p.license = ""
|
||||||
|
p.web = ""
|
||||||
|
p.doc = ""
|
||||||
|
p.description = ""
|
||||||
|
p.alias = ""
|
||||||
|
p.tags = @[]
|
||||||
|
|
||||||
|
proc newHook*(nimpkgs: var NimPkgs) =
|
||||||
|
nimpkgs.packagesHash = ""
|
||||||
|
|
||||||
|
proc parseHook*(s: string, i: var int, v: var Time) =
|
||||||
|
var num: int
|
||||||
|
parseHook(s, i, num)
|
||||||
|
v = fromUnix(num)
|
||||||
|
|
||||||
|
proc sortCommit*(a, b: NimPackage): int =
|
||||||
|
cmp(a.lastCommitTime, b.lastCommitTime)
|
||||||
|
|
||||||
|
proc sortAlphabetical*(a, b: NimPackage): int =
|
||||||
|
cmp(a.name, b.name)
|
||||||
|
|
||||||
|
proc sortVersion*(a, b: NimPackage): int =
|
||||||
|
let lengths = (a.versions.len, b.versions.len)
|
||||||
|
if lengths[0] > 0 and lengths[1] > 0:
|
||||||
|
result = cmp(a.versions[0].time, b.versions[0].time)
|
||||||
|
elif lengths[0] == 0 and lengths[1] == 0:
|
||||||
|
result = sortCommit(a, b)
|
||||||
|
elif lengths[0] == 0:
|
||||||
|
result = -1
|
||||||
|
else:
|
||||||
|
result = 1
|
||||||
|
|
||||||
|
|
||||||
#[
|
proc isAlias*(p: NimPackage): bool {.inline.} = p.alias != ""
|
||||||
import strutils, tables, heapqueue, algorithm
|
|
||||||
iterator topN[T](h: CountTable[T]|Table[T, int], n=10):
|
|
||||||
tuple[cnt: int; key: T] =
|
|
||||||
var q = initHeapQueue[tuple[cnt: int; key: T]]()
|
|
||||||
for key, cnt in h:
|
|
||||||
if q.len < n:
|
|
||||||
q.push((cnt, key))
|
|
||||||
elif cnt > q[0].cnt: # retain 1st seen on tied cnt
|
|
||||||
discard q.replace((cnt, key))
|
|
||||||
while q.len > 0: # q now has top n entries
|
|
||||||
yield q.pop
|
|
||||||
]#
|
|
||||||
|
|
||||||
proc getTags(pkgs: seq[Package]): seq[Tag] =
|
|
||||||
const minPackageCutoff = 10
|
|
||||||
var tags: seq[kstring]
|
|
||||||
for pkg in pkgs:
|
|
||||||
for tag in pkg.tags:
|
|
||||||
tags.add tag
|
|
||||||
for key, cnt in tags.toCountTable:
|
|
||||||
if cnt > minPackageCutoff:
|
|
||||||
result.add Tag(name: key, packages: cnt)
|
|
||||||
|
|
||||||
const
|
|
||||||
packagesHash* {.strdefine.} = "master"
|
|
||||||
packagesHashAbbr* {.strdefine.} = "master"
|
|
||||||
allPackages* = getPackages()
|
|
||||||
allTags* = allPackages.getTags()
|
|
||||||
|
|
22
src/pages/index.nim
Normal file
22
src/pages/index.nim
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import karax/[karaxdsl, vdom]
|
||||||
|
|
||||||
|
import ../components/[search, tag, package]
|
||||||
|
import ../context
|
||||||
|
|
||||||
|
proc render*(): VNode =
|
||||||
|
result = buildHtml(tdiv(class = "justify-center")):
|
||||||
|
tdiv(class = "flex flex-col space-y-5"):
|
||||||
|
tdiv(class = "md:text-4xl text-2xl font-bold font-mono-casual text-center"):
|
||||||
|
text "discover Nim's ecosystem of third-party libraries and tools"
|
||||||
|
tdiv(class = "grow md:w-4/5 mx-auto"):
|
||||||
|
tdiv(class = "flex flex-col md:flex-row grow"):
|
||||||
|
searchBar()
|
||||||
|
tdiv():
|
||||||
|
tdiv():
|
||||||
|
text "explore tags:"
|
||||||
|
ctx.randomTags()
|
||||||
|
tdiv():
|
||||||
|
tdiv():
|
||||||
|
text "recently released versions:"
|
||||||
|
ctx.recentPackageVersionList
|
||||||
|
|
101
src/pages/metrics.nim
Normal file
101
src/pages/metrics.nim
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import std/[algorithm, sequtils, tables, uri, strutils, times]
|
||||||
|
import karax/[kbase, karaxdsl, vdom, jstrutils]
|
||||||
|
|
||||||
|
import ../[context, packages, style, utils]
|
||||||
|
|
||||||
|
type
|
||||||
|
Metrics = object
|
||||||
|
total: int
|
||||||
|
isDeleted: int
|
||||||
|
isAlias: int
|
||||||
|
isVersioned: int
|
||||||
|
commitMonth: int
|
||||||
|
commitYear: int
|
||||||
|
tags, domains, authors, license, : seq[(string, int)]
|
||||||
|
|
||||||
|
|
||||||
|
proc sortCounts(x, y: (string, int)): int =
|
||||||
|
cmp(x[1], y[1])
|
||||||
|
|
||||||
|
proc calculateMetics(ctx: Context): Metrics =
|
||||||
|
let currentTime = getTime()
|
||||||
|
var
|
||||||
|
tags: CountTable[string]
|
||||||
|
domains: CountTable[string]
|
||||||
|
authors: CountTable[string]
|
||||||
|
license: CountTable[string]
|
||||||
|
|
||||||
|
result.total = ctx.nimpkgs.packages.len
|
||||||
|
for pkg in ctx.nimpkgs.packages.values():
|
||||||
|
let timeSinceLastCommit = (currentTime - pkg.lastCommitTime)
|
||||||
|
|
||||||
|
if pkg.versions.len > 0: inc result.isVersioned
|
||||||
|
if pkg.isAlias: inc result.isAlias
|
||||||
|
if pkg.deleted: inc result.isDeleted
|
||||||
|
if pkg.license != "": license.inc $pkg.license
|
||||||
|
if timeSinceLastCommit < initDuration(weeks = 52):
|
||||||
|
inc result.commitYear
|
||||||
|
if timeSinceLastCommit < initDuration(days = 30):
|
||||||
|
inc result.commitMonth
|
||||||
|
if pkg.url != "":
|
||||||
|
let u = parseUri($pkg.url)
|
||||||
|
domains.inc u.hostname
|
||||||
|
authors.inc u.path.split("/")[1]
|
||||||
|
if pkg.tags.len > 0:
|
||||||
|
for tag in pkg.tags:
|
||||||
|
tags.inc $tag
|
||||||
|
|
||||||
|
result.tags = tags.pairs.toSeq()
|
||||||
|
result.domains = domains.pairs.toSeq()
|
||||||
|
result.authors = authors.pairs.toSeq()
|
||||||
|
result.license = license.pairs.toSeq()
|
||||||
|
result.tags.sort(sortCounts, order = Descending)
|
||||||
|
result.domains.sort(sortCounts, order = Descending)
|
||||||
|
result.authors.sort(sortCounts, order = Descending)
|
||||||
|
result.license.sort(sortCounts, order = Descending)
|
||||||
|
|
||||||
|
|
||||||
|
proc totalsTable(metrics: Metrics): VNode =
|
||||||
|
let cellClass = "border md:px-10 px-5" & borderStyle
|
||||||
|
buildHtml(tdiv(class = "my-10")):
|
||||||
|
tdiv:
|
||||||
|
h2(class = "text-2xl"): text "totals"
|
||||||
|
table(class = "bg-ctp-mantle"):
|
||||||
|
tr:
|
||||||
|
th(class = cellClass): text "category"
|
||||||
|
th(class = cellClass): text "number"
|
||||||
|
for (msg, metric) in [
|
||||||
|
("total", metrics.total),
|
||||||
|
("authors/orgs", metrics.authors.len),
|
||||||
|
("deleted", metrics.isDeleted),
|
||||||
|
("alias", metrics.isAlias),
|
||||||
|
("versioned", metrics.isVersioned),
|
||||||
|
("last commit (< 1 year)", metrics.commitYear),
|
||||||
|
("last commit (< 30 days)", metrics.commitMonth),
|
||||||
|
]:
|
||||||
|
tr:
|
||||||
|
td(class = cellClass): text msg
|
||||||
|
td(class = cellClass): text metric.jss
|
||||||
|
|
||||||
|
|
||||||
|
proc blockCountList(itemList: seq[(string, int)], title: string): VNode =
|
||||||
|
buildHtml(tdiv(class = "border-t-1 border-dashed my-5 py-5")):
|
||||||
|
h2(class = "text-2xl"): text title.jss
|
||||||
|
for (item, cnt) in itemList:
|
||||||
|
tdiv(class = "inline-block p-2 m-1 border rounded space-x-2" & borderStyle):
|
||||||
|
span: text item.kstring & ":"
|
||||||
|
span: text kstring($cnt)
|
||||||
|
|
||||||
|
|
||||||
|
proc render*(): VNode =
|
||||||
|
let metrics = ctx.calculateMetics()
|
||||||
|
result = buildHtml(tdiv):
|
||||||
|
h2(class = "text-4xl"):
|
||||||
|
text "metrics"
|
||||||
|
tdiv(class = "my-1"):
|
||||||
|
text "a small collection of metrics from the current nim-lang/packages"
|
||||||
|
metrics.totalsTable
|
||||||
|
blockCountList(metrics.tags[0..20], title = "tags (top 20)")
|
||||||
|
blockCountList(metrics.authors[0..20], title = "authors (top 20)")
|
||||||
|
blockCountList(metrics.license[0..20], title = "licenses (top 20)")
|
||||||
|
blockCountList(metrics.domains, title = "domains")
|
8
src/pages/notfound.nim
Normal file
8
src/pages/notfound.nim
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import karax/[karaxdsl, vdom]
|
||||||
|
|
||||||
|
proc render*(): VNode =
|
||||||
|
result = buildHtml:
|
||||||
|
tdiv(class = "mx-auto text-center"):
|
||||||
|
span(class = "text-9xl lg:text-[25rem] font-black my-5"):
|
||||||
|
text "404"
|
||||||
|
|
90
src/pages/package.nim
Normal file
90
src/pages/package.nim
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import std/[algorithm, sugar]
|
||||||
|
import karax/[kbase, karaxdsl, vdom, jstrutils]
|
||||||
|
|
||||||
|
import ../[context, packages, style]
|
||||||
|
import ../components/[tag, package]
|
||||||
|
import ../utils
|
||||||
|
import notfound
|
||||||
|
|
||||||
|
proc versionTable(pkg: NimPackage): VNode =
|
||||||
|
var versions = pkg.versions
|
||||||
|
versions.sort((a, b: Version) => cmp(a.time, b.time), order = Descending)
|
||||||
|
|
||||||
|
buildHtml(tdiv(class = "my-5 p-10 bg-ctp-crust rounded")):
|
||||||
|
table(class = "table-auto w-full text-center"):
|
||||||
|
tr:
|
||||||
|
th: text "version"
|
||||||
|
th: text "released"
|
||||||
|
th: text "hash"
|
||||||
|
for version in versions:
|
||||||
|
tr:
|
||||||
|
td: text version.tag
|
||||||
|
td: text version.time.format("yyyy-MM-dd")
|
||||||
|
td: text ($version.hash)[0..8]
|
||||||
|
|
||||||
|
proc renderAlias(pkg: NimPackage): VNode = buildHtml:
|
||||||
|
tdiv:
|
||||||
|
text pkg.name & "is alias for "
|
||||||
|
a(href = "#/pkg/" & pkg.alias):
|
||||||
|
text pkg.alias
|
||||||
|
|
||||||
|
proc renderLinks(pkg: NimPackage): VNode = buildHtml(tdiv):
|
||||||
|
tdiv: text "links:"
|
||||||
|
tdiv:
|
||||||
|
pkg.projectUrl
|
||||||
|
if pkg.web != "" and pkg.web != pkg.url:
|
||||||
|
tdiv():
|
||||||
|
a(href = pkg.web, class = "flex items-center space-x-2"):
|
||||||
|
tdiv(class = "i-mdi-web shrink-0")
|
||||||
|
span: text pkg.web.noProtocol
|
||||||
|
if pkg.doc != "":
|
||||||
|
tdiv():
|
||||||
|
a(href = pkg.doc, class = "flex items-center space-x-2"):
|
||||||
|
tdiv(class = "i-mdi-file-outline shrink-0")
|
||||||
|
span: text pkg.doc.noProtocol
|
||||||
|
|
||||||
|
proc getTimeSinceCommit(pkg: NimPackage): kstring =
|
||||||
|
if pkg.lastCommitTime == fromUnix(0): "unknown".jss
|
||||||
|
else:
|
||||||
|
let d = getTime() - pkg.lastCommitTime
|
||||||
|
d.inDays.jss & " days ago"
|
||||||
|
|
||||||
|
proc renderPkgInfo(pkg: NimPackage): VNode =
|
||||||
|
buildHtml:
|
||||||
|
tdiv(class = "space-y-5 text-2xl"):
|
||||||
|
tdiv(class = "md:text-4xl text-xl"):
|
||||||
|
text pkg.description
|
||||||
|
pkg.renderLinks
|
||||||
|
tdiv:
|
||||||
|
tdiv: text "license:"
|
||||||
|
text pkg.license.jss
|
||||||
|
tdiv:
|
||||||
|
tdiv: text "tags:"
|
||||||
|
pkg.tags.renderTags
|
||||||
|
tdiv:
|
||||||
|
tdiv: text "last commit:"
|
||||||
|
text pkg.getTimeSinceCommit
|
||||||
|
tdiv:
|
||||||
|
tdiv: text "usage:"
|
||||||
|
tdiv(class = "bg-ctp-surfacezero rounded my-2 mx-3 p-2 w-auto"):
|
||||||
|
pre:
|
||||||
|
text "nimble install " & pkg.name
|
||||||
|
pre:
|
||||||
|
text "atlas use " & pkg.name
|
||||||
|
|
||||||
|
proc render*(packageName: string): VNode =
|
||||||
|
if packageName notin ctx.nimpkgs.packages: return notfound.render()
|
||||||
|
let pkg = ctx.nimpkgs.packages[packageName]
|
||||||
|
result = buildHtml(tdiv(class = "flex flex-col")):
|
||||||
|
if pkg.deleted:
|
||||||
|
tdiv(class = "md:text-5xl text-2xl text-ctp-red my-5 "):
|
||||||
|
tdiv(class = "flex items-center md:text-5xl text-2xl font-mono-casual font-black"):
|
||||||
|
tdiv(class = "i-mdi-alert inline-block")
|
||||||
|
span: text "WARNING!"
|
||||||
|
text "The provided url for this package is unreachable, it may have been deleted."
|
||||||
|
tdiv(class = "bg-ctp-mantle rounded p-5"):
|
||||||
|
h2(class = textStyle & "text-3xl md:text-6xl font-bold font-mono-casual my-2"):
|
||||||
|
text pkg.name
|
||||||
|
if pkg.isAlias: pkg.renderAlias
|
||||||
|
else: pkg.renderPkgInfo
|
||||||
|
if pkg.versions.len > 0: pkg.versionTable
|
2
src/pages/pages.nim
Normal file
2
src/pages/pages.nim
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import index, search, package, notfound, metrics
|
||||||
|
export index, search, package, notfound, metrics
|
106
src/pages/search.nim
Normal file
106
src/pages/search.nim
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import std/[algorithm, strutils, sequtils, dom]
|
||||||
|
|
||||||
|
import karax/[kbase, karax, karaxdsl, vdom, jstrutils, kdom]
|
||||||
|
|
||||||
|
import ../[packages, context]
|
||||||
|
import ../components/[package, search]
|
||||||
|
import ../utils
|
||||||
|
|
||||||
|
type
|
||||||
|
SortMethod = enum
|
||||||
|
smAlphabetical, smCommitAge, smVersionAge
|
||||||
|
PageContext = object
|
||||||
|
sortMethod: SortMethod = smAlphabetical
|
||||||
|
filteredPackages: seq[NimPackage]
|
||||||
|
search: kstring
|
||||||
|
|
||||||
|
var pgCtx = PageContext()
|
||||||
|
|
||||||
|
proc scrollToAnchor(a: string): proc() =
|
||||||
|
result = proc() =
|
||||||
|
let d = getVNodeById(a)
|
||||||
|
scrollIntoView(d.dom)
|
||||||
|
|
||||||
|
proc letterlink(activeLinks: seq[char]): VNode = buildHtml:
|
||||||
|
tdiv(
|
||||||
|
class = "flex flex-wrap md:text-xl text-lg capitalize w-full justify-evenly gap-x-2 md:gap-x-auto"
|
||||||
|
):
|
||||||
|
for l in LowercaseLetters:
|
||||||
|
tdiv(class = "w-5"):
|
||||||
|
if l in activeLinks:
|
||||||
|
span(
|
||||||
|
class = "link underline decoration-dotted",
|
||||||
|
onClick = scrollToAnchor($l)
|
||||||
|
): text l.jss
|
||||||
|
else: span(class = "text-ctp-crust"): text l.jss
|
||||||
|
|
||||||
|
proc startChar(p: NimPackage): char =
|
||||||
|
p.name[0].toLowerAscii
|
||||||
|
|
||||||
|
proc alphabeticalPackageList(pkgs: seq[NimPackage]): VNode =
|
||||||
|
var charPackages: OrderedTable[char, seq[NimPackage]]
|
||||||
|
for pkg in pkgs:
|
||||||
|
let c = pkg.startChar
|
||||||
|
if c in charPackages:
|
||||||
|
charPackages[c].add pkg
|
||||||
|
else:
|
||||||
|
charPackages[c] = @[pkg]
|
||||||
|
result = buildHtml(tdiv):
|
||||||
|
letterlink(charPackages.keys.toSeq)
|
||||||
|
for c, packages in charPackages:
|
||||||
|
tdiv(`id` = c.jss)
|
||||||
|
for pkg in packages:
|
||||||
|
pkg.card
|
||||||
|
|
||||||
|
proc selectSortMethod() =
|
||||||
|
let v = getVNodeById("sort-select").getInputText
|
||||||
|
pgCtx.sortMethod = SortMethod(parseInt(v))
|
||||||
|
|
||||||
|
proc sortSelector(): VNode =
|
||||||
|
buildHtml(tdiv(class = "flex items-center")):
|
||||||
|
label(`for` = "sort-select"): text "sort:"
|
||||||
|
select(class = "bg-ctp-crust rounded p-3", name = "sort",
|
||||||
|
`id` = "sort-select", onChange = selectSortMethod):
|
||||||
|
for i, msg in ["alphabetical", "recent commit", "recent version"]:
|
||||||
|
if i == ord(pgCtx.sortMethod):
|
||||||
|
option(value = ($i).cstring, selected = ""): text msg
|
||||||
|
else:
|
||||||
|
option(value = ($i).cstring): text msg
|
||||||
|
|
||||||
|
proc filteredPackagesDom(): VNode =
|
||||||
|
if pgCtx.filteredPackages.len == 0:
|
||||||
|
return buildHtml(): text "no match...try a different query"
|
||||||
|
else:
|
||||||
|
case pgCtx.sortMethod:
|
||||||
|
of smAlphabetical:
|
||||||
|
pgCtx.filteredPackages.sort(sortAlphabetical)
|
||||||
|
of smCommitAge:
|
||||||
|
pgCtx.filteredPackages.sort(sortCommit, order = Descending)
|
||||||
|
of smVersionAge:
|
||||||
|
pgCtx.filteredPackages.sort(sortVersion, order = Descending)
|
||||||
|
|
||||||
|
result = buildHtml(tdiv):
|
||||||
|
tdiv(class = "text-ctp-surfacetwo"):
|
||||||
|
text ($pgCtx.filteredPackages.len & "/" & $ctx.nimpkgs.packages.len) & " packages"
|
||||||
|
case pgCtx.sortMethod:
|
||||||
|
of smAlphabetical:
|
||||||
|
pgCtx.filteredPackages.alphabeticalPackageList
|
||||||
|
else:
|
||||||
|
for pkg in pgCtx.filteredPackages:
|
||||||
|
pkg.card
|
||||||
|
|
||||||
|
proc update(pgCtx: var PageContext) =
|
||||||
|
pgCtx.filteredPackages = ctx.nimpkgs.packages.values().toSeq()
|
||||||
|
pgCtx.search = getSearchFromUri()
|
||||||
|
pgCtx.filteredPackages = searchPackages(parseQuery(pgCtx.search))
|
||||||
|
|
||||||
|
proc render*(): VNode =
|
||||||
|
pgCtx.update
|
||||||
|
result =
|
||||||
|
buildHtml(tdiv):
|
||||||
|
tdiv(class = "flex md:flex-row flex-col md:space-x-5"):
|
||||||
|
searchBar(value = pgCtx.search)
|
||||||
|
sortSelector()
|
||||||
|
filteredPackagesDom()
|
||||||
|
|
||||||
|
|
25
src/style.nim
Normal file
25
src/style.nim
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import std/random
|
||||||
|
import karax/[kbase, jstrutils]
|
||||||
|
|
||||||
|
randomize()
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
"flamingo",
|
||||||
|
"pink",
|
||||||
|
"mauve",
|
||||||
|
"red",
|
||||||
|
"maroon",
|
||||||
|
"peach",
|
||||||
|
"yellow",
|
||||||
|
"green",
|
||||||
|
"teal",
|
||||||
|
"sky",
|
||||||
|
"sapphire",
|
||||||
|
"blue",
|
||||||
|
"lavender"
|
||||||
|
]
|
||||||
|
let
|
||||||
|
accent* = (colors.sample() & " ").kstring
|
||||||
|
textStyle* = (" text-ctp-" & accent & " ").kstring
|
||||||
|
borderStyle* = (" border rounded b-ctp-" & accent & " ").kstring
|
||||||
|
|
31
src/utils.nim
Normal file
31
src/utils.nim
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import std/[strutils, uri]
|
||||||
|
import std/jsffi except `&`
|
||||||
|
import jsconsole
|
||||||
|
export jsconsole
|
||||||
|
|
||||||
|
import karax/[kbase, karax, vdom, kdom]
|
||||||
|
|
||||||
|
proc jss*[T](arg: T): kstring = ($arg).kstring
|
||||||
|
proc jss*(arg: kstring): kstring = arg
|
||||||
|
|
||||||
|
proc noProtocol*(s: kstring): kstring =
|
||||||
|
($s)
|
||||||
|
.replace("http://", "")
|
||||||
|
.replace("https://", "")
|
||||||
|
.jss
|
||||||
|
|
||||||
|
|
||||||
|
func replace*(c: kstring, sub: string, by = " "): kstring =
|
||||||
|
($c).replace(sub, by).jss
|
||||||
|
|
||||||
|
proc setSearchUrl*(searchQuery: kstring): proc() =
|
||||||
|
proc() =
|
||||||
|
var url = parseUri($window.location.href)
|
||||||
|
url.anchor = "/search"
|
||||||
|
url = url ? {"query": $searchQuery}
|
||||||
|
window.history.pushState(js{}, "".jss, url.jss)
|
||||||
|
let d = getVNodeById("search")
|
||||||
|
let node = d.dom
|
||||||
|
scrollIntoView(node)
|
||||||
|
redraw()
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { variants } from "@catppuccin/palette";
|
import { variants } from "@catppuccin/palette";
|
||||||
import { defineConfig, presetUno } from "unocss";
|
import { defineConfig, presetUno, presetIcons } from "unocss";
|
||||||
|
|
||||||
const generatePalette = () => {
|
const generatePalette = (): { [key: string]: string } => {
|
||||||
const colors = {};
|
const colors: { [key: string]: string } = {};
|
||||||
|
|
||||||
Object.keys(variants.mocha).forEach((colorName) => {
|
Object.keys(variants.mocha).forEach((colorName) => {
|
||||||
const sanitizedName = colorName
|
const sanitizedName = colorName
|
||||||
|
@ -27,7 +27,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
layer: "mycss",
|
layer: "mycss",
|
||||||
getCSS: ({ theme }) => `
|
getCSS: () => `
|
||||||
body {
|
body {
|
||||||
font-family: 'Recursive', monospace;
|
font-family: 'Recursive', monospace;
|
||||||
font-variation-settings: 'MONO' 1;
|
font-variation-settings: 'MONO' 1;
|
||||||
|
@ -37,28 +37,53 @@ export default defineConfig({
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
text-decoration: underline dotted;
|
text-decoration: underline dotted;
|
||||||
color: ${theme.colors.ctp.rosewater};
|
color: ${catppuccinColors.rosewater};
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
color: ${theme.colors.ctp.mauve};
|
color: ${catppuccinColors.mauve};
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
// loading animation
|
||||||
|
.lds-dual-ring {
|
||||||
|
display: inline-block;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
.lds-dual-ring:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 6px solid #fff;
|
||||||
|
border-color: #fff transparent #fff transparent;
|
||||||
|
animation: lds-dual-ring 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes lds-dual-ring {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// accent color is dynamically generated
|
// accent color is dynamically generated
|
||||||
safelist: Object.keys(catppuccinColors).flatMap((key: string) => [`text-ctp-${key}`, `b-ctp-${key}`]),
|
safelist: Object.keys(catppuccinColors).flatMap((key: string) => [`text-ctp-${key}`, `b-ctp-${key}`]),
|
||||||
presets: [presetUno()],
|
presets: [presetUno(), presetIcons()],
|
||||||
rules: [
|
rules: [
|
||||||
["font-casual", { "font-variation-settings": "'CASL' 1;" }],
|
["font-casual", { "font-variation-settings": "'CASL' 1;" }],
|
||||||
["font-mono-casual", { "font-variation-settings": "'MONO' 1, 'CASL' 1;" }],
|
["font-mono-casual", { "font-variation-settings": "'MONO' 1, 'CASL' 1;" }],
|
||||||
],
|
],
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
btn: "border-1 border-solid rounded border-ctp-mauve flex flex-row hover:border-ctp-sky hover:text-ctp-rosewater m-2",
|
link: "cursor-pointer text-ctp-rosewater hover:text-ctp-mauve",
|
||||||
// link: "underline text-ctp-rosewater"
|
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
ctp: generatePalette(),
|
ctp: catppuccinColors,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
layers: {
|
layers: {
|
||||||
|
|
Loading…
Reference in a new issue