Compare commits
56 Commits
Author | SHA1 | Date |
---|---|---|
Bauke | cda4cba1bf | |
Bauke | a830007ff2 | |
Bauke | 8b5d8c047b | |
Bauke | c9db2e7b30 | |
Bauke | 44b7fdbcb0 | |
Bauke | e2215c8a3c | |
Bauke | 0c56e3f368 | |
Bauke | 4bddf6c9f7 | |
Bauke | 37dfcfc1f0 | |
Bauke | 9cd91dc788 | |
Bauke | 537c8c0f3e | |
Bauke | 2e828eb087 | |
Bauke | cb2e5e9f17 | |
Bauke | 7129322105 | |
Bauke | 2a4f6f2c48 | |
Bauke | 9f0f5a46b7 | |
Bauke | 1a53885993 | |
Bauke | 8d50aee844 | |
Bauke | 85469eabf7 | |
Bauke | 6d121fb7b4 | |
Bauke | 279f40004f | |
Bauke | cc98bd87f9 | |
Bauke | ebfae2069d | |
Bauke | 7b881c6162 | |
Bauke | 61e59cb88c | |
Bauke | 24640cce06 | |
Bauke | f47b53b928 | |
Bauke | e48d500a0c | |
Bauke | ac9acff203 | |
Bauke | fb47a795ec | |
Bauke | 57f3b64958 | |
Bauke | ff4e486455 | |
Bauke | 2762395adb | |
Bauke | af0a0bc71b | |
Bauke | cdc61576c1 | |
Bauke | 0b2026daf3 | |
Bauke | d9895d7b87 | |
Bauke | 18be96943e | |
Bauke | 421c64cd49 | |
Bauke | 71331a6ee0 | |
Bauke | bc02927d8b | |
Bauke | 25c45d9d01 | |
Bauke | 98034ae621 | |
Bauke | 521de9ec70 | |
Bauke | 09656ee76c | |
Bauke | a3761eb2bb | |
Bauke | 319a352d2e | |
Bauke | d4d9a8b0f0 | |
Bauke | 70746f4079 | |
Bauke | 2f5c75a68f | |
Bauke | 7e59bfbb6b | |
Bauke | 0b9b9fa162 | |
Bauke | aeb3baaca2 | |
Bauke | e3faf966df | |
Bauke | 67457393d6 | |
Bauke | b0a3b960cc |
|
@ -1,107 +1,3 @@
|
||||||
# Logs
|
.direnv/
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
node_modules/
|
||||||
jspm_packages/
|
public/
|
||||||
|
|
||||||
# TypeScript v1 declaration files
|
|
||||||
typings/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variables file
|
|
||||||
.env
|
|
||||||
.env.test
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Site output directory
|
|
||||||
/public/
|
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
# href+
|
# href+
|
||||||
|
|
||||||
> https://href.plus
|
> **Song-linking website using MusicBrainz data.**
|
||||||
|
|
||||||
* If you find a release that has a link which just shows the domain and not a proper name, please post it in [#5](https://github.com/Bauke/href-plus/issues/5). Thank you!
|
## License
|
||||||
|
|
||||||
|
Distributed under the [GPL-3.0-or-later](https://spdx.org/licenses/GPL-3.0-or-later.html) license, see [LICENSE](https://git.bauke.xyz/Bauke/href-plus/src/branch/main/LICENSE) for more information.
|
||||||
|
|
||||||
|
Icons in [`source/assets/icons`](source/assets/icons) belong to their respective owners.
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1705309234,
|
||||||
|
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1705883077,
|
||||||
|
"narHash": "sha256-ByzHHX3KxpU1+V0erFy8jpujTufimh6KaS/Iv3AciHk=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "5f5210aa20e343b7e35f40c033000db0ef80d7b9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"id": "nixpkgs",
|
||||||
|
"type": "indirect"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = import ./shell.nix { inherit pkgs; };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
43
package.json
|
@ -1,8 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "href-plus",
|
"name": "href-plus",
|
||||||
"version": "0.1.0",
|
"description": "Song-linking website using MusicBrainz data.",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
|
"version": "0.1.0",
|
||||||
"author": "Bauke <me@bauke.xyz>",
|
"author": "Bauke <me@bauke.xyz>",
|
||||||
|
"repository": "https://git.bauke.xyz/Bauke/href-plus",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"test": "xo && stylelint 'source/**/*.scss' && tsc",
|
"test": "xo && stylelint 'source/**/*.scss' && tsc",
|
||||||
|
@ -10,32 +13,36 @@
|
||||||
"deploy:netlify": "netlify deploy --prod -d 'public' -s 'href.plus'"
|
"deploy:netlify": "netlify deploy --prod -d 'public' -s 'href.plus'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^4.5.1",
|
"@fontsource/inter": "^5.0.16",
|
||||||
"htm": "^3.1.0",
|
"htm": "^3.1.1",
|
||||||
"modern-normalize": "^1.1.0",
|
"modern-normalize": "^2.0.0",
|
||||||
"preact": "^10.6.4",
|
"preact": "^10.19.3",
|
||||||
"preact-router": "^3.2.1"
|
"preact-router": "^4.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^17.0.5",
|
"@types/node": "^20.11.5",
|
||||||
"postcss": "^8.4.5",
|
"netlify-cli": "^17.15.1",
|
||||||
"sass": "^1.45.1",
|
"postcss": "^8.4.33",
|
||||||
"stylelint": "^14.2.0",
|
"sass": "^1.70.0",
|
||||||
"stylelint-config-standard-scss": "^3.0.0",
|
"stylelint": "^16.2.0",
|
||||||
"typescript": "^4.5.4",
|
"stylelint-config-standard-scss": "^13.0.0",
|
||||||
"vite": "^2.7.10",
|
"typescript": "^5.3.3",
|
||||||
"xo": "^0.47.0"
|
"vite": "^5.0.12",
|
||||||
|
"xo": "^0.56.0"
|
||||||
},
|
},
|
||||||
"stylelint": {
|
"stylelint": {
|
||||||
"extends": [
|
"extends": [
|
||||||
"stylelint-config-standard-scss"
|
"stylelint-config-standard-scss"
|
||||||
],
|
]
|
||||||
"rules": {
|
|
||||||
"string-quotes": "single"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"xo": {
|
"xo": {
|
||||||
"prettier": true,
|
"prettier": true,
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-declaration-merging": "off",
|
||||||
|
"n/file-extension-in-import": "off"
|
||||||
|
},
|
||||||
"space": true
|
"space": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
10583
pnpm-lock.yaml
|
@ -0,0 +1,7 @@
|
||||||
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
|
||||||
|
with pkgs;
|
||||||
|
|
||||||
|
mkShell rec {
|
||||||
|
packages = [ cargo-make nodejs nodePackages.pnpm ];
|
||||||
|
}
|
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 42 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 13 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 21 KiB |
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /release/
|
||||||
|
Disallow: /settings
|
|
@ -1,19 +1,21 @@
|
||||||
.search-bar {
|
.search-bar {
|
||||||
|
max-width: 70rem;
|
||||||
|
|
||||||
input[type='text'] {
|
input[type='text'] {
|
||||||
background-color: var(--background-2);
|
background-color: var(--background-2);
|
||||||
border: 1px solid var(--foreground-1);
|
border: var(--border-size) solid var(--foreground-1);
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
border: 1px solid var(--foreground-1);
|
border: var(--border-size) solid var(--foreground-1);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
max-height: 50vh;
|
max-height: 50vh;
|
||||||
overflow-y: scroll;
|
overflow-y: auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,4 +47,17 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.load-more {
|
||||||
|
background-color: var(--background-1);
|
||||||
|
border: var(--border-size) solid var(--foreground-1);
|
||||||
|
color: var(--foreground-1);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--foreground-1);
|
||||||
|
color: var(--background-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
|
|
||||||
.explainer {
|
.explainer {
|
||||||
background-color: var(--background-2);
|
background-color: var(--background-2);
|
||||||
|
border: var(--border-size) solid var(--foreground-2);
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
max-width: 70rem;
|
max-width: 70rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.release {
|
.release {
|
||||||
background-color: var(--background-2);
|
background-color: var(--background-2);
|
||||||
box-shadow: 0 0 1rem #000;
|
border: var(--border-size) solid var(--foreground-2);
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
}
|
}
|
||||||
|
@ -39,15 +39,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
border-top: 2px solid var(--background-1);
|
border-top: var(--border-size) solid var(--background-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-link {
|
.release-link {
|
||||||
a {
|
a {
|
||||||
|
align-items: center;
|
||||||
background-color: var(--background-1);
|
background-color: var(--background-1);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
display: block;
|
display: flex;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
@ -55,4 +56,14 @@
|
||||||
background-color: var(--foreground-1);
|
background-color: var(--foreground-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
height: 3rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-date {
|
||||||
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
.settings-page {
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
.setting {
|
||||||
|
background-color: var(--background-2);
|
||||||
|
border: var(--border-size) solid var(--foreground-2);
|
||||||
|
margin-top: 1rem;
|
||||||
|
max-width: 70rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
h2,
|
||||||
|
select {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type='checkbox'] {
|
||||||
|
height: 1.75rem;
|
||||||
|
width: 1.75rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,13 @@
|
||||||
@use 'common';
|
@use 'common';
|
||||||
|
|
||||||
// Theme definitions
|
// Theme definitions
|
||||||
|
@use 'themes/default';
|
||||||
|
@use 'themes/dracula';
|
||||||
|
@use 'themes/gruvbox';
|
||||||
|
@use 'themes/high-contrast';
|
||||||
@use 'themes/love';
|
@use 'themes/love';
|
||||||
|
@use 'themes/monokai';
|
||||||
|
@use 'themes/solarized';
|
||||||
|
|
||||||
// Component styling
|
// Component styling
|
||||||
@use 'components/search-bar';
|
@use 'components/search-bar';
|
||||||
|
@ -12,3 +18,4 @@
|
||||||
@use 'pages/home';
|
@use 'pages/home';
|
||||||
@use 'pages/not-found';
|
@use 'pages/not-found';
|
||||||
@use 'pages/release';
|
@use 'pages/release';
|
||||||
|
@use 'pages/settings';
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
body {
|
||||||
|
--border-radius: 8px;
|
||||||
|
--border-size: 2px;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
.dracula {
|
||||||
|
--foreground-1: #f8f8f2;
|
||||||
|
--foreground-2: #f8f8f2;
|
||||||
|
--background-1: #282a36;
|
||||||
|
--background-2: #44475a;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
.gruvbox-dark {
|
||||||
|
--foreground-1: #fbf1c7;
|
||||||
|
--foreground-2: #ebdbb2;
|
||||||
|
--background-1: #282828;
|
||||||
|
--background-2: #3c3836;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gruvbox-light {
|
||||||
|
--foreground-1: #282828;
|
||||||
|
--foreground-2: #3c3836;
|
||||||
|
--background-1: #fbf1c7;
|
||||||
|
--background-2: #ebdbb2;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
.high-contrast-black {
|
||||||
|
--foreground-1: #fff;
|
||||||
|
--foreground-2: #ddd;
|
||||||
|
--background-1: #000;
|
||||||
|
--background-2: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.high-contrast-white {
|
||||||
|
--foreground-1: #000;
|
||||||
|
--foreground-2: #222;
|
||||||
|
--background-1: #fff;
|
||||||
|
--background-2: #ddd;
|
||||||
|
}
|
|
@ -1,9 +1,15 @@
|
||||||
body,
|
|
||||||
.love-dark {
|
.love-dark {
|
||||||
--border-radius: 8px;
|
--border-radius: 8px;
|
||||||
|
--border-size: 2px;
|
||||||
--foreground-1: #f2efff;
|
--foreground-1: #f2efff;
|
||||||
--foreground-2: #e6deff;
|
--foreground-2: #e6deff;
|
||||||
--background-1: #1f1731;
|
--background-1: #1f1731;
|
||||||
--background-2: #2a2041;
|
--background-2: #2a2041;
|
||||||
--accent-1: #d2b83a;
|
}
|
||||||
|
|
||||||
|
.love-light {
|
||||||
|
--foreground-1: #1f1731;
|
||||||
|
--foreground-2: #2a2041;
|
||||||
|
--background-1: #f2efff;
|
||||||
|
--background-2: #e6deff;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
@use 'sass:color';
|
||||||
|
|
||||||
|
.monokai {
|
||||||
|
--foreground-1: #d6d6d6;
|
||||||
|
--foreground-2: #d6d6d6;
|
||||||
|
--background-1: #{color.scale(#2e2e2e, $lightness: -25%)};
|
||||||
|
--background-2: #2e2e2e;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
.solarized-dark {
|
||||||
|
--foreground-1: #839496;
|
||||||
|
--foreground-2: #93a1a1;
|
||||||
|
--background-1: #002b36;
|
||||||
|
--background-2: #073642;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solarized-light {
|
||||||
|
--foreground-1: #657b83;
|
||||||
|
--foreground-2: #586e75;
|
||||||
|
--background-1: #fdf6e3;
|
||||||
|
--background-2: #eee8d5;
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import {Component, html} from 'htm/preact';
|
import {Component, html} from 'htm/preact';
|
||||||
|
|
||||||
import debounce from '../utilities/debounce.js';
|
import debounce from '../utilities/debounce.js';
|
||||||
import searchReleases, {SearchResult} from '../utilities/search.js';
|
import searchReleases, {
|
||||||
|
searchLimit,
|
||||||
|
SearchResult,
|
||||||
|
} from '../utilities/search.js';
|
||||||
|
|
||||||
type Props = Record<string, unknown>;
|
type Props = Record<string, unknown>;
|
||||||
|
|
||||||
|
@ -24,6 +26,23 @@ export default class SearchBar extends Component<Props, State> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
searchMore = async (): Promise<void> => {
|
||||||
|
if (this.state.searchValue.length === 0) {
|
||||||
|
this.setState({searchResults: [], searchState: 'waiting'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
searchResults: [
|
||||||
|
...this.state.searchResults,
|
||||||
|
...(await searchReleases(
|
||||||
|
this.state.searchValue,
|
||||||
|
this.state.searchResults.length,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
searchQuery = async (): Promise<void> => {
|
searchQuery = async (): Promise<void> => {
|
||||||
if (this.state.searchValue.length === 0) {
|
if (this.state.searchValue.length === 0) {
|
||||||
this.setState({searchResults: [], searchState: 'waiting'});
|
this.setState({searchResults: [], searchState: 'waiting'});
|
||||||
|
@ -54,16 +73,14 @@ export default class SearchBar extends Component<Props, State> {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
results.push(
|
results.push(html`
|
||||||
html`
|
<li>
|
||||||
<li>
|
<a class="search-result" href="/release/${result.id}">
|
||||||
<a class="search-result" href="/release/${result.id}">
|
<span class="display">${result.artist} - ${result.title}</span>
|
||||||
<span class="display">${result.artist} - ${result.title}</span>
|
${disambiguation}
|
||||||
${disambiguation}
|
</a>
|
||||||
</a>
|
</li>
|
||||||
</li>
|
`);
|
||||||
`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = this.state.searchState === 'searching';
|
const isLoading = this.state.searchState === 'searching';
|
||||||
|
@ -79,6 +96,17 @@ export default class SearchBar extends Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resultAmount = this.state.searchResults.length;
|
||||||
|
if (resultAmount > 0 && resultAmount % searchLimit === 0) {
|
||||||
|
results.push(html`
|
||||||
|
<li class="search-state">
|
||||||
|
<button class="load-more" onClick=${this.searchMore}>
|
||||||
|
Load more…
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -1,16 +1,32 @@
|
||||||
import {Component, html} from 'htm/preact';
|
import {Component, html} from 'htm/preact';
|
||||||
|
|
||||||
import ExternalAnchor from './external-anchor.js';
|
import ExternalAnchor from './external-anchor.js';
|
||||||
|
|
||||||
export default class SharedFooter extends Component {
|
type Props = {
|
||||||
|
page: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class SharedFooter extends Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const githubUrl = 'https://github.com/Bauke/href-plus';
|
const {page} = this.props;
|
||||||
|
|
||||||
|
const homeLink =
|
||||||
|
page === 'home' || page === 'not-found'
|
||||||
|
? undefined
|
||||||
|
: html`<a href="/">Home</a>${' '}`;
|
||||||
|
|
||||||
|
const settingsLink =
|
||||||
|
page === 'settings'
|
||||||
|
? undefined
|
||||||
|
: html`<a href="/settings">Settings</a>${' '}`;
|
||||||
|
|
||||||
|
const giteaUrl = 'https://git.bauke.xyz/Bauke/href-plus';
|
||||||
const versionText = `v${hrefPlusVersion}/${hrefPlusCommitHash}`;
|
const versionText = `v${hrefPlusVersion}/${hrefPlusCommitHash}`;
|
||||||
const versionUrl = `${githubUrl}/tree/${hrefPlusCommitHash}`;
|
const versionUrl = `${giteaUrl}/src/commit/${hrefPlusCommitHash}`;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<footer class="shared-footer">
|
<footer class="shared-footer">
|
||||||
<${ExternalAnchor} text="GitHub" url=${githubUrl} />
|
${homeLink}${settingsLink}
|
||||||
|
<${ExternalAnchor} text="Gitea" url=${giteaUrl} />
|
||||||
${' '}
|
${' '}
|
||||||
<${ExternalAnchor} text=${versionText} url=${versionUrl} />
|
<${ExternalAnchor} text=${versionText} url=${versionUrl} />
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {Component, html} from 'htm/preact';
|
import {Component, html} from 'htm/preact';
|
||||||
|
|
||||||
import ExternalAnchor from '../components/external-anchor.js';
|
import ExternalAnchor from '../components/external-anchor.js';
|
||||||
import SearchBar from '../components/search-bar.js';
|
import SearchBar from '../components/search-bar.js';
|
||||||
import SharedFooter from '../components/shared-footer.js';
|
import SharedFooter from '../components/shared-footer.js';
|
||||||
|
@ -59,7 +58,7 @@ export default class HomePage extends Component {
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<${SharedFooter} />
|
<${SharedFooter} page="home" />
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {Component, html} from 'htm/preact';
|
import {Component, html} from 'htm/preact';
|
||||||
|
|
||||||
import SharedFooter from '../components/shared-footer.js';
|
import SharedFooter from '../components/shared-footer.js';
|
||||||
|
|
||||||
export default class NotFoundPage extends Component {
|
export default class NotFoundPage extends Component {
|
||||||
|
@ -13,7 +12,7 @@ export default class NotFoundPage extends Component {
|
||||||
<a href="/">← Home</a>
|
<a href="/">← Home</a>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<${SharedFooter} />
|
<${SharedFooter} page="not-found" />
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import {Component, html} from 'htm/preact';
|
import {Component, html} from 'htm/preact';
|
||||||
|
|
||||||
import ExternalAnchor from '../components/external-anchor.js';
|
import ExternalAnchor from '../components/external-anchor.js';
|
||||||
import Release from '../utilities/release.js';
|
import Release from '../utilities/release.js';
|
||||||
|
|
||||||
|
@ -66,39 +65,50 @@ export default class ReleasePage extends Component<Props, State> {
|
||||||
if (loading === 'finished' && release !== undefined) {
|
if (loading === 'finished' && release !== undefined) {
|
||||||
document.title = release.display();
|
document.title = release.display();
|
||||||
|
|
||||||
|
const date =
|
||||||
|
release.date === undefined
|
||||||
|
? undefined
|
||||||
|
: html`<span class="release-date">${release.date}</span>`;
|
||||||
|
|
||||||
const image =
|
const image =
|
||||||
release.image === undefined
|
release.image === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: html`<img class="cover-art" src="${release.image}" />`;
|
: html`<img class="cover-art" src="${release.image}" />`;
|
||||||
|
|
||||||
const urls = release.links.map(
|
const urls = release.links.map((link) => {
|
||||||
(link) =>
|
let linkImage;
|
||||||
html`
|
if (link.icon !== undefined) {
|
||||||
<li class="release-link">
|
linkImage = html`<img src="/icons/${link.icon}" />`;
|
||||||
<${ExternalAnchor} url="${link.original}" text="${link.text}" />
|
}
|
||||||
</li>
|
|
||||||
`,
|
return html`
|
||||||
);
|
<li class="release-link">
|
||||||
|
<a href="${link.original}" rel="noopener noreferrer">
|
||||||
|
${linkImage} ${link.text}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
const releaseUrl = `https://musicbrainz.org/release/${mbid}`;
|
const releaseUrl = `https://musicbrainz.org/release/${mbid}`;
|
||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
const editUrl = `${releaseUrl}/edit`;
|
const editUrl = `${releaseUrl}/edit`;
|
||||||
urls.push(
|
urls.push(html`
|
||||||
html`
|
<li class="no-links">
|
||||||
<li class="no-links">
|
<p>
|
||||||
<p>
|
There are no links for this release yet, consider${' '}
|
||||||
There are no links for this release yet, consider${' '}
|
<${ExternalAnchor} url="${editUrl}" text="adding some" />?
|
||||||
<${ExternalAnchor} url="${editUrl}" text="adding some" />?
|
</p>
|
||||||
</p>
|
</li>
|
||||||
</li>
|
`);
|
||||||
`,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
urls.push(
|
urls.push(
|
||||||
html`<li class="divider"></li>`,
|
html`<li class="divider"></li>`,
|
||||||
html`
|
html`
|
||||||
<li class="release-link">
|
<li class="release-link">
|
||||||
<${ExternalAnchor} url="${releaseUrl}" text="MusicBrainz" />
|
<a href="${releaseUrl}" rel="noopener noreferrer">
|
||||||
|
<img src="/icons/musicbrainz.png" /> MusicBrainz
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
@ -109,6 +119,7 @@ export default class ReleasePage extends Component<Props, State> {
|
||||||
<header class="release-header">
|
<header class="release-header">
|
||||||
${image}
|
${image}
|
||||||
<h1>${release.artist}<br />${release.title}</h1>
|
<h1>${release.artist}<br />${release.title}</h1>
|
||||||
|
${date}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="release-main">
|
<main class="release-main">
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
import {Component, html} from 'htm/preact';
|
||||||
|
import ExternalAnchor from '../components/external-anchor.js';
|
||||||
|
import SharedFooter from '../components/shared-footer.js';
|
||||||
|
import {isDebugEnabled} from '../utilities/debug.js';
|
||||||
|
import {
|
||||||
|
defaultTheme,
|
||||||
|
getThemeByCssClass,
|
||||||
|
setTheme,
|
||||||
|
themes,
|
||||||
|
} from '../utilities/themes.js';
|
||||||
|
|
||||||
|
type Props = Record<string, unknown>;
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
debugChecked: boolean;
|
||||||
|
selectedTheme: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class SettingsPage extends Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
debugChecked: isDebugEnabled(),
|
||||||
|
selectedTheme:
|
||||||
|
window.localStorage.getItem('theme') ?? defaultTheme.cssClass,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onDebugChange = (event: Event) => {
|
||||||
|
const checked = (event.target as HTMLInputElement).checked;
|
||||||
|
window.localStorage.setItem('debug', checked.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
onThemeChange = (event: Event) => {
|
||||||
|
const theme = getThemeByCssClass((event.target as HTMLSelectElement).value);
|
||||||
|
setTheme(theme);
|
||||||
|
this.setState({selectedTheme: theme.cssClass});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
document.title = 'Settings';
|
||||||
|
|
||||||
|
const {selectedTheme} = this.state;
|
||||||
|
const themeOptions = themes.map(
|
||||||
|
(theme) => html`<option value=${theme.cssClass}>${theme.name}</option>`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="settings-page">
|
||||||
|
<header>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="setting">
|
||||||
|
<h2>Theme</h2>
|
||||||
|
|
||||||
|
<select value=${selectedTheme} onChange=${this.onThemeChange}>
|
||||||
|
${themeOptions}
|
||||||
|
</select>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="setting">
|
||||||
|
<h2>Debug</h2>
|
||||||
|
|
||||||
|
<input
|
||||||
|
checked=${this.state.debugChecked}
|
||||||
|
id="debug-checkbox"
|
||||||
|
name="debug-checkbox"
|
||||||
|
onChange=${this.onDebugChange}
|
||||||
|
type="checkbox"
|
||||||
|
/>
|
||||||
|
<label for="debug-checkbox">
|
||||||
|
Log debug information to the console.
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<${SharedFooter} page="settings" />
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,20 +2,29 @@
|
||||||
// import 'preact/debug';
|
// import 'preact/debug';
|
||||||
|
|
||||||
import '@fontsource/inter/latin.css';
|
import '@fontsource/inter/latin.css';
|
||||||
|
|
||||||
import {html, render} from 'htm/preact';
|
import {html, render} from 'htm/preact';
|
||||||
import {Router} from 'preact-router';
|
import {Router} from 'preact-router';
|
||||||
|
|
||||||
import HomePage from './pages/home.js';
|
import HomePage from './pages/home.js';
|
||||||
import NotFoundPage from './pages/not-found.js';
|
import NotFoundPage from './pages/not-found.js';
|
||||||
import ReleasePage from './pages/release.js';
|
import ReleasePage from './pages/release.js';
|
||||||
|
import SettingsPage from './pages/settings.js';
|
||||||
|
import {getThemeByCssClass, themeContext} from './utilities/themes.js';
|
||||||
|
|
||||||
|
const activeTheme = getThemeByCssClass(
|
||||||
|
window.localStorage.getItem('theme') ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
document.body.classList.value = activeTheme.cssClass;
|
||||||
|
|
||||||
render(
|
render(
|
||||||
html`
|
html`
|
||||||
<${Router}>
|
<${themeContext.Provider} value=${activeTheme}>
|
||||||
<${HomePage} path="/" />
|
<${Router}>
|
||||||
<${ReleasePage} path="/release/:mbid" />
|
<${HomePage} path="/" />
|
||||||
<${NotFoundPage} default />
|
<${SettingsPage} path="/settings" />
|
||||||
|
<${ReleasePage} path="/release/:mbid" />
|
||||||
|
<${NotFoundPage} default />
|
||||||
|
<//>
|
||||||
<//>
|
<//>
|
||||||
`,
|
`,
|
||||||
document.body,
|
document.body,
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function isDebugEnabled(): boolean {
|
||||||
|
return window.localStorage.getItem('debug') === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function debug(...args: any[]): void {
|
||||||
|
if (!isDebugEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const argument of args) {
|
||||||
|
console.debug(argument);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,49 +1,101 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"regex": "amazon\\.com$",
|
"icon": "7digital.png",
|
||||||
|
"regex": "7digital\\.com$",
|
||||||
|
"text": "7digital"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "amazon.png",
|
||||||
|
"regex": "amazon\\.(co\\.jp|co\\.uk|com|de|fr|nl)$",
|
||||||
"text": "Amazon"
|
"text": "Amazon"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "apple-music.png",
|
||||||
"regex": "(itunes|music)\\.apple\\.com$",
|
"regex": "(itunes|music)\\.apple\\.com$",
|
||||||
"text": "Apple Music"
|
"text": "Apple Music"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "bandcamp.png",
|
||||||
"regex": "bandcamp\\.com$",
|
"regex": "bandcamp\\.com$",
|
||||||
"text": "Bandcamp"
|
"text": "Bandcamp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "beatport.png",
|
||||||
"regex": "beatport\\.com$",
|
"regex": "beatport\\.com$",
|
||||||
"text": "Beatport"
|
"text": "Beatport"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "deezer.png",
|
||||||
"regex": "deezer\\.com$",
|
"regex": "deezer\\.com$",
|
||||||
"text": "Deezer"
|
"text": "Deezer"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "discogs.png",
|
||||||
"regex": "discogs\\.com$",
|
"regex": "discogs\\.com$",
|
||||||
"text": "Discogs"
|
"text": "Discogs"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "juno-download.png",
|
||||||
|
"regex": "junodownload\\.com$",
|
||||||
|
"text": "Juno Download"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "liquicity.png",
|
||||||
|
"regex": "store\\.liquicity\\.com$",
|
||||||
|
"text": "Liquicity Store"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "monstercat-music.jpg",
|
||||||
|
"regex": "music\\.monstercat\\.com$",
|
||||||
|
"text": "Monstercat Music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "mora.png",
|
||||||
|
"regex": "mora\\.jp$",
|
||||||
|
"text": "Mora"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "napster.png",
|
||||||
|
"regex": "napster\\.com$",
|
||||||
|
"text": "Napster"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "ototoy.png",
|
||||||
|
"regex": "ototoy\\.jp$",
|
||||||
|
"text": "Ototoy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "qobuz.png",
|
||||||
"regex": "qobuz\\.com$",
|
"regex": "qobuz\\.com$",
|
||||||
"text": "Qobuz"
|
"text": "Qobuz"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "rateyourmusic.png",
|
||||||
|
"regex": "rateyourmusic\\.com$",
|
||||||
|
"text": "Rate Your Music"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon": "soundcloud.png",
|
||||||
"regex": "soundcloud\\.com$",
|
"regex": "soundcloud\\.com$",
|
||||||
"text": "SoundCloud"
|
"text": "SoundCloud"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "spotify.png",
|
||||||
"regex": "spotify\\.com$",
|
"regex": "spotify\\.com$",
|
||||||
"text": "Spotify"
|
"text": "Spotify"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "tidal.png",
|
||||||
"regex": "tidal\\.com$",
|
"regex": "tidal\\.com$",
|
||||||
"text": "Tidal"
|
"text": "Tidal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "youtube-music.png",
|
||||||
"regex": "music\\.youtube\\.com$",
|
"regex": "music\\.youtube\\.com$",
|
||||||
"text": "YouTube Music"
|
"text": "YouTube Music"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"icon": "youtube.png",
|
||||||
"__comment": "Make sure we don't match the YouTube Music sub-domain",
|
"__comment": "Make sure we don't match the YouTube Music sub-domain",
|
||||||
"regex": "^(www\\.)?(youtu\\.be|youtube\\.com)$",
|
"regex": "^(www\\.)?(youtu\\.be|youtube\\.com)$",
|
||||||
"text": "YouTube"
|
"text": "YouTube"
|
||||||
|
|
|
@ -8,16 +8,20 @@
|
||||||
import knownLinks from './known-links.json';
|
import knownLinks from './known-links.json';
|
||||||
|
|
||||||
type KnownLink = {
|
type KnownLink = {
|
||||||
|
icon: string | undefined;
|
||||||
regex: RegExp;
|
regex: RegExp;
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const known: KnownLink[] = knownLinks.map((data: Record<string, unknown>) => ({
|
const known: KnownLink[] = knownLinks.map((data: Record<string, unknown>) => ({
|
||||||
|
icon: data.icon as string | undefined,
|
||||||
regex: new RegExp(data.regex as string),
|
regex: new RegExp(data.regex as string),
|
||||||
text: data.text as string,
|
text: data.text as string,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default class RelationLink {
|
export default class RelationLink {
|
||||||
|
public readonly icon: string | undefined;
|
||||||
|
public readonly isKnown: boolean;
|
||||||
public readonly link: URL;
|
public readonly link: URL;
|
||||||
public readonly original: string;
|
public readonly original: string;
|
||||||
public readonly text: string;
|
public readonly text: string;
|
||||||
|
@ -27,6 +31,8 @@ export default class RelationLink {
|
||||||
this.link = new URL(relationUrl);
|
this.link = new URL(relationUrl);
|
||||||
|
|
||||||
const knownLink = known.find(({regex}) => regex.test(this.link.host));
|
const knownLink = known.find(({regex}) => regex.test(this.link.host));
|
||||||
|
this.icon = knownLink?.icon;
|
||||||
|
this.isKnown = knownLink !== undefined;
|
||||||
this.text = knownLink?.text ?? this.link.host;
|
this.text = knownLink?.text ?? this.link.host;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import {debug} from './debug.js';
|
||||||
import RelationLink from './relation-link.js';
|
import RelationLink from './relation-link.js';
|
||||||
|
|
||||||
type ApiReleaseData = {
|
type ApiReleaseData = {
|
||||||
|
@ -8,8 +9,11 @@ type ApiReleaseData = {
|
||||||
'cover-art-archive': {
|
'cover-art-archive': {
|
||||||
front: boolean;
|
front: boolean;
|
||||||
};
|
};
|
||||||
|
date: string | undefined;
|
||||||
id: string;
|
id: string;
|
||||||
relations: Array<{
|
relations: Array<{
|
||||||
|
ended: boolean;
|
||||||
|
type: string;
|
||||||
url: {
|
url: {
|
||||||
resource: string;
|
resource: string;
|
||||||
};
|
};
|
||||||
|
@ -20,6 +24,7 @@ type ApiReleaseData = {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
interface IRelease {
|
interface IRelease {
|
||||||
artist: string;
|
artist: string;
|
||||||
|
date: string | undefined;
|
||||||
image: string | undefined;
|
image: string | undefined;
|
||||||
links: RelationLink[];
|
links: RelationLink[];
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -41,6 +46,7 @@ export default class Release {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await apiResponse.json()) as ApiReleaseData;
|
const data = (await apiResponse.json()) as ApiReleaseData;
|
||||||
|
debug(data);
|
||||||
|
|
||||||
const artist = data['artist-credit']
|
const artist = data['artist-credit']
|
||||||
.map(({name, joinphrase}) => `${name}${joinphrase}`)
|
.map(({name, joinphrase}) => `${name}${joinphrase}`)
|
||||||
|
@ -50,12 +56,30 @@ export default class Release {
|
||||||
? `https://coverartarchive.org/release/${mbid}/front-500`
|
? `https://coverartarchive.org/release/${mbid}/front-500`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const links = data.relations
|
const relations = new Set(
|
||||||
.map(({url}) => new RelationLink(url.resource))
|
data.relations
|
||||||
.sort((a, b) => a.text.localeCompare(b.text));
|
// Remove discography entries and links that have been marked as no
|
||||||
|
// longer working.
|
||||||
|
.filter(
|
||||||
|
(relation) =>
|
||||||
|
relation.type !== 'discography entry' && !relation.ended,
|
||||||
|
)
|
||||||
|
.map((relation) => relation.url.resource),
|
||||||
|
);
|
||||||
|
const links = Array.from(relations)
|
||||||
|
.map((url) => new RelationLink(url))
|
||||||
|
.sort((a, b) =>
|
||||||
|
// Sort links that aren't known to us at the bottom.
|
||||||
|
a.isKnown === b.isKnown
|
||||||
|
? a.text.localeCompare(b.text)
|
||||||
|
: b.isKnown
|
||||||
|
? 1 // This return 1 or -1 is because .sort() expects a number.
|
||||||
|
: -1,
|
||||||
|
);
|
||||||
|
|
||||||
return new Release({
|
return new Release({
|
||||||
artist,
|
artist,
|
||||||
|
date: data.date,
|
||||||
image,
|
image,
|
||||||
links,
|
links,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import {debug} from './debug.js';
|
||||||
|
|
||||||
type ApiSearchData = {
|
type ApiSearchData = {
|
||||||
releases: Array<{
|
releases: Array<{
|
||||||
'artist-credit': Array<{
|
'artist-credit': Array<{
|
||||||
|
@ -17,11 +19,18 @@ export type SearchResult = {
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const searchLimit = 25;
|
||||||
|
|
||||||
export default async function searchReleases(
|
export default async function searchReleases(
|
||||||
query: string,
|
query: string,
|
||||||
|
offset?: number,
|
||||||
): Promise<SearchResult[]> {
|
): Promise<SearchResult[]> {
|
||||||
query = encodeURIComponent(query);
|
query = encodeURIComponent(query);
|
||||||
const url = `https://musicbrainz.org/ws/2/release?query=${query}`;
|
let url = `https://musicbrainz.org/ws/2/release?query=${query}&limit=${searchLimit}`;
|
||||||
|
if (offset !== undefined) {
|
||||||
|
url += `&offset=${offset}`;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await window.fetch(url, {
|
const response = await window.fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
accept: 'application/json',
|
accept: 'application/json',
|
||||||
|
@ -34,6 +43,8 @@ export default async function searchReleases(
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = (await response.json()) as ApiSearchData;
|
const data = (await response.json()) as ApiSearchData;
|
||||||
|
debug(data);
|
||||||
|
|
||||||
const results: SearchResult[] = [];
|
const results: SearchResult[] = [];
|
||||||
|
|
||||||
for (const release of data.releases) {
|
for (const release of data.releases) {
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import {createContext} from 'preact';
|
||||||
|
|
||||||
|
type Theme = {
|
||||||
|
cssClass: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultTheme: Theme = {
|
||||||
|
cssClass: 'love-dark',
|
||||||
|
name: 'Love Dark',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const themes: Theme[] = [
|
||||||
|
defaultTheme,
|
||||||
|
{
|
||||||
|
cssClass: 'love-light',
|
||||||
|
name: 'Love Light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cssClass: 'solarized-dark',
|
||||||
|
name: 'Solarized Dark',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cssClass: 'solarized-light',
|
||||||
|
name: 'Solarized Light',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cssClass: 'dracula',
|
||||||
|
name: 'Dracula',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cssClass: 'monokai',
|
||||||
|
name: 'Monokai',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cssClass: 'high-contrast-black',
|
||||||
|
name: 'High Contrast Black',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cssClass: 'high-contrast-white',
|
||||||
|
name: 'High Contrast White',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cssClass: 'gruvbox-dark',
|
||||||
|
name: 'Gruvbox Dark',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cssClass: 'gruvbox-light',
|
||||||
|
name: 'Gruvbox Light',
|
||||||
|
},
|
||||||
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
|
||||||
|
export const themeContext = createContext<Theme>(defaultTheme);
|
||||||
|
|
||||||
|
export function getThemeByCssClass(cssClass: string): Theme {
|
||||||
|
return themes.find((theme) => theme.cssClass === cssClass) ?? defaultTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTheme(theme: Theme): void {
|
||||||
|
document.body.classList.value = theme.cssClass;
|
||||||
|
window.localStorage.setItem('theme', theme.cssClass);
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ function gitRevParse(): string {
|
||||||
const revParse = childProcess.spawnSync(
|
const revParse = childProcess.spawnSync(
|
||||||
'git',
|
'git',
|
||||||
['rev-parse', '--short', '--verify', 'main'],
|
['rev-parse', '--short', '--verify', 'main'],
|
||||||
{encoding: 'utf-8'},
|
{encoding: 'utf8'},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (revParse.error) {
|
if (revParse.error) {
|
||||||
|
@ -33,7 +33,7 @@ export default defineConfig({
|
||||||
define: {
|
define: {
|
||||||
hrefPlusVersion: JSON.stringify(hrefPlusVersion),
|
hrefPlusVersion: JSON.stringify(hrefPlusVersion),
|
||||||
hrefPlusCommitHash: gitRevParse(),
|
hrefPlusCommitHash: gitRevParse(),
|
||||||
hrefPlusUserAgent: `"href-plus/${hrefPlusVersion} (https://github.com/Bauke/href-plus)"`,
|
hrefPlusUserAgent: `"href-plus/${hrefPlusVersion} (https://git.bauke.xyz/Bauke/href-plus)"`,
|
||||||
},
|
},
|
||||||
publicDir: path.join(sourceDir, 'assets'),
|
publicDir: path.join(sourceDir, 'assets'),
|
||||||
root: sourceDir,
|
root: sourceDir,
|
||||||
|
|