Compare commits

...

8 commits

29 changed files with 1097 additions and 512 deletions

View file

@ -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
View file

@ -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

View file

@ -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).

View file

@ -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"

View file

@ -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"

View file

@ -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"
} }
} }

File diff suppressed because it is too large Load diff

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

View file

@ -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
View 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
View file

@ -0,0 +1 @@
--backend:js

View file

@ -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
View 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
View 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

View 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
View 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
View 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
View 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

View file

@ -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

View file

@ -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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
import index, search, package, notfound, metrics
export index, search, package, notfound, metrics

106
src/pages/search.nim Normal file
View 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
View 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
View 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()

View file

@ -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: {