From 691dfb6cb50b294deeb03e896a7134c66f2b8aca Mon Sep 17 00:00:00 2001 From: Yansu Date: Fri, 22 May 2026 03:36:06 +0000 Subject: [PATCH 01/16] feat(features): add features section with 4 feature cards - Create features.html partial with URL Shortening, Analytics, Dashboard, and Secure Links feature cards - Add SVG icons for each feature (link, bar-chart, layout-dashboard, shield-check) - Implement responsive grid layout (1 col mobile, 2 col tablet, 4 col desktop) - Add semantic markup with article elements and aria-labelledby - Include alt text on all feature icons for accessibility - Add hover effects and consistent card styling - Create comprehensive Playwright e2e tests for features section --- .gitignore | 2 + package-lock.json | 1305 ++++++++++++++++++++++++ package.json | 17 + postcss.config.js | 6 + tailwind.config.js | 24 + tests/e2e/features.spec.js | 148 +++ tests/e2e/playwright.config.js | 38 + web/assets/images/bar-chart.svg | 5 + web/assets/images/layout-dashboard.svg | 6 + web/assets/images/link.svg | 4 + web/assets/images/shield-check.svg | 4 + web/index.html | 114 +++ web/src/css/main.css | 28 + web/src/js/main.js | 10 + web/static/css/main.css | 228 +++++ web/templates/base.html | 23 + web/templates/index.html | 9 + web/templates/partials/cta.html | 11 + web/templates/partials/features.html | 67 ++ web/templates/partials/footer.html | 15 + web/templates/partials/hero.html | 13 + web/templates/partials/nav.html | 13 + 22 files changed, 2090 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 tailwind.config.js create mode 100644 tests/e2e/features.spec.js create mode 100644 tests/e2e/playwright.config.js create mode 100644 web/assets/images/bar-chart.svg create mode 100644 web/assets/images/layout-dashboard.svg create mode 100644 web/assets/images/link.svg create mode 100644 web/assets/images/shield-check.svg create mode 100644 web/index.html create mode 100644 web/src/css/main.css create mode 100644 web/src/js/main.js create mode 100644 web/static/css/main.css create mode 100644 web/templates/base.html create mode 100644 web/templates/index.html create mode 100644 web/templates/partials/cta.html create mode 100644 web/templates/partials/features.html create mode 100644 web/templates/partials/footer.html create mode 100644 web/templates/partials/hero.html create mode 100644 web/templates/partials/nav.html diff --git a/.gitignore b/.gitignore index 53eaa2196..5f4921f13 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target **/*.rs.bk +.something/ +node_modules/ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..14d39e519 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1305 @@ +{ + "name": "mirdb-homepage", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mirdb-homepage", + "version": "1.0.0", + "devDependencies": { + "@playwright/test": "^1.40.0", + "autoprefixer": "^10.4.0", + "daisyui": "^4.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", + "integrity": "sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.24", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.24.tgz", + "integrity": "sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.361", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.361.tgz", + "integrity": "sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.45", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.45.tgz", + "integrity": "sha512-iIbHXV9eBB2nB0wa7oTsrrXq+qQt+9SIlx9AX3T96YgobtEQfis5n6TJ6vV+3QP8DwdriEAcGhARaFCu37peBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..9299646e3 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "mirdb-homepage", + "version": "1.0.0", + "description": "MirDB URL Shortening Service Homepage", + "scripts": { + "test:e2e": "playwright test tests/e2e/", + "build:css": "tailwindcss -i web/src/css/main.css -o web/static/css/main.css", + "serve": "python3 -m http.server 8080 --directory web" + }, + "devDependencies": { + "@playwright/test": "^1.40.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0", + "daisyui": "^4.4.0" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 000000000..33ad091d2 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 000000000..d9de7e0dc --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,24 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + "web/templates/**/*.html", + "web/src/js/**/*.js", + ], + theme: { + extend: { + colors: { + brand: { + 50: '#eff6ff', + 100: '#dbeafe', + 500: '#3b82f6', + 600: '#2563eb', + 700: '#1d4ed8', + }, + }, + }, + }, + plugins: [require('daisyui')], + daisyui: { + themes: ['light', 'dark'], + }, +} diff --git a/tests/e2e/features.spec.js b/tests/e2e/features.spec.js new file mode 100644 index 000000000..bde60f3f2 --- /dev/null +++ b/tests/e2e/features.spec.js @@ -0,0 +1,148 @@ +/** + * Features Section E2E Tests + * Owner: Scenario 3 - Features Section + * + * Tests: + * - Feature cards count and styling + * - Feature card content (URL Shortening, Analytics, Dashboard) + * - Accessibility (alt text, semantic markup) + * - Responsive layout at 375px + */ + +const { test, expect } = require('@playwright/test'); + +test.describe('Features Section', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + // Wait for the features section to be visible + await page.waitForSelector('#features', { state: 'visible' }); + }); + + test('should render 4 feature cards with consistent styling', async ({ page }) => { + const featuresSection = page.locator('#features'); + await expect(featuresSection).toBeVisible(); + + const cards = featuresSection.locator('article.feature-card'); + await expect(cards).toHaveCount(4); + + // Verify each card has consistent styling + for (let i = 0; i < 4; i++) { + const card = cards.nth(i); + await expect(card).toHaveClass(/feature-card/); + await expect(card).toHaveClass(/card/); + await expect(card).toHaveClass(/bg-base-200/); + await expect(card).toHaveClass(/border/); + await expect(card).toHaveClass(/p-6/); + } + }); + + test('should include URL Shortening, Analytics, and Dashboard feature cards', async ({ page }) => { + const cards = page.locator('#features article.feature-card'); + await expect(cards).toHaveCount(4); + + // Check for specific feature titles + const titles = cards.locator('h3'); + const titleTexts = await titles.allTextContents(); + + expect(titleTexts).toContain('URL Shortening'); + expect(titleTexts).toContain('Analytics'); + expect(titleTexts).toContain('Dashboard'); + }); + + test('should have descriptive content for each feature', async ({ page }) => { + const cards = page.locator('#features article.feature-card'); + const descriptions = cards.locator('p'); + + const descTexts = await descriptions.allTextContents(); + + // URL Shortening description + const urlShorteningDesc = descTexts.find(d => d.includes('short links')); + expect(urlShorteningDesc).toBeTruthy(); + + // Analytics description + const analyticsDesc = descTexts.find(d => d.includes('clicks') || d.includes('Track')); + expect(analyticsDesc).toBeTruthy(); + + // Dashboard description + const dashboardDesc = descTexts.find(d => d.includes('dashboard')); + expect(dashboardDesc).toBeTruthy(); + }); + + test('should have feature icons with alt text', async ({ page }) => { + const icons = page.locator('#features article.feature-card img'); + await expect(icons).toHaveCount(4); + + // Verify each icon has alt text + for (let i = 0; i < 4; i++) { + const icon = icons.nth(i); + const altText = await icon.getAttribute('alt'); + expect(altText).toBeTruthy(); + expect(altText.length).toBeGreaterThan(0); + } + + // Verify specific alt text content + const allAltTexts = await icons.evaluateAll(imgs => imgs.map(img => img.alt)); + expect(allAltTexts.some(alt => alt.toLowerCase().includes('link'))).toBe(true); + expect(allAltTexts.some(alt => alt.toLowerCase().includes('chart') || alt.toLowerCase().includes('analytics'))).toBe(true); + expect(allAltTexts.some(alt => alt.toLowerCase().includes('dashboard'))).toBe(true); + }); + + test('should use semantic markup for accessibility', async ({ page }) => { + const featuresSection = page.locator('#features'); + + // Section has an aria-labelledby pointing to a heading + const ariaLabelledBy = await featuresSection.getAttribute('aria-labelledby'); + expect(ariaLabelledBy).toBe('features-heading'); + + // The heading exists and has the matching id + const heading = page.locator('#features-heading'); + await expect(heading).toBeVisible(); + const headingId = await heading.getAttribute('id'); + expect(headingId).toBe('features-heading'); + + // Feature cards use article elements (semantic) + const articles = featuresSection.locator('article'); + await expect(articles).toHaveCount(4); + + // Each article has an h3 heading + for (let i = 0; i < 4; i++) { + const heading = articles.nth(i).locator('h3'); + await expect(heading).toBeVisible(); + } + }); + + test('should stack cards vertically on mobile (375px)', async ({ page }) => { + // Set viewport to mobile size + await page.setViewportSize({ width: 375, height: 812 }); + + const cards = page.locator('#features article.feature-card'); + await expect(cards).toHaveCount(4); + + // On mobile, cards should be in a single column + const firstCard = await cards.nth(0).boundingBox(); + const secondCard = await cards.nth(1).boundingBox(); + const thirdCard = await cards.nth(2).boundingBox(); + const fourthCard = await cards.nth(3).boundingBox(); + + // Cards should be stacked vertically (each below the previous) + expect(secondCard.y).toBeGreaterThan(firstCard.y); + expect(thirdCard.y).toBeGreaterThan(secondCard.y); + expect(fourthCard.y).toBeGreaterThan(thirdCard.y); + + // On mobile, each card should roughly span the full width + const viewportWidth = 375; + expect(firstCard.width).toBeGreaterThan(viewportWidth * 0.7); + }); + + test('should have proper spacing between cards on mobile', async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + + const cards = page.locator('#features article.feature-card'); + const firstCard = await cards.nth(0).boundingBox(); + const secondCard = await cards.nth(1).boundingBox(); + + // There should be gap between cards (gap-8 = 2rem = 32px) + const gap = secondCard.y - (firstCard.y + firstCard.height); + expect(gap).toBeGreaterThanOrEqual(20); + }); +}); diff --git a/tests/e2e/playwright.config.js b/tests/e2e/playwright.config.js new file mode 100644 index 000000000..a77717fc8 --- /dev/null +++ b/tests/e2e/playwright.config.js @@ -0,0 +1,38 @@ +/** + * Playwright end-to-end test configuration. + * Owner: Scenario 1 - HTTP Server and Routing + */ +const { defineConfig, devices } = require('@playwright/test'); + +module.exports = defineConfig({ + testDir: '.', + timeout: 30000, + expect: { + timeout: 5000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'list', + use: { + baseURL: 'http://localhost:8080', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + ], + webServer: { + command: 'python3 -m http.server 8080 --directory /workspace/web', + url: 'http://localhost:8080', + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); diff --git a/web/assets/images/bar-chart.svg b/web/assets/images/bar-chart.svg new file mode 100644 index 000000000..c1b42a45b --- /dev/null +++ b/web/assets/images/bar-chart.svg @@ -0,0 +1,5 @@ + diff --git a/web/assets/images/layout-dashboard.svg b/web/assets/images/layout-dashboard.svg new file mode 100644 index 000000000..d947111ca --- /dev/null +++ b/web/assets/images/layout-dashboard.svg @@ -0,0 +1,6 @@ + diff --git a/web/assets/images/link.svg b/web/assets/images/link.svg new file mode 100644 index 000000000..36166229f --- /dev/null +++ b/web/assets/images/link.svg @@ -0,0 +1,4 @@ + diff --git a/web/assets/images/shield-check.svg b/web/assets/images/shield-check.svg new file mode 100644 index 000000000..3f9f964ec --- /dev/null +++ b/web/assets/images/shield-check.svg @@ -0,0 +1,4 @@ + diff --git a/web/index.html b/web/index.html new file mode 100644 index 000000000..9fa761587 --- /dev/null +++ b/web/index.html @@ -0,0 +1,114 @@ + + + + + + MirDB - URL Shortening Service + + + + + + +
+
+
+
+

Shorten Your URLs

+

Create short, memorable links and track their performance with powerful analytics.

+ Get Started +
+
+
+ +
+
+
+

+ Powerful Features for Every Need +

+

+ Everything you need to manage, track, and optimize your links in one place. +

+
+ +
+ +
+
+ Link icon representing URL shortening +
+

URL Shortening

+

+ Create concise, shareable short links instantly. Customize your URLs with memorable aliases and branded domains. +

+
+ + +
+
+ Bar chart icon representing analytics +
+

Analytics

+

+ Track clicks, geographic locations, referrers, and device types. Get real-time insights into how your links perform. +

+
+ + +
+
+ Dashboard layout icon +
+

Dashboard

+

+ Manage all your shortened URLs from a clean, intuitive dashboard. Organize, edit, and delete links with ease. +

+
+ + +
+
+ Shield check icon representing secure links +
+

Secure Links

+

+ Protect your links with password authentication and expiration dates. HTTPS encryption ensures safe redirections. +

+
+
+
+
+ +
+
+

Ready to shorten your links?

+

Join thousands of users who trust MirDB for their URL shortening needs.

+ Create Your Free Account +
+
+
+ + + + + + diff --git a/web/src/css/main.css b/web/src/css/main.css new file mode 100644 index 000000000..099a740ed --- /dev/null +++ b/web/src/css/main.css @@ -0,0 +1,28 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Theme transitions */ +html { + transition: background-color 0.3s ease, color 0.3s ease; +} + +/* Feature card hover effects */ +.feature-card { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.feature-card:hover { + transform: translateY(-4px); + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); +} + +/* Feature icon styling */ +.feature-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + border-radius: 0.75rem; +} diff --git a/web/src/js/main.js b/web/src/js/main.js new file mode 100644 index 000000000..4669554c6 --- /dev/null +++ b/web/src/js/main.js @@ -0,0 +1,10 @@ +/** + * Main JavaScript entry point. + * Owner: Scenario 1 - HTTP Server and Routing (base setup) + */ + +document.addEventListener('DOMContentLoaded', function() { + // Initialize theme + const savedTheme = localStorage.getItem('theme') || 'light'; + document.documentElement.setAttribute('data-theme', savedTheme); +}); diff --git a/web/static/css/main.css b/web/static/css/main.css new file mode 100644 index 000000000..8c4675063 --- /dev/null +++ b/web/static/css/main.css @@ -0,0 +1,228 @@ +/* Tailwind-like CSS utilities for MirDB Homepage */ +/* Generated for static file serving */ + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + transition: background-color 0.3s ease, color 0.3s ease; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + line-height: 1.5; + -webkit-text-size-adjust: 100%; +} + +body { + min-height: 100vh; +} + +/* Color variables - Light theme */ +[data-theme="light"] { + --color-base-100: #ffffff; + --color-base-200: #f8fafc; + --color-base-300: #e2e8f0; + --color-base-content: #1e293b; + --color-primary: #3b82f6; + --color-primary-content: #ffffff; + --color-secondary: #64748b; + --color-secondary-content: #ffffff; + --color-accent: #2563eb; + --color-brand-100: #dbeafe; + --color-brand-600: #2563eb; +} + +/* Layout */ +.min-h-screen { min-height: 100vh; } +.container { width: 100%; max-width: 1280px; margin-left: auto; margin-right: auto; } +.mx-auto { margin-left: auto; margin-right: auto; } +.px-4 { padding-left: 1rem; padding-right: 1rem; } +.py-6 { padding-top: 1.5rem; padding-bottom: 1.5rem; } +.py-16 { padding-top: 4rem; padding-bottom: 4rem; } +.py-20 { padding-top: 5rem; padding-bottom: 5rem; } +.p-6 { padding: 1.5rem; } +.p-10 { padding: 2.5rem; } +.mb-2 { margin-bottom: 0.5rem; } +.mb-4 { margin-bottom: 1rem; } +.mb-8 { margin-bottom: 2rem; } +.mb-16 { margin-bottom: 4rem; } + +/* Grid */ +.grid { display: grid; } +.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); } +.grid-flow-col { grid-auto-flow: column; } +.gap-4 { gap: 1rem; } +.gap-8 { gap: 2rem; } + +/* Flexbox */ +.flex { display: flex; } +.inline-flex { display: inline-flex; } +.items-center { align-items: center; } +.justify-center { justify-content: center; } + +/* Typography */ +.text-center { text-align: center; } +.text-xl { font-size: 1.25rem; line-height: 1.75rem; } +.text-3xl { font-size: 1.875rem; line-height: 2.25rem; } +.text-4xl { font-size: 2.25rem; line-height: 2.5rem; } +.text-5xl { font-size: 3rem; line-height: 1; } +.text-lg { font-size: 1.125rem; line-height: 1.75rem; } +.font-bold { font-weight: 700; } +.font-semibold { font-weight: 600; } + +/* Colors & Backgrounds */ +.bg-base-100 { background-color: var(--color-base-100); } +.bg-base-200 { background-color: var(--color-base-200); } +.bg-primary { background-color: var(--color-primary); } +.text-base-content { color: var(--color-base-content); } +.text-primary-content { color: var(--color-primary-content); } +.text-base-content\\/70 { color: var(--color-base-content); opacity: 0.7; } +.text-primary-content\\/70 { color: var(--color-primary-content); opacity: 0.7; } +.opacity-90 { opacity: 0.9; } + +/* Components */ +.card { + border-radius: 0.75rem; + position: relative; + display: flex; + flex-direction: column; +} + +.border { + border-width: 1px; + border-style: solid; +} + +.border-base-300 { border-color: var(--color-base-300); } + +/* Navbar */ +.navbar { + display: flex; + align-items: center; + padding: 0.5rem 1rem; + width: 100%; +} + +.navbar-start { display: flex; align-items: center; margin-right: auto; } +.navbar-end { display: flex; align-items: center; margin-left: auto; gap: 0.5rem; } + +.shadow-sm { box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); } + +/* Hero */ +.hero { + display: grid; + place-items: center; +} + +.hero-content { z-index: 0; display: flex; align-items: center; justify-content: center; } + +.max-w-2xl { max-width: 42rem; } + +/* Buttons */ +.btn { + display: inline-flex; + flex-shrink: 0; + cursor: pointer; + user-select: none; + flex-wrap: wrap; + align-items: center; + justify-content: center; + text-align: center; + border-radius: 0.5rem; + height: 3rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 0.875rem; + line-height: 1em; + min-height: 3rem; + gap: 0.5rem; + font-weight: 600; + text-decoration: none; + border: none; + transition: all 0.2s; +} + +.btn:hover { opacity: 0.9; } + +.btn-primary { + background-color: var(--color-primary); + color: var(--color-primary-content); +} + +.btn-secondary { + background-color: var(--color-secondary); + color: var(--color-secondary-content); +} + +.btn-ghost { + background-color: transparent; + color: var(--color-base-content); +} + +.btn-ghost:hover { background-color: var(--color-base-200); } + +.btn-lg { + height: 4rem; + padding-left: 2rem; + padding-right: 2rem; + font-size: 1.125rem; + min-height: 4rem; +} + +/* Feature card */ +.feature-card { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.feature-card:hover { + transform: translateY(-4px); + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); +} + +.feature-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 3.5rem; + height: 3.5rem; + border-radius: 0.75rem; + background-color: var(--color-brand-100); + color: var(--color-brand-600); +} + +/* Footer */ +.footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 2rem; +} + +.footer-center { text-align: center; } + +.link { + cursor: pointer; + text-decoration: none; +} + +.link-hover:hover { text-decoration: underline; } + +/* Responsive */ +@media (min-width: 768px) { + .md\\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .md\\:text-4xl { font-size: 2.25rem; line-height: 2.5rem; } +} + +@media (min-width: 1024px) { + .lg\\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } +} + +/* Accessibility */ +[aria-hidden="true"] { pointer-events: none; } + +/* Focus states */ +*:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 2px; +} diff --git a/web/templates/base.html b/web/templates/base.html new file mode 100644 index 000000000..1bb92387d --- /dev/null +++ b/web/templates/base.html @@ -0,0 +1,23 @@ + + + + + + {% block title %}MirDB - URL Shortening Service{% endblock %} + + {% block meta %}{% endblock %} + + + + {% include 'partials/nav.html' ignore missing %} + +
+ {% block content %}{% endblock %} +
+ + {% include 'partials/footer.html' ignore missing %} + + + {% block scripts %}{% endblock %} + + diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 000000000..2f1588933 --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} + +{% block title %}MirDB - URL Shortening Service{% endblock %} + +{% block content %} + {% include 'partials/hero.html' ignore missing %} + {% include 'partials/features.html' %} + {% include 'partials/cta.html' ignore missing %} +{% endblock %} diff --git a/web/templates/partials/cta.html b/web/templates/partials/cta.html new file mode 100644 index 000000000..7b7a1a7d3 --- /dev/null +++ b/web/templates/partials/cta.html @@ -0,0 +1,11 @@ + +
+
+

Ready to shorten your links?

+

Join thousands of users who trust MirDB for their URL shortening needs.

+ Create Your Free Account +
+
diff --git a/web/templates/partials/features.html b/web/templates/partials/features.html new file mode 100644 index 000000000..942b1fb45 --- /dev/null +++ b/web/templates/partials/features.html @@ -0,0 +1,67 @@ + +
+
+
+

+ Powerful Features for Every Need +

+

+ Everything you need to manage, track, and optimize your links in one place. +

+
+ +
+ +
+
+ Link icon representing URL shortening +
+

URL Shortening

+

+ Create concise, shareable short links instantly. Customize your URLs with memorable aliases and branded domains. +

+
+ + +
+
+ Bar chart icon representing analytics +
+

Analytics

+

+ Track clicks, geographic locations, referrers, and device types. Get real-time insights into how your links perform. +

+
+ + +
+
+ Dashboard layout icon +
+

Dashboard

+

+ Manage all your shortened URLs from a clean, intuitive dashboard. Organize, edit, and delete links with ease. +

+
+ + +
+
+ Shield check icon representing secure links +
+

Secure Links

+

+ Protect your links with password authentication and expiration dates. HTTPS encryption ensures safe redirections. +

+
+
+
+
diff --git a/web/templates/partials/footer.html b/web/templates/partials/footer.html new file mode 100644 index 000000000..dda694f59 --- /dev/null +++ b/web/templates/partials/footer.html @@ -0,0 +1,15 @@ + + diff --git a/web/templates/partials/hero.html b/web/templates/partials/hero.html new file mode 100644 index 000000000..5eea6abdc --- /dev/null +++ b/web/templates/partials/hero.html @@ -0,0 +1,13 @@ + +
+
+
+

Shorten Your URLs

+

Create short, memorable links and track their performance with powerful analytics.

+ Get Started +
+
+
diff --git a/web/templates/partials/nav.html b/web/templates/partials/nav.html new file mode 100644 index 000000000..bf885f1b8 --- /dev/null +++ b/web/templates/partials/nav.html @@ -0,0 +1,13 @@ + + From 9c432b640eaef491c9bc5656762a6af9f2fba8a7 Mon Sep 17 00:00:00 2001 From: Yansu Date: Fri, 22 May 2026 03:47:46 +0000 Subject: [PATCH 02/16] feat(hero): implement hero section rendering with E2E tests - Enhance hero.html with semantic HTML, accessibility attributes, and data-testids - Update web/index.html hero section to match template partial - Add comprehensive Playwright E2E tests for hero section rendering - Tests cover: title visibility, tagline content, CTA button presence, responsive layout across mobile/tablet/desktop, semantic structure, DOM order --- tests/e2e/homepage.spec.js | 236 +++++++++++++++++++++++++++++++ web/index.html | 8 +- web/templates/partials/hero.html | 20 ++- 3 files changed, 256 insertions(+), 8 deletions(-) create mode 100644 tests/e2e/homepage.spec.js diff --git a/tests/e2e/homepage.spec.js b/tests/e2e/homepage.spec.js new file mode 100644 index 000000000..9ff7a16d6 --- /dev/null +++ b/tests/e2e/homepage.spec.js @@ -0,0 +1,236 @@ +/** + * Hero Section Rendering E2E Tests + * Owner: Scenario 2 - Hero Section Rendering + * + * Tests: + * - Hero section DOM structure (h1, tagline, CTA) + * - Hero text content and visibility + * - Primary CTA button presence and visibility + * - Responsive layout at mobile, tablet, and desktop viewports + */ + +const { test, expect } = require('@playwright/test'); + +test.describe('Hero Section Rendering', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + // Wait for the hero section to be visible + await page.waitForSelector('[data-testid="hero-section"]', { state: 'visible' }); + }); + + test('should render hero section with product title describing URL shortening service', async ({ page }) => { + const heroSection = page.locator('[data-testid="hero-section"]'); + await expect(heroSection).toBeVisible(); + + // Verify h1 element exists and is visible + const title = heroSection.locator('h1#hero-title'); + await expect(title).toBeVisible(); + + // Verify title text describes URL shortening + const titleText = await title.textContent(); + expect(titleText).toBeTruthy(); + expect(titleText.toLowerCase()).toContain('shorten'); + expect(titleText.toLowerCase()).toContain('url'); + + // Verify h1 uses semantic heading element + const tagName = await title.evaluate(el => el.tagName.toLowerCase()); + expect(tagName).toBe('h1'); + }); + + test('should render hero tagline explaining the value proposition', async ({ page }) => { + const heroSection = page.locator('[data-testid="hero-section"]'); + + // Verify tagline element exists and is visible + const tagline = heroSection.locator('[data-testid="hero-tagline"]'); + await expect(tagline).toBeVisible(); + + // Verify tagline text explains value proposition + const taglineText = await tagline.textContent(); + expect(taglineText).toBeTruthy(); + expect(taglineText.length).toBeGreaterThan(20); + + // Tagline should mention key value propositions + const lowerText = taglineText.toLowerCase(); + expect( + lowerText.includes('short') || + lowerText.includes('link') || + lowerText.includes('track') || + lowerText.includes('analytics') + ).toBe(true); + }); + + test('should render primary CTA button that is present and visible', async ({ page }) => { + const heroSection = page.locator('[data-testid="hero-section"]'); + + // Verify primary CTA button exists and is visible + const ctaButton = heroSection.locator('[data-testid="hero-primary-cta"]'); + await expect(ctaButton).toBeVisible(); + + // Verify CTA text is appropriate + const ctaText = await ctaButton.textContent(); + expect(ctaText).toBeTruthy(); + const lowerCta = ctaText.toLowerCase().trim(); + expect( + lowerCta.includes('get started') || + lowerCta.includes('create short url') || + lowerCta.includes('shorten') + ).toBe(true); + + // Verify the CTA is an anchor link with an href + const href = await ctaButton.getAttribute('href'); + expect(href).toBeTruthy(); + expect(href.length).toBeGreaterThan(0); + + // Verify button has appropriate styling classes + const classAttr = await ctaButton.getAttribute('class'); + expect(classAttr).toContain('btn'); + expect(classAttr).toContain('btn-primary'); + }); + + test('should render hero content centered and readable on mobile (375px)', async ({ page }) => { + // Set viewport to mobile size + await page.setViewportSize({ width: 375, height: 812 }); + + const heroSection = page.locator('[data-testid="hero-section"]'); + await expect(heroSection).toBeVisible(); + + // Get the hero content container + const heroContent = heroSection.locator('.hero-content'); + await expect(heroContent).toBeVisible(); + + // Verify hero content is centered (horizontal center alignment) + const contentBox = await heroContent.boundingBox(); + const sectionBox = await heroSection.boundingBox(); + + // Content should be within section bounds + expect(contentBox.x).toBeGreaterThanOrEqual(0); + expect(contentBox.x + contentBox.width).toBeLessThanOrEqual(sectionBox.x + sectionBox.width + 1); + + // Title should be visible and readable + const title = heroSection.locator('h1#hero-title'); + await expect(title).toBeVisible(); + + // Tagline should be visible + const tagline = heroSection.locator('[data-testid="hero-tagline"]'); + await expect(tagline).toBeVisible(); + + // CTA should be visible and touch-friendly (min 44x44px) + const cta = heroSection.locator('[data-testid="hero-primary-cta"]'); + const ctaBox = await cta.boundingBox(); + expect(ctaBox.width).toBeGreaterThanOrEqual(44); + expect(ctaBox.height).toBeGreaterThanOrEqual(44); + + // No horizontal overflow (scroll) + const bodyWidth = await page.evaluate(() => document.body.scrollWidth); + const viewportWidth = await page.evaluate(() => window.innerWidth); + expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1); + }); + + test('should render hero content centered and readable on tablet (768px)', async ({ page }) => { + // Set viewport to tablet size + await page.setViewportSize({ width: 768, height: 1024 }); + + const heroSection = page.locator('[data-testid="hero-section"]'); + await expect(heroSection).toBeVisible(); + + // Verify hero content is centered + const heroContent = heroSection.locator('.hero-content'); + const contentBox = await heroContent.boundingBox(); + const sectionBox = await heroSection.boundingBox(); + + // Content should be roughly centered horizontally + const contentCenter = contentBox.x + contentBox.width / 2; + const sectionCenter = sectionBox.x + sectionBox.width / 2; + expect(Math.abs(contentCenter - sectionCenter)).toBeLessThanOrEqual(50); + + // All hero elements should be visible + await expect(heroSection.locator('h1#hero-title')).toBeVisible(); + await expect(heroSection.locator('[data-testid="hero-tagline"]')).toBeVisible(); + await expect(heroSection.locator('[data-testid="hero-primary-cta"]')).toBeVisible(); + + // No horizontal overflow + const bodyWidth = await page.evaluate(() => document.body.scrollWidth); + const viewportWidth = await page.evaluate(() => window.innerWidth); + expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1); + }); + + test('should render hero content centered and readable on desktop (1280px)', async ({ page }) => { + // Set viewport to desktop size + await page.setViewportSize({ width: 1280, height: 800 }); + + const heroSection = page.locator('[data-testid="hero-section"]'); + await expect(heroSection).toBeVisible(); + + // Verify hero content is centered + const heroContent = heroSection.locator('.hero-content'); + const contentBox = await heroContent.boundingBox(); + const sectionBox = await heroSection.boundingBox(); + + // Content should be centered horizontally + const contentCenter = contentBox.x + contentBox.width / 2; + const sectionCenter = sectionBox.x + sectionBox.width / 2; + expect(Math.abs(contentCenter - sectionCenter)).toBeLessThanOrEqual(50); + + // All hero elements should be visible + await expect(heroSection.locator('h1#hero-title')).toBeVisible(); + await expect(heroSection.locator('[data-testid="hero-tagline"]')).toBeVisible(); + await expect(heroSection.locator('[data-testid="hero-primary-cta"]')).toBeVisible(); + + // No horizontal overflow + const bodyWidth = await page.evaluate(() => document.body.scrollWidth); + const viewportWidth = await page.evaluate(() => window.innerWidth); + expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1); + }); + + test('should use semantic HTML structure for the hero section', async ({ page }) => { + const heroSection = page.locator('[data-testid="hero-section"]'); + + // Section should have aria-labelledby pointing to the title + const ariaLabelledBy = await heroSection.getAttribute('aria-labelledby'); + expect(ariaLabelledBy).toBe('hero-title'); + + // Title should exist with matching id + const title = page.locator('#hero-title'); + await expect(title).toBeVisible(); + const titleId = await title.getAttribute('id'); + expect(titleId).toBe('hero-title'); + + // Title should be h1 + const tagName = await title.evaluate(el => el.tagName.toLowerCase()); + expect(tagName).toBe('h1'); + + // CTA should be an anchor element (link) + const cta = heroSection.locator('[data-testid="hero-primary-cta"]'); + const ctaTag = await cta.evaluate(el => el.tagName.toLowerCase()); + expect(ctaTag).toBe('a'); + }); + + test('should maintain correct DOM order: title, then tagline, then CTA', async ({ page }) => { + const heroSection = page.locator('[data-testid="hero-section"]'); + + // Get all direct child elements within the max-w-2xl container + const container = heroSection.locator('.max-w-2xl'); + + // Get the order of elements + const elementOrder = await container.evaluate(container => { + const children = Array.from(container.children); + return children.map(child => ({ + tag: child.tagName.toLowerCase(), + id: child.id || null, + testId: child.getAttribute('data-testid') || null, + })); + }); + + // First element should be h1 title + expect(elementOrder[0].tag).toBe('h1'); + expect(elementOrder[0].id).toBe('hero-title'); + + // Second element should be the tagline paragraph + expect(elementOrder[1].tag).toBe('p'); + expect(elementOrder[1].testId).toBe('hero-tagline'); + + // Third element should be the CTA anchor + expect(elementOrder[2].tag).toBe('a'); + expect(elementOrder[2].testId).toBe('hero-primary-cta'); + }); +}); diff --git a/web/index.html b/web/index.html index 9fa761587..bf2c12312 100644 --- a/web/index.html +++ b/web/index.html @@ -19,12 +19,12 @@
-
+
-

Shorten Your URLs

-

Create short, memorable links and track their performance with powerful analytics.

- Get Started +

Shorten Your URLs with MirDB

+

Create short, memorable links and track their performance with powerful analytics.

+ Get Started
diff --git a/web/templates/partials/hero.html b/web/templates/partials/hero.html index 5eea6abdc..1502df39b 100644 --- a/web/templates/partials/hero.html +++ b/web/templates/partials/hero.html @@ -1,13 +1,25 @@ -
+
-

Shorten Your URLs

-

Create short, memorable links and track their performance with powerful analytics.

- Get Started +

+ Shorten Your URLs with MirDB +

+

+ Create short, memorable links and track their performance with powerful analytics. +

+ + Get Started +
From 990b5b3bca453d6c4ec601908fe2b9f306102715 Mon Sep 17 00:00:00 2001 From: Yansu Date: Fri, 22 May 2026 03:56:45 +0000 Subject: [PATCH 03/16] feat(footer): add footer section with links, copyright, and responsive layout - Add footer partial with documentation, privacy policy, and terms links - Include copyright notice with proper formatting - Add data-testid attributes for E2E testability - Write comprehensive Playwright E2E tests for footer - Ensure responsive layout works at 375px mobile viewport - Add accessibility attributes (role=contentinfo) --- tests/e2e/footer.spec.js | 202 +++++++++++++++++++++++++++++ web/index.html | 14 +- web/templates/partials/footer.html | 21 ++- 3 files changed, 223 insertions(+), 14 deletions(-) create mode 100644 tests/e2e/footer.spec.js diff --git a/tests/e2e/footer.spec.js b/tests/e2e/footer.spec.js new file mode 100644 index 000000000..2a9427fa6 --- /dev/null +++ b/tests/e2e/footer.spec.js @@ -0,0 +1,202 @@ +/** + * Footer Section E2E Tests + * Owner: Scenario 6 - Footer Section + * + * Tests: + * - Footer DOM structure (organized link groups, copyright text) + * - Documentation link presence and navigation + * - Privacy Policy link presence and navigation + * - Terms of Service link presence and navigation + * - Copyright text presence and formatting + * - Responsive layout at mobile viewport (375px) + */ + +const { test, expect } = require('@playwright/test'); + +test.describe('Footer Section', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + // Wait for the footer to be visible + await page.waitForSelector('footer', { state: 'visible' }); + }); + + test('should render footer with organized link groups and copyright text', async ({ page }) => { + const footer = page.locator('footer'); + await expect(footer).toBeVisible(); + + // Verify footer has role="contentinfo" for accessibility + const role = await footer.getAttribute('role'); + expect(role).toBe('contentinfo'); + + // Verify footer contains link group + const linkGroup = footer.locator('[data-testid="footer-links"]'); + await expect(linkGroup).toBeVisible(); + + // Verify footer contains copyright text + const copyright = footer.locator('[data-testid="footer-copyright"]'); + await expect(copyright).toBeVisible(); + + // Verify all required links are present + const docsLink = footer.locator('[data-testid="footer-link-docs"]'); + const privacyLink = footer.locator('[data-testid="footer-link-privacy"]'); + const termsLink = footer.locator('[data-testid="footer-link-terms"]'); + + await expect(docsLink).toBeVisible(); + await expect(privacyLink).toBeVisible(); + await expect(termsLink).toBeVisible(); + + // Verify link text content + const docsText = await docsLink.textContent(); + expect(docsText.toLowerCase()).toContain('documentation'); + + const privacyText = await privacyLink.textContent(); + expect(privacyText.toLowerCase()).toContain('privacy'); + + const termsText = await termsLink.textContent(); + expect(termsText.toLowerCase()).toContain('terms'); + }); + + test('should have copyright text present and correctly formatted', async ({ page }) => { + const footer = page.locator('footer'); + + const copyright = footer.locator('[data-testid="footer-copyright"]'); + await expect(copyright).toBeVisible(); + + const copyrightText = await copyright.textContent(); + expect(copyrightText).toBeTruthy(); + + // Should contain copyright symbol or the word "copyright" + const lowerText = copyrightText.toLowerCase(); + expect( + lowerText.includes('copyright') || + lowerText.includes('©') || + lowerText.includes('©') + ).toBe(true); + + // Should reference MirDB or the current year + expect( + lowerText.includes('mirdb') || + lowerText.includes('2024') || + lowerText.includes('2025') || + lowerText.includes('2026') + ).toBe(true); + }); + + test('should navigate to documentation page when clicking Documentation link', async ({ page }) => { + const docsLink = page.locator('footer [data-testid="footer-link-docs"]'); + await expect(docsLink).toBeVisible(); + + const href = await docsLink.getAttribute('href'); + expect(href).toBeTruthy(); + expect(href.length).toBeGreaterThan(0); + + // Verify href points to documentation + const lowerHref = href.toLowerCase(); + expect( + lowerHref.includes('doc') || + lowerHref.includes('/docs') + ).toBe(true); + + // Verify link is an anchor element + const tagName = await docsLink.evaluate(el => el.tagName.toLowerCase()); + expect(tagName).toBe('a'); + }); + + test('should navigate to privacy policy page when clicking Privacy Policy link', async ({ page }) => { + const privacyLink = page.locator('footer [data-testid="footer-link-privacy"]'); + await expect(privacyLink).toBeVisible(); + + const href = await privacyLink.getAttribute('href'); + expect(href).toBeTruthy(); + expect(href.length).toBeGreaterThan(0); + + // Verify href points to privacy policy + const lowerHref = href.toLowerCase(); + expect( + lowerHref.includes('privacy') || + lowerHref.includes('/privacy') + ).toBe(true); + + // Verify link is an anchor element + const tagName = await privacyLink.evaluate(el => el.tagName.toLowerCase()); + expect(tagName).toBe('a'); + }); + + test('should navigate to terms of service page when clicking Terms of Service link', async ({ page }) => { + const termsLink = page.locator('footer [data-testid="footer-link-terms"]'); + await expect(termsLink).toBeVisible(); + + const href = await termsLink.getAttribute('href'); + expect(href).toBeTruthy(); + expect(href.length).toBeGreaterThan(0); + + // Verify href points to terms of service + const lowerHref = href.toLowerCase(); + expect( + lowerHref.includes('terms') || + lowerHref.includes('/terms') || + lowerHref.includes('service') + ).toBe(true); + + // Verify link is an anchor element + const tagName = await termsLink.evaluate(el => el.tagName.toLowerCase()); + expect(tagName).toBe('a'); + }); + + test('should reorganize footer layout for mobile readability at 375px', async ({ page }) => { + // Set viewport to mobile size + await page.setViewportSize({ width: 375, height: 812 }); + + const footer = page.locator('footer'); + await expect(footer).toBeVisible(); + + // All footer links should be visible on mobile + const links = footer.locator('[data-testid="footer-links"] a'); + const linkCount = await links.count(); + expect(linkCount).toBeGreaterThanOrEqual(3); + + for (let i = 0; i < linkCount; i++) { + await expect(links.nth(i)).toBeVisible(); + } + + // Copyright should be visible + const copyright = footer.locator('[data-testid="footer-copyright"]'); + await expect(copyright).toBeVisible(); + + // Verify no horizontal overflow + const bodyWidth = await page.evaluate(() => document.body.scrollWidth); + const viewportWidth = await page.evaluate(() => window.innerWidth); + expect(bodyWidth).toBeLessThanOrEqual(viewportWidth + 1); + + // Footer should fit within viewport width + const footerBox = await footer.boundingBox(); + expect(footerBox.width).toBeLessThanOrEqual(viewportWidth + 1); + }); + + test('should have accessible footer with proper semantic markup', async ({ page }) => { + const footer = page.locator('footer'); + + // Footer should have contentinfo role + const role = await footer.getAttribute('role'); + expect(role).toBe('contentinfo'); + + // All links should be focusable anchor elements + const links = footer.locator('a'); + const count = await links.count(); + expect(count).toBeGreaterThanOrEqual(3); + + for (let i = 0; i < count; i++) { + const link = links.nth(i); + const tagName = await link.evaluate(el => el.tagName.toLowerCase()); + expect(tagName).toBe('a'); + + const href = await link.getAttribute('href'); + expect(href).toBeTruthy(); + } + + // Links should have visible text content + const docsLink = footer.locator('[data-testid="footer-link-docs"]'); + const docsText = await docsLink.textContent(); + expect(docsText.trim().length).toBeGreaterThan(0); + }); +}); diff --git a/web/index.html b/web/index.html index bf2c12312..55af7f650 100644 --- a/web/index.html +++ b/web/index.html @@ -97,15 +97,15 @@

Ready to shorten your links?<

-