mirror of
https://github.com/daylinmorgan/nimpkgs.git
synced 2024-11-13 23:27: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:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 2 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -133,7 +133,10 @@ dist
|
|||
##
|
||||
|
||||
*.workspace
|
||||
nim.cfg
|
||||
site/uno.css
|
||||
site/nimpkgs.js
|
||||
src/nim.cfg
|
||||
src/packages
|
||||
site/app.js
|
||||
src/*.js
|
||||
|
||||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
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]
|
||||
|
||||
--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])
|
||||
switch("backend","js")
|
||||
|
||||
task setup, "run atlas init":
|
||||
exec "atlas init --deps=.workspace"
|
||||
exec "atlas install"
|
||||
|
||||
task build, "build":
|
||||
let (short,long) = getCommitInfo()
|
||||
selfExec fmt"js -o:site/nimpkgs.js -d:packagesHash:{long} -d:packagesHashAbbr:{short} -d:release src/nimpkgs.nim"
|
||||
selfExec "js -o:site/app.js -d:release src/app.nim"
|
||||
exec "pnpm run build"
|
||||
|
||||
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
|
||||
|
||||
requires "nim >= 2.0.0"
|
||||
requires "karax"
|
||||
requires "jsony"
|
||||
|
||||
|
||||
|
||||
|
|
13
package.json
13
package.json
|
@ -4,22 +4,23 @@
|
|||
"server": "http-server ./site",
|
||||
"watch": "nim watch",
|
||||
"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:prd": "unocss \"./site/**/*.html\" \"./src/**/*.nim\" --out-file site/uno.css"
|
||||
},
|
||||
"author": "Daylin Morgan",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devDependencies": {
|
||||
"@catppuccin/palette": "^0.2.0",
|
||||
"@iconify-json/mdi": "^1.1.55",
|
||||
"@iconify-json/simple-icons": "^1.1.79",
|
||||
"@types/promise-fs": "^2.1.5",
|
||||
"@unocss/cli": "^0.57.3",
|
||||
"@unocss/preset-icons": "^0.57.7",
|
||||
"@unocss/reset": "^0.57.3",
|
||||
"unocss": "^0.57.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^8.2.2",
|
||||
"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>
|
||||
<meta charset="UTF-8"/>
|
||||
<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>
|
||||
<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">
|
||||
</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>
|
||||
<script type="text/javascript" src="nimpkgs.js"></script>
|
||||
<script type="text/javascript" src="app.js"></script>
|
||||
</body>
|
||||
</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]
|
||||
|
||||
include karax/prelude
|
||||
import karax/vstyles
|
||||
import karax/[karax, karaxdsl, vdom, vstyles]
|
||||
|
||||
proc showScrollToTop() =
|
||||
# TODO: only show button when scrolling up
|
||||
|
@ -18,14 +16,14 @@ proc scrollToTop*() =
|
|||
document.body.scrollTop = 0
|
||||
document.documentElement.scrollTop = 0
|
||||
|
||||
document.addEventListener("scroll", (e: Event) => showScrollToTop())
|
||||
document.addEventListener("scroll", (e: dom.Event) => showScrollToTop())
|
||||
|
||||
proc scrollToTopButton*(): VNode =
|
||||
|
||||
result = buildHtml(tdiv):
|
||||
button(
|
||||
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 " &
|
||||
" bg-ctp-rosewater hover:bg-ctp-mauve text-ctp-mantle ",
|
||||
`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 karax/kbase
|
||||
|
||||
import std/[
|
||||
algorithm, asyncjs,
|
||||
strutils, sugar, tables, times
|
||||
]
|
||||
import karax/[kbase]
|
||||
import jsony
|
||||
|
||||
type
|
||||
Package* = object
|
||||
name*, url*, `method`*, description*, license*, web*, doc*, alias*: kstring
|
||||
tags*: seq[kstring]
|
||||
Tag* = object
|
||||
name*: kstring
|
||||
packages*: int
|
||||
export algorithm, tables, times, asyncjs, sugar
|
||||
|
||||
proc parseHook*(s: string, i: var int, v: var kstring) =
|
||||
var str: string
|
||||
parseHook(s, i, str)
|
||||
v = cstring(str)
|
||||
|
||||
proc cmpPkgs(a, b: Package): int =
|
||||
cmp(toLowerAscii($a.name), toLowerAscii($b.name))
|
||||
type
|
||||
Version* = object
|
||||
tag*, hash*: kstring
|
||||
time*: Time
|
||||
|
||||
proc getPackages(): seq[Package] =
|
||||
const packagesJsonStr = slurp "./packages/packages.json"
|
||||
result = packagesJsonStr.fromJson(seq[Package])
|
||||
result.sort(cmpPkgs)
|
||||
NimPackage* = object
|
||||
name*, url*, `method`*, description*,
|
||||
license*, web*, doc*, alias*: kstring
|
||||
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
|
||||
|
||||
|
||||
#[
|
||||
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()
|
||||
proc isAlias*(p: NimPackage): bool {.inline.} = p.alias != ""
|
||||
|
|
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 { variants } from "@catppuccin/palette";
|
||||
import { defineConfig, presetUno } from "unocss";
|
||||
import { defineConfig, presetUno, presetIcons } from "unocss";
|
||||
|
||||
const generatePalette = () => {
|
||||
const colors = {};
|
||||
const generatePalette = (): { [key: string]: string } => {
|
||||
const colors: { [key: string]: string } = {};
|
||||
|
||||
Object.keys(variants.mocha).forEach((colorName) => {
|
||||
const sanitizedName = colorName
|
||||
|
@ -27,7 +27,7 @@ export default defineConfig({
|
|||
},
|
||||
{
|
||||
layer: "mycss",
|
||||
getCSS: ({ theme }) => `
|
||||
getCSS: () => `
|
||||
body {
|
||||
font-family: 'Recursive', monospace;
|
||||
font-variation-settings: 'MONO' 1;
|
||||
|
@ -37,28 +37,53 @@ export default defineConfig({
|
|||
}
|
||||
a {
|
||||
text-decoration: underline dotted;
|
||||
color: ${theme.colors.ctp.rosewater};
|
||||
color: ${catppuccinColors.rosewater};
|
||||
}
|
||||
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
|
||||
safelist: Object.keys(catppuccinColors).flatMap((key: string) => [`text-ctp-${key}`, `b-ctp-${key}`]),
|
||||
presets: [presetUno()],
|
||||
presets: [presetUno(), presetIcons()],
|
||||
rules: [
|
||||
["font-casual", { "font-variation-settings": "'CASL' 1;" }],
|
||||
["font-mono-casual", { "font-variation-settings": "'MONO' 1, 'CASL' 1;" }],
|
||||
],
|
||||
shortcuts: {
|
||||
btn: "border-1 border-solid rounded border-ctp-mauve flex flex-row hover:border-ctp-sky hover:text-ctp-rosewater m-2",
|
||||
// link: "underline text-ctp-rosewater"
|
||||
link: "cursor-pointer text-ctp-rosewater hover:text-ctp-mauve",
|
||||
},
|
||||
theme: {
|
||||
colors: {
|
||||
ctp: generatePalette(),
|
||||
ctp: catppuccinColors,
|
||||
},
|
||||
},
|
||||
layers: {
|
||||
|
|
Loading…
Reference in a new issue