also looking at this
Migrate x-text/:text bindings to {{ }} interpolation (stx 0.2.72)
#120
31 files
+179
-171
bun.lockmodified+22-12
| @@ -7,9 +7,9 @@ | ||
| 7 | 7 | "dependencies": { |
| 8 | 8 | "@cwcss/crosswind": "^0.2.6", |
| 9 | 9 | "@stacksjs/bun-router": "^0.0.16", |
| 10 | "@stacksjs/components": "^0.2.67", | |
| 11 | "@stacksjs/stx": "^0.2.70", | |
| 12 | "bun-plugin-stx": "^0.2.70", | |
| 10 | "@stacksjs/components": "^0.2.72", | |
| 11 | "@stacksjs/stx": "^0.2.72", | |
| 12 | "bun-plugin-stx": "^0.2.72", | |
| 13 | 13 | "bun-query-builder": "^0.1.13", |
| 14 | 14 | "dynamodb-tooling": "^0.3.2", |
| 15 | 15 | "ts-countries": "^0.1.2", |
| @@ -41,9 +41,9 @@ | ||
| 41 | 41 | |
| 42 | 42 | "@stacksjs/clarity": ["@stacksjs/clarity@0.3.28", "", { "dependencies": { "bunfig": "^0.15.6" }, "bin": { "clarity": "./dist/bin/cli.js" } }, "sha512-SElzGeDewEbp0c4C8PTfdvi/Xtr1HGAjG6b0prHvkVxyYGM+2R0BFU0otUmy+DbR7wsfZ4rNU3YN1+JK/CzoTw=="], |
| 43 | 43 | |
| 44 | "@stacksjs/components": ["@stacksjs/components@0.2.70", "", { "dependencies": { "@cwcss/crosswind": "^0.2.0", "@stacksjs/stx": "0.2.70", "bun-plugin-stx": "0.2.70", "ts-syntax-highlighter": "^0.2.1" } }, "sha512-1yDmqRwKxNY073Rw2RiESWMHplXNP6ITrERquDjcCNCoS3vPqHsnItrJ9XEuYOnKtfAHNEhIVuGM738d1Km/PQ=="], | |
| 44 | "@stacksjs/components": ["@stacksjs/components@0.2.72", "", { "dependencies": { "@cwcss/crosswind": "^0.2.0", "@stacksjs/stx": "0.2.72", "bun-plugin-stx": "0.2.72", "ts-syntax-highlighter": "^0.2.1" } }, "sha512-g1F7UYVSD13vQHx1U+/U2bor10OqUmC/C6Yh+uwrCmh+l6Vt4T52VLWPNPETQow8zCjqX5/Xme4HZqYm3HC/xw=="], | |
| 45 | 45 | |
| 46 | "@stacksjs/desktop": ["@stacksjs/desktop@0.2.70", "", { "peerDependencies": { "craft": "*" }, "optionalPeers": ["craft"] }, "sha512-tBwE98HT+bajYzm4oY2NZtXz5+c9DjX6rUVXs0d8vr3Kv/4pof6tx+GTw64Z+lqvdB8wgCISRduDIVZiZjyDWQ=="], | |
| 46 | "@stacksjs/desktop": ["@stacksjs/desktop@0.2.72", "", { "peerDependencies": { "craft": "*" }, "optionalPeers": ["craft"] }, "sha512-R436Ag4EdDhzM96idRHF61R+FkhwbtRbalQzEV/TRSYCMwJpVwJju5gQA08u+5elEZgCGATjtUia4GBbnJKWCw=="], | |
| 47 | 47 | |
| 48 | 48 | "@stacksjs/dtsx": ["@stacksjs/dtsx@0.9.26", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0" }, "peerDependencies": { "typescript": ">=5.9.3" }, "bin": { "dtsx": "./dist/bin/cli.js" } }, "sha512-MOUxZn8BhXpFYM+vh7cnQLUOLjZ4G6q76V+4OypkRiu/Gvpek9ywBQLbmv8iWLjT6gXlFMpEdw33o3GKKLpMCw=="], |
| 49 | 49 | |
| @@ -51,9 +51,9 @@ | ||
| 51 | 51 | |
| 52 | 52 | "@stacksjs/logsmith": ["@stacksjs/logsmith@0.2.3", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.6", "pickier": "^0.1.9" }, "bin": { "@stacksjs/logsmith": "./dist/bin/cli.js", "logsmith": "./dist/bin/cli.js" } }, "sha512-Jy2AHVdEQwDtjlOVSG4xOmIMSy/D2iZcPK7KaoDOmKfZbMmfmbhveK16w1TWKCY9duhMZigdpeFx39ZzXGARVg=="], |
| 53 | 53 | |
| 54 | "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.70", "", {}, "sha512-jRuRn0VyCmlqM9H2yOuChV1dkxBozW+THBAnGcraYzPjf5CD1x/tRz9PGzS8h6PlEUSBfBi2zr+wPEwUKj1GoA=="], | |
| 54 | "@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.72", "", {}, "sha512-66a3QSfuSAgDDZJ3Z/mgqHEtkYwfag1m+9636I4X5cveTA69O92/0aEp6+f5MgGfPA+Gv4gkl3ArP8FBslR0BQ=="], | |
| 55 | 55 | |
| 56 | "@stacksjs/stx": ["@stacksjs/stx@0.2.70", "", { "dependencies": { "@cwcss/crosswind": "^0.2.0", "@stacksjs/clapp": "^0.2.0", "@stacksjs/desktop": "0.2.70", "@stacksjs/sanitizer": "0.2.70", "@stacksjs/ts-craft": "^0.0.2", "bun-plugin-stx": "0.2.70", "bunfig": "^0.15.6", "stx-router": "0.2.70", "ts-broadcasting": "^0.0.4", "ts-images": "^0.1.8", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-dPOj5qvaZLQGKd3PlZawH/qL9IiUFUaSIKVpjUFp4/fqeOpdA7QbPC5vWhyYCeVa+tHyQwetBndwwjCYAHPjOw=="], | |
| 56 | "@stacksjs/stx": ["@stacksjs/stx@0.2.72", "", { "dependencies": { "@cwcss/crosswind": "^0.2.0", "@stacksjs/clapp": "^0.2.0", "@stacksjs/desktop": "0.2.72", "@stacksjs/sanitizer": "0.2.72", "@stacksjs/ts-craft": "^0.0.2", "@stacksjs/ts-i18n": "^0.1.11", "bun-plugin-stx": "0.2.72", "bunfig": "^0.15.6", "stx-router": "0.2.72", "ts-broadcasting": "^0.0.4", "ts-images": "^0.1.8", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-aW6w0epktzFz0Jai9vCctBXEkPcri42jYNrojHyEm0oaRxyEgq9BLn6K5GUljLoJprr6wzfbDHZQp4FCopakaw=="], | |
| 57 | 57 | |
| 58 | 58 | "@stacksjs/ts-avif": ["@stacksjs/ts-avif@0.1.0", "", {}, "sha512-ytcta6ZhZeZwXJTcWlTraNLBsqL4imOfruSkJ2PwEp5RjHb4eSoqb21W4u7HLILRcILr7B1nOkIMeQct4Pozbw=="], |
| 59 | 59 | |
| @@ -67,6 +67,8 @@ | ||
| 67 | 67 | |
| 68 | 68 | "@stacksjs/ts-faker": ["@stacksjs/ts-faker@0.1.8", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0" }, "bin": { "@stacksjs/ts-faker": "dist/bin/cli.js", "faker": "dist/bin/cli.js", "fake": "dist/bin/cli.js", "mock": "dist/bin/cli.js" } }, "sha512-YEOIr9mlMeXodcl8FmX3Rq7fqZDcubi7nwdl7lCKr682GIimOQjdN8miQmVEgbBYlpwP1cOn8eO6DhVotMwWnw=="], |
| 69 | 69 | |
| 70 | "@stacksjs/ts-i18n": ["@stacksjs/ts-i18n@0.1.11", "", { "dependencies": { "@stacksjs/clapp": "^0.2.9", "bunfig": "^0.15.6" }, "bin": { "i18n": "./dist/cli.js" } }, "sha512-33FM0gKdxBy6EUsUZLGAMe0D7QUZ4f/BQXRsQIyzkZgs2+juruUTevPuJGCjWSZsgJuiaBfEJn8Oq8ctuo1OWQ=="], | |
| 71 | ||
| 70 | 72 | "@stacksjs/ts-md": ["@stacksjs/ts-md@0.1.1", "", { "bin": { "md": "./bin/cli.ts" } }, "sha512-kvsSg7S3vHq65fnHIRggTLudN4n1sjkos86xLB62V6PVRa8iosYAmFKVDTGaJk+0MdsIoRwQbJ2vLWVcNQsgGw=="], |
| 71 | 73 | |
| 72 | 74 | "@stacksjs/ts-png": ["@stacksjs/ts-png@0.1.2", "", {}, "sha512-NT9AVCAlZiOMzcBH+OlAAUAbfTzakm/6Ibp5S31JInrpTDd+Qe3arEfatPLuMasFR9tyO+NMAFsF84MT6TK1vA=="], |
| @@ -135,7 +137,7 @@ | ||
| 135 | 137 | |
| 136 | 138 | "bun-plugin-dtsx": ["bun-plugin-dtsx@0.9.26", "", { "dependencies": { "@stacksjs/dtsx": "0.9.26" } }, "sha512-dOlA+gx3mju+Be0uoZqsuQ3U+GPu08wv7QQwA1k569tw8Bv9GwVWJz0jjQw6tzJi9Rvwfl31MVXu9rHYQXSKow=="], |
| 137 | 139 | |
| 138 | "bun-plugin-stx": ["bun-plugin-stx@0.2.70", "", { "dependencies": { "@stacksjs/stx": "0.2.70", "stx-router": "0.2.70" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-7bJ6uzQLWqNM5p9Pgha9fNhVTz0gBT4LJyXEEmkMOIljevp7zKiWBa0r2F1CM0eXqSlUfqgI659mtfkhB9MiSA=="], | |
| 140 | "bun-plugin-stx": ["bun-plugin-stx@0.2.72", "", { "dependencies": { "@stacksjs/stx": "0.2.72", "stx-router": "0.2.72" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-vkvFCmAm+w1YpCCBD5EImLaGFxgJmKZWDdi4mBvO9lxMFXBZiDyGcFxPnWGTdXcw2S085+lEG32BHwCV9kiqmw=="], | |
| 139 | 141 | |
| 140 | 142 | "bun-query-builder": ["bun-query-builder@0.1.37", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "@stacksjs/ts-faker": "^0.1.8", "@stacksjs/ts-validation": "^0.5.0", "dynamodb-tooling": "^0.3.2" }, "bin": { "query-builder": "./dist/bin/cli.js", "qbx": "./dist/bin/cli.js", "qb": "./dist/bin/cli.js" } }, "sha512-IWgItHagukC4lf/11YmvccNrSEeX1wKeMiNTbUuw71c29Ru+3G2RD4kAPAVjZxkqLglrkyHRUM0iZ1c3yTdT1w=="], |
| 141 | 143 | |
| @@ -249,7 +251,7 @@ | ||
| 249 | 251 | |
| 250 | 252 | "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], |
| 251 | 253 | |
| 252 | "stx-router": ["stx-router@0.2.70", "", {}, "sha512-b0tF8pmqVsgC0I0epav9NURHJk48vVe62QfQFK0jQiVBnAYTJREgjRK9gdrCxIIX/DAmoyI39zsYoKzv/tyiAQ=="], | |
| 254 | "stx-router": ["stx-router@0.2.72", "", {}, "sha512-WUr5XaZVWp4JifqqOzU7YhmxIAiEPO8iRFO81VhKaGlF+FHrTPzhh3O0AmkAN+kmHv+IajbJ1UpMIcPTa5nFEg=="], | |
| 253 | 255 | |
| 254 | 256 | "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], |
| 255 | 257 | |
| @@ -291,9 +293,7 @@ | ||
| 291 | 293 | |
| 292 | 294 | "zip-stream": ["zip-stream@6.0.1", "", { "dependencies": { "archiver-utils": "^5.0.0", "compress-commons": "^6.0.2", "readable-stream": "^4.0.0" } }, "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA=="], |
| 293 | 295 | |
| 294 | "@stacksjs/components/@cwcss/crosswind": ["@cwcss/crosswind@0.2.4", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.0" }, "bin": { "crosswind": "./dist/cli.js" } }, "sha512-LKcLryQl7Ue4LTbS3l1pBgcr2t4wIXdBtEJB1nlW5uJmS1o+cwR1uMASb2r9b1mtmfkp4xIouSRUvRRGAGbxww=="], | |
| 295 | ||
| 296 | "@stacksjs/stx/@cwcss/crosswind": ["@cwcss/crosswind@0.2.4", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.0" }, "bin": { "crosswind": "./dist/cli.js" } }, "sha512-LKcLryQl7Ue4LTbS3l1pBgcr2t4wIXdBtEJB1nlW5uJmS1o+cwR1uMASb2r9b1mtmfkp4xIouSRUvRRGAGbxww=="], | |
| 296 | "@stacksjs/bunpress/@stacksjs/stx": ["@stacksjs/stx@0.2.70", "", { "dependencies": { "@cwcss/crosswind": "^0.2.0", "@stacksjs/clapp": "^0.2.0", "@stacksjs/desktop": "0.2.70", "@stacksjs/sanitizer": "0.2.70", "@stacksjs/ts-craft": "^0.0.2", "bun-plugin-stx": "0.2.70", "bunfig": "^0.15.6", "stx-router": "0.2.70", "ts-broadcasting": "^0.0.4", "ts-images": "^0.1.8", "ts-syntax-highlighter": "^0.2.1" }, "bin": { "stx": "./dist/cli.js" } }, "sha512-dPOj5qvaZLQGKd3PlZawH/qL9IiUFUaSIKVpjUFp4/fqeOpdA7QbPC5vWhyYCeVa+tHyQwetBndwwjCYAHPjOw=="], | |
| 297 | 297 | |
| 298 | 298 | "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], |
| 299 | 299 | |
| @@ -311,6 +311,16 @@ | ||
| 311 | 311 | |
| 312 | 312 | "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], |
| 313 | 313 | |
| 314 | "@stacksjs/bunpress/@stacksjs/stx/@cwcss/crosswind": ["@cwcss/crosswind@0.2.4", "", { "dependencies": { "@stacksjs/clapp": "^0.2.0", "bunfig": "^0.15.0" }, "bin": { "crosswind": "./dist/cli.js" } }, "sha512-LKcLryQl7Ue4LTbS3l1pBgcr2t4wIXdBtEJB1nlW5uJmS1o+cwR1uMASb2r9b1mtmfkp4xIouSRUvRRGAGbxww=="], | |
| 315 | ||
| 316 | "@stacksjs/bunpress/@stacksjs/stx/@stacksjs/desktop": ["@stacksjs/desktop@0.2.70", "", { "peerDependencies": { "craft": "*" }, "optionalPeers": ["craft"] }, "sha512-tBwE98HT+bajYzm4oY2NZtXz5+c9DjX6rUVXs0d8vr3Kv/4pof6tx+GTw64Z+lqvdB8wgCISRduDIVZiZjyDWQ=="], | |
| 317 | ||
| 318 | "@stacksjs/bunpress/@stacksjs/stx/@stacksjs/sanitizer": ["@stacksjs/sanitizer@0.2.70", "", {}, "sha512-jRuRn0VyCmlqM9H2yOuChV1dkxBozW+THBAnGcraYzPjf5CD1x/tRz9PGzS8h6PlEUSBfBi2zr+wPEwUKj1GoA=="], | |
| 319 | ||
| 320 | "@stacksjs/bunpress/@stacksjs/stx/bun-plugin-stx": ["bun-plugin-stx@0.2.70", "", { "dependencies": { "@stacksjs/stx": "0.2.70", "stx-router": "0.2.70" }, "bin": { "serve": "./dist/serve.js", "stx-serve": "./dist/serve.js" } }, "sha512-7bJ6uzQLWqNM5p9Pgha9fNhVTz0gBT4LJyXEEmkMOIljevp7zKiWBa0r2F1CM0eXqSlUfqgI659mtfkhB9MiSA=="], | |
| 321 | ||
| 322 | "@stacksjs/bunpress/@stacksjs/stx/stx-router": ["stx-router@0.2.70", "", {}, "sha512-b0tF8pmqVsgC0I0epav9NURHJk48vVe62QfQFK0jQiVBnAYTJREgjRK9gdrCxIIX/DAmoyI39zsYoKzv/tyiAQ=="], | |
| 323 | ||
| 314 | 324 | "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], |
| 315 | 325 | |
| 316 | 326 | "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], |
| @@ -66,9 +66,9 @@ | ||
| 66 | 66 | "dependencies": { |
| 67 | 67 | "@cwcss/crosswind": "^0.2.6", |
| 68 | 68 | "@stacksjs/bun-router": "^0.0.16", |
| 69 | "@stacksjs/components": "^0.2.67", | |
| 70 | "@stacksjs/stx": "^0.2.70", | |
| 71 | "bun-plugin-stx": "^0.2.70", | |
| 69 | "@stacksjs/components": "^0.2.72", | |
| 70 | "@stacksjs/stx": "^0.2.72", | |
| 71 | "bun-plugin-stx": "^0.2.72", | |
| 72 | 72 | "bun-query-builder": "^0.1.13", |
| 73 | 73 | "dynamodb-tooling": "^0.3.2", |
| 74 | 74 | "ts-countries": "^0.1.2" |
| @@ -107,8 +107,8 @@ onMount(() => { | ||
| 107 | 107 | <div class="col-span-full max-w-[640px]"> |
| 108 | 108 | <h3 class="text-base font-semibold text-text mb-1">Account</h3> |
| 109 | 109 | <p class="text-xs text-muted mb-6"> |
| 110 | Signed in as <span class="text-text" x-text="email()"></span> | |
| 111 | <span class="text-[0.625rem] font-semibold py-0.5 px-[7px] rounded uppercase tracking-wide bg-bg-3 text-text-2 ml-1" x-text="plan()"></span> | |
| 110 | Signed in as <span class="text-text">{{ email() }}</span> | |
| 111 | <span class="text-[0.625rem] font-semibold py-0.5 px-[7px] rounded uppercase tracking-wide bg-bg-3 text-text-2 ml-1">{{ plan() }}</span> | |
| 112 | 112 | <span x-show="!emailVerified()" class="text-warning ml-1">— email not verified</span> |
| 113 | 113 | </p> |
| 114 | 114 | |
| @@ -121,8 +121,8 @@ onMount(() => { | ||
| 121 | 121 | </div> |
| 122 | 122 | <button type="submit" class="btn btn-primary text-[0.8125rem] py-2 px-4">Save</button> |
| 123 | 123 | </form> |
| 124 | <p x-show="profileMsg()" class="text-xs text-success mt-2" x-text="profileMsg()"></p> | |
| 125 | <p x-show="profileErr()" class="text-xs text-error mt-2" x-text="profileErr()"></p> | |
| 124 | <p x-show="profileMsg()" class="text-xs text-success mt-2">{{ profileMsg() }}</p> | |
| 125 | <p x-show="profileErr()" class="text-xs text-error mt-2">{{ profileErr() }}</p> | |
| 126 | 126 | </div> |
| 127 | 127 | |
| 128 | 128 | <div class="panel mb-4"> |
| @@ -138,8 +138,8 @@ onMount(() => { | ||
| 138 | 138 | </div> |
| 139 | 139 | <button type="submit" class="btn btn-primary text-[0.8125rem] py-2 px-4 self-start">Change password</button> |
| 140 | 140 | </form> |
| 141 | <p x-show="passwordMsg()" class="text-xs text-success mt-2" x-text="passwordMsg()"></p> | |
| 142 | <p x-show="passwordErr()" class="text-xs text-error mt-2" x-text="passwordErr()"></p> | |
| 141 | <p x-show="passwordMsg()" class="text-xs text-success mt-2">{{ passwordMsg() }}</p> | |
| 142 | <p x-show="passwordErr()" class="text-xs text-error mt-2">{{ passwordErr() }}</p> | |
| 143 | 143 | </div> |
| 144 | 144 | |
| 145 | 145 | <div class="panel mb-4"> |
| @@ -155,8 +155,8 @@ onMount(() => { | ||
| 155 | 155 | </div> |
| 156 | 156 | <button type="submit" class="btn btn-primary text-[0.8125rem] py-2 px-4 self-start">Change email</button> |
| 157 | 157 | </form> |
| 158 | <p x-show="emailMsg()" class="text-xs text-success mt-2" x-text="emailMsg()"></p> | |
| 159 | <p x-show="emailErr()" class="text-xs text-error mt-2" x-text="emailErr()"></p> | |
| 158 | <p x-show="emailMsg()" class="text-xs text-success mt-2">{{ emailMsg() }}</p> | |
| 159 | <p x-show="emailErr()" class="text-xs text-error mt-2">{{ emailErr() }}</p> | |
| 160 | 160 | </div> |
| 161 | 161 | |
| 162 | 162 | <div class="panel"> |
| @@ -35,9 +35,9 @@ onMount(() => { | ||
| 35 | 35 | </thead> |
| 36 | 36 | <tbody> |
| 37 | 37 | <tr x-for="browser in analytics.browsers" @loading="analytics.loading()"> |
| 38 | <td class="cell-name" x-text="browser.name ? browser.name : 'Unknown'"></td> | |
| 39 | <td class="cell-value" x-text="fmt(browser.visitors)"></td> | |
| 40 | <td class="cell-value" x-text="(browser.percentage ? browser.percentage : 0) + '%'"></td> | |
| 38 | <td class="cell-name">{{ browser.name ? browser.name : 'Unknown' }}</td> | |
| 39 | <td class="cell-value">{{ fmt(browser.visitors) }}</td> | |
| 40 | <td class="cell-value">{{ (browser.percentage ? browser.percentage : 0) + '%' }}</td> | |
| 41 | 41 | </tr> |
| 42 | 42 | <tr @for-loading> |
| 43 | 43 | <td colspan="3" class="text-center text-muted py-4">Loading...</td> |
| @@ -53,9 +53,9 @@ onMount(() => { | ||
| 53 | 53 | </thead> |
| 54 | 54 | <tbody> |
| 55 | 55 | <tr x-for="campaign in campaigns" @loading="loading()"> |
| 56 | <td class="cell-name" x-text="campaign.name ? campaign.name : (campaign.source ? campaign.source : 'Unknown')"></td> | |
| 57 | <td class="cell-value" x-text="fmt(campaign.visitors ? campaign.visitors : 0)"></td> | |
| 58 | <td class="cell-value" x-text="fmt(campaign.views ? campaign.views : 0)"></td> | |
| 56 | <td class="cell-name">{{ campaign.name ? campaign.name : (campaign.source ? campaign.source : 'Unknown') }}</td> | |
| 57 | <td class="cell-value">{{ fmt(campaign.visitors ? campaign.visitors : 0) }}</td> | |
| 58 | <td class="cell-value">{{ fmt(campaign.views ? campaign.views : 0) }}</td> | |
| 59 | 59 | </tr> |
| 60 | 60 | <tr @for-loading> |
| 61 | 61 | <td colspan="3" class="text-center text-muted py-4">Loading...</td> |
| @@ -62,13 +62,13 @@ effect(() => { | ||
| 62 | 62 | <div class="flex flex-col gap-3"> |
| 63 | 63 | <div x-for="ch in channels" @loading="loading()" class="flex flex-col gap-1"> |
| 64 | 64 | <div class="flex justify-between items-center"> |
| 65 | <span class="text-[0.8125rem] text-text font-medium" x-text="ch.channel"></span> | |
| 66 | <span class="text-xs text-muted shrink-0 ml-2" x-text="pct(ch.visitors ? ch.visitors : 0) + '%'"></span> | |
| 65 | <span class="text-[0.8125rem] text-text font-medium">{{ ch.channel }}</span> | |
| 66 | <span class="text-xs text-muted shrink-0 ml-2">{{ pct(ch.visitors ? ch.visitors : 0) + '%' }}</span> | |
| 67 | 67 | </div> |
| 68 | 68 | <div class="h-1.5 bg-bg-3 rounded-sm overflow-hidden"> |
| 69 | 69 | <div class="h-full bg-accent rounded-sm transition-[width] duration-300" x-style="'width: ' + pct(ch.visitors ? ch.visitors : 0) + '%'"></div> |
| 70 | 70 | </div> |
| 71 | <div class="text-[0.6875rem] text-muted text-right" x-text="fmtNum(ch.visitors) + ' visitors'"></div> | |
| 71 | <div class="text-[0.6875rem] text-muted text-right">{{ fmtNum(ch.visitors) + ' visitors' }}</div> | |
| 72 | 72 | </div> |
| 73 | 73 | <div @for-loading class="text-center text-muted py-4 text-[0.8125rem]">Loading...</div> |
| 74 | 74 | <div @for-empty class="text-center text-muted py-4 text-[0.8125rem]">No channel data</div> |
| @@ -116,7 +116,7 @@ provide('controlsBar', { | ||
| 116 | 116 | |
| 117 | 117 | <div id="controls-bar" class="flex justify-between items-center py-3 px-6 bg-bg border-b border-border gap-4 flex-wrap max-sm:flex-col max-sm:items-stretch" x-show="dashboard.controlsVisible()"> |
| 118 | 118 | <div class="flex items-center gap-1 max-sm:justify-center"> |
| 119 | <button x-for="range in ranges" x-class="rangeBtnClass(range)" @click="setRange(range.id)" x-text="range.label"></button> | |
| 119 | <button x-for="range in ranges" x-class="rangeBtnClass(range)" @click="setRange(range.id)">{{ range.label }}</button> | |
| 120 | 120 | <span class="border-l border-border h-6 mx-2"></span> |
| 121 | 121 | <button id="compare-btn" x-class="compareBtnClass()" @click="toggleComparison()" title="Compare with previous period"> |
| 122 | 122 | <Icon name="bar-chart-2" size="14" /> |
| @@ -126,9 +126,9 @@ provide('controlsBar', { | ||
| 126 | 126 | <div class="flex items-center gap-3 max-sm:justify-between"> |
| 127 | 127 | <div class="realtime-badge flex items-center gap-2 py-1.5 px-3 rounded-full text-xs font-medium text-success"> |
| 128 | 128 | <span class="pulse"></span> |
| 129 | <span id="realtime-count" x-text="realtimeLabel()"></span> | |
| 129 | <span id="realtime-count">{{ realtimeLabel() }}</span> | |
| 130 | 130 | </div> |
| 131 | <span id="last-updated" class="text-xs text-muted" x-show="lastUpdated()" x-text="lastUpdated()"></span> | |
| 131 | <span id="last-updated" class="text-xs text-muted" x-show="lastUpdated()">{{ lastUpdated() }}</span> | |
| 132 | 132 | <button id="refresh-btn" x-class="refreshBtnClass()" @click="refresh()" title="Refresh data"> |
| 133 | 133 | <Icon name="refresh-cw" size="16" /> |
| 134 | 134 | </button> |
| @@ -52,10 +52,10 @@ onMount(() => { | ||
| 52 | 52 | <tbody> |
| 53 | 53 | <tr x-for="country in analytics.countries" @loading="analytics.loading()"> |
| 54 | 54 | <td class="cell-name flex items-center gap-1.5"> |
| 55 | <span class="text-base" x-text="getFlag(country.name ? country.name : country.code)"></span> | |
| 56 | <span x-text="country.name ? country.name : (country.code ? country.code : 'Unknown')"></span> | |
| 55 | <span class="text-base">{{ getFlag(country.name ? country.name : country.code) }}</span> | |
| 56 | <span>{{ country.name ? country.name : (country.code ? country.code : 'Unknown') }}</span> | |
| 57 | 57 | </td> |
| 58 | <td class="cell-value" x-text="fmt(country.visitors)"></td> | |
| 58 | <td class="cell-value">{{ fmt(country.visitors) }}</td> | |
| 59 | 59 | </tr> |
| 60 | 60 | <tr @for-loading> |
| 61 | 61 | <td colspan="2" class="text-center text-muted py-4">Loading...</td> |
| @@ -156,7 +156,7 @@ provide('dashboardHeader', { | ||
| 156 | 156 | <button class="icon-btn" @click="handleBack()" title="Back to sites"> |
| 157 | 157 | <Icon name="chevron-left" size="20" /> |
| 158 | 158 | </button> |
| 159 | <span id="current-site-name" class="font-semibold text-base text-text" x-text="siteName()"></span> | |
| 159 | <span id="current-site-name" class="font-semibold text-base text-text">{{ siteName() }}</span> | |
| 160 | 160 | </div> |
| 161 | 161 | |
| 162 | 162 | <nav id="header-nav" class="header-nav flex gap-1 flex-wrap justify-center"> |
| @@ -165,7 +165,7 @@ provide('dashboardHeader', { | ||
| 165 | 165 | x-href="tabHref(tab)" |
| 166 | 166 | x-data-tab="tab.id" |
| 167 | 167 | data-stx-prefetch |
| 168 | x-text="tab.label"></a> | |
| 168 | >{{ tab.label }}</a> | |
| 169 | 169 | </nav> |
| 170 | 170 | |
| 171 | 171 | <div class="flex items-center gap-2"> |
| @@ -181,7 +181,7 @@ provide('dashboardHeader', { | ||
| 181 | 181 | </button> |
| 182 | 182 | <a x-href="'/dashboard/account' + (siteId() ? '?siteId=' + encodeURIComponent(siteId()) : '')" x-show="userEmail()" class="flex items-center gap-1.5 icon-btn no-underline" title="Account settings" data-stx-prefetch> |
| 183 | 183 | <Icon name="user" size="18" /> |
| 184 | <span class="text-xs text-muted max-w-[160px] truncate hidden md:inline" x-text="userEmail()"></span> | |
| 184 | <span class="text-xs text-muted max-w-[160px] truncate hidden md:inline">{{ userEmail() }}</span> | |
| 185 | 185 | </a> |
| 186 | 186 | <form method="POST" action="/logout" class="inline"> |
| 187 | 187 | <button type="submit" class="icon-btn hover:text-error" title="Logout"> |
| @@ -54,10 +54,10 @@ onMount(() => { | ||
| 54 | 54 | <svg class="text-muted shrink-0" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"> |
| 55 | 55 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="getIconPath(device.type)"/> |
| 56 | 56 | </svg> |
| 57 | <span x-text="capitalize(device.type ? device.type : 'Unknown')"></span> | |
| 57 | <span>{{ capitalize(device.type ? device.type : 'Unknown') }}</span> | |
| 58 | 58 | </td> |
| 59 | <td class="cell-value" x-text="fmt(device.visitors)"></td> | |
| 60 | <td class="cell-value" x-text="(device.percentage ? device.percentage : 0) + '%'"></td> | |
| 59 | <td class="cell-value">{{ fmt(device.visitors) }}</td> | |
| 60 | <td class="cell-value">{{ (device.percentage ? device.percentage : 0) + '%' }}</td> | |
| 61 | 61 | </tr> |
| 62 | 62 | <tr @for-loading> |
| 63 | 63 | <td colspan="3" class="text-center text-muted py-4">Loading...</td> |
| @@ -61,11 +61,11 @@ effect(() => { | ||
| 61 | 61 | <div class="flex gap-8 mb-4"> |
| 62 | 62 | <div class="flex flex-col"> |
| 63 | 63 | <span class="text-[0.6875rem] uppercase tracking-wide text-muted">Avg. time on page</span> |
| 64 | <span class="text-lg font-semibold text-text" x-text="fmtTime(avgSeconds())"></span> | |
| 64 | <span class="text-lg font-semibold text-text">{{ fmtTime(avgSeconds()) }}</span> | |
| 65 | 65 | </div> |
| 66 | 66 | <div class="flex flex-col"> |
| 67 | 67 | <span class="text-[0.6875rem] uppercase tracking-wide text-muted">Avg. scroll depth</span> |
| 68 | <span class="text-lg font-semibold text-text" x-text="avgScroll() + '%'"></span> | |
| 68 | <span class="text-lg font-semibold text-text">{{ avgScroll() + '%' }}</span> | |
| 69 | 69 | </div> |
| 70 | 70 | </div> |
| 71 | 71 | |
| @@ -79,9 +79,9 @@ effect(() => { | ||
| 79 | 79 | </thead> |
| 80 | 80 | <tbody> |
| 81 | 81 | <tr x-for="p in pages" @loading="loading()"> |
| 82 | <td class="cell-name truncate" x-text="p.path"></td> | |
| 83 | <td class="cell-value" x-text="fmtTime(p.avgSeconds)"></td> | |
| 84 | <td class="cell-value" x-text="p.avgScrollDepth + '%'"></td> | |
| 82 | <td class="cell-name truncate">{{ p.path }}</td> | |
| 83 | <td class="cell-value">{{ fmtTime(p.avgSeconds) }}</td> | |
| 84 | <td class="cell-value">{{ p.avgScrollDepth + '%' }}</td> | |
| 85 | 85 | </tr> |
| 86 | 86 | <tr @for-loading> |
| 87 | 87 | <td colspan="3" class="text-center text-muted py-4">Loading...</td> |
| @@ -83,16 +83,16 @@ finally { | ||
| 83 | 83 | </a> |
| 84 | 84 | |
| 85 | 85 | <div x-show="loading()" class="text-center text-muted py-12">Loading issue…</div> |
| 86 | <div x-show="!loading() && loadError()" class="text-center text-error py-12" x-text="loadError()"></div> | |
| 86 | <div x-show="!loading() && loadError()" class="text-center text-error py-12">{{ loadError() }}</div> | |
| 87 | 87 | |
| 88 | 88 | <div x-show="!loading() && !loadError()"> |
| 89 | 89 | <!-- Header --> |
| 90 | 90 | <div class="flex items-start justify-between gap-4 mb-4"> |
| 91 | <h1 class="text-lg font-semibold text-text break-words" x-text="issue().message"></h1> | |
| 91 | <h1 class="text-lg font-semibold text-text break-words">{{ issue().message }}</h1> | |
| 92 | 92 | <div class="flex gap-2 shrink-0"> |
| 93 | <span class="text-[0.625rem] font-semibold py-0.5 px-2 rounded uppercase tracking-wide border" x-class="statusColor()" x-text="statusLabel()"></span> | |
| 94 | <span class="text-[0.625rem] font-semibold py-0.5 px-2 rounded uppercase tracking-wide border border-border text-text-2" x-text="severityLabel()"></span> | |
| 95 | <span x-show="issue().status === 'open' && issue().trend" class="text-[0.625rem] font-semibold py-0.5 px-2 rounded uppercase tracking-wide border border-border text-text-2" x-text="issue().trend"></span> | |
| 93 | <span class="text-[0.625rem] font-semibold py-0.5 px-2 rounded uppercase tracking-wide border" x-class="statusColor()">{{ statusLabel() }}</span> | |
| 94 | <span class="text-[0.625rem] font-semibold py-0.5 px-2 rounded uppercase tracking-wide border border-border text-text-2">{{ severityLabel() }}</span> | |
| 95 | <span x-show="issue().status === 'open' && issue().trend" class="text-[0.625rem] font-semibold py-0.5 px-2 rounded uppercase tracking-wide border border-border text-text-2">{{ issue().trend }}</span> | |
| 96 | 96 | </div> |
| 97 | 97 | </div> |
| 98 | 98 | |
| @@ -100,19 +100,19 @@ finally { | ||
| 100 | 100 | <div class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-6"> |
| 101 | 101 | <div class="panel py-3"> |
| 102 | 102 | <div class="text-xs text-muted">Events</div> |
| 103 | <div class="text-base font-semibold text-text" x-text="fmtNum(issue().count)"></div> | |
| 103 | <div class="text-base font-semibold text-text">{{ fmtNum(issue().count) }}</div> | |
| 104 | 104 | </div> |
| 105 | 105 | <div class="panel py-3"> |
| 106 | 106 | <div class="text-xs text-muted">Affected</div> |
| 107 | <div class="text-base font-semibold text-text" x-text="fmtNum(issue().affectedVisitors)"></div> | |
| 107 | <div class="text-base font-semibold text-text">{{ fmtNum(issue().affectedVisitors) }}</div> | |
| 108 | 108 | </div> |
| 109 | 109 | <div class="panel py-3"> |
| 110 | 110 | <div class="text-xs text-muted">First seen</div> |
| 111 | <div class="text-[0.8125rem] font-medium text-text" x-text="when(issue().firstSeen)"></div> | |
| 111 | <div class="text-[0.8125rem] font-medium text-text">{{ when(issue().firstSeen) }}</div> | |
| 112 | 112 | </div> |
| 113 | 113 | <div class="panel py-3"> |
| 114 | 114 | <div class="text-xs text-muted">Last seen</div> |
| 115 | <div class="text-[0.8125rem] font-medium text-text" x-text="when(issue().lastSeen)"></div> | |
| 115 | <div class="text-[0.8125rem] font-medium text-text">{{ when(issue().lastSeen) }}</div> | |
| 116 | 116 | </div> |
| 117 | 117 | </div> |
| 118 | 118 | |
| @@ -122,10 +122,10 @@ finally { | ||
| 122 | 122 | <div class="panel-title m-0">Stack trace</div> |
| 123 | 123 | <span x-show="latest().symbolicatedStack" class="text-[0.625rem] font-semibold py-0.5 px-[7px] rounded uppercase tracking-wide border border-success text-success">Symbolicated</span> |
| 124 | 124 | </div> |
| 125 | <pre class="text-xs text-text-2 overflow-x-auto whitespace-pre-wrap break-words bg-bg-3 rounded-md p-3 m-0" x-text="latest().symbolicatedStack || latest().stack || 'No stack trace captured'"></pre> | |
| 125 | <pre class="text-xs text-text-2 overflow-x-auto whitespace-pre-wrap break-words bg-bg-3 rounded-md p-3 m-0">{{ latest().symbolicatedStack || latest().stack || 'No stack trace captured' }}</pre> | |
| 126 | 126 | <details x-show="latest().symbolicatedStack" class="mt-2"> |
| 127 | 127 | <summary class="text-[0.6875rem] text-muted cursor-pointer">Show raw (minified) stack</summary> |
| 128 | <pre class="text-xs text-text-2 overflow-x-auto whitespace-pre-wrap break-words bg-bg-3 rounded-md p-3 mt-2 mb-0" x-text="latest().stack"></pre> | |
| 128 | <pre class="text-xs text-text-2 overflow-x-auto whitespace-pre-wrap break-words bg-bg-3 rounded-md p-3 mt-2 mb-0">{{ latest().stack }}</pre> | |
| 129 | 129 | </details> |
| 130 | 130 | </div> |
| 131 | 131 | |
| @@ -134,8 +134,8 @@ finally { | ||
| 134 | 134 | <div class="panel-title mb-2">Breadcrumbs</div> |
| 135 | 135 | <div class="flex flex-col gap-1"> |
| 136 | 136 | <div x-for="c in breadcrumbs" class="flex items-center gap-2 text-xs border-b border-bg-3 py-1.5 last:border-0"> |
| 137 | <span class="text-muted font-mono shrink-0 w-28 truncate" x-text="c.category || 'log'"></span> | |
| 138 | <span class="text-text-2 truncate" x-text="c.message || ''"></span> | |
| 137 | <span class="text-muted font-mono shrink-0 w-28 truncate">{{ c.category || 'log' }}</span> | |
| 138 | <span class="text-text-2 truncate">{{ c.message || '' }}</span> | |
| 139 | 139 | </div> |
| 140 | 140 | </div> |
| 141 | 141 | </div> |
| @@ -154,10 +154,10 @@ finally { | ||
| 154 | 154 | </thead> |
| 155 | 155 | <tbody> |
| 156 | 156 | <tr x-for="o in occurrences" class="border-b border-bg-3"> |
| 157 | <td class="py-1.5 text-text-2 whitespace-nowrap" x-text="when(o.timestamp)"></td> | |
| 158 | <td class="py-1.5 text-text-2" x-text="o.browser || '—'"></td> | |
| 159 | <td class="py-1.5 text-text-2" x-text="o.os || '—'"></td> | |
| 160 | <td class="py-1.5 text-text-2 truncate max-w-[24rem]" x-text="o.url || '—'"></td> | |
| 157 | <td class="py-1.5 text-text-2 whitespace-nowrap">{{ when(o.timestamp) }}</td> | |
| 158 | <td class="py-1.5 text-text-2">{{ o.browser || '—' }}</td> | |
| 159 | <td class="py-1.5 text-text-2">{{ o.os || '—' }}</td> | |
| 160 | <td class="py-1.5 text-text-2 truncate max-w-[24rem]">{{ o.url || '—' }}</td> | |
| 161 | 161 | </tr> |
| 162 | 162 | </tbody> |
| 163 | 163 | </table> |
| @@ -167,7 +167,7 @@ finally { | ||
| 167 | 167 | <div x-show="browsers().length > 0" class="panel"> |
| 168 | 168 | <div class="panel-title mb-2">Affected browsers</div> |
| 169 | 169 | <div class="flex flex-wrap gap-2"> |
| 170 | <span x-for="b in browsers" class="text-xs text-text-2 border border-border rounded px-2 py-0.5" x-text="b"></span> | |
| 170 | <span x-for="b in browsers" class="text-xs text-text-2 border border-border rounded px-2 py-0.5">{{ b }}</span> | |
| 171 | 171 | </div> |
| 172 | 172 | </div> |
| 173 | 173 | </div> |
| @@ -74,21 +74,21 @@ effect(() => { | ||
| 74 | 74 | |
| 75 | 75 | <div x-show="eventChips().length > 0" class="flex flex-wrap gap-2 mb-3"> |
| 76 | 76 | <span x-for="chip in eventChips" class="inline-flex items-center gap-1 rounded-full bg-bg-3 px-2 py-0.5 text-[0.6875rem] text-muted"> |
| 77 | <span x-text="chip.name"></span> | |
| 78 | <span class="font-medium text-text" x-text="fmtNum(chip.count)"></span> | |
| 77 | <span>{{ chip.name }}</span> | |
| 78 | <span class="font-medium text-text">{{ fmtNum(chip.count) }}</span> | |
| 79 | 79 | </span> |
| 80 | 80 | </div> |
| 81 | 81 | |
| 82 | 82 | <div class="flex flex-col gap-3"> |
| 83 | 83 | <div x-for="r in rows" @loading="loading()" class="flex flex-col gap-1"> |
| 84 | 84 | <div class="flex justify-between items-center gap-2"> |
| 85 | <span class="text-[0.8125rem] text-text font-medium truncate" x-text="rowLabel(r)"></span> | |
| 86 | <span class="text-xs text-muted shrink-0 ml-2" x-text="fmtNum(r.count)"></span> | |
| 85 | <span class="text-[0.8125rem] text-text font-medium truncate">{{ rowLabel(r) }}</span> | |
| 86 | <span class="text-xs text-muted shrink-0 ml-2">{{ fmtNum(r.count) }}</span> | |
| 87 | 87 | </div> |
| 88 | 88 | <div class="h-1.5 bg-bg-3 rounded-sm overflow-hidden"> |
| 89 | 89 | <div class="h-full bg-accent rounded-sm transition-[width] duration-300" x-style="'width: ' + barWidth(r.count ? r.count : 0) + '%'"></div> |
| 90 | 90 | </div> |
| 91 | <div class="text-[0.6875rem] text-muted text-right" x-text="fmtNum(r.visitors) + ' visitors'"></div> | |
| 91 | <div class="text-[0.6875rem] text-muted text-right">{{ fmtNum(r.visitors) + ' visitors' }}</div> | |
| 92 | 92 | </div> |
| 93 | 93 | <div @for-loading class="text-center text-muted py-4 text-[0.8125rem]">Loading...</div> |
| 94 | 94 | <div @for-empty class="text-center text-muted py-4 text-[0.8125rem]">No event properties tracked yet</div> |
| @@ -60,13 +60,13 @@ onMount(() => { | ||
| 60 | 60 | <div class="flex flex-col gap-3"> |
| 61 | 61 | <div x-for="event in events" @loading="loading()" class="flex flex-col gap-1"> |
| 62 | 62 | <div class="flex justify-between items-center"> |
| 63 | <span class="text-[0.8125rem] text-text font-medium truncate" x-text="event.name"></span> | |
| 64 | <span class="text-xs text-muted shrink-0 ml-2" x-text="getPercentage(event.count ? event.count : 0) + '%'"></span> | |
| 63 | <span class="text-[0.8125rem] text-text font-medium truncate">{{ event.name }}</span> | |
| 64 | <span class="text-xs text-muted shrink-0 ml-2">{{ getPercentage(event.count ? event.count : 0) + '%' }}</span> | |
| 65 | 65 | </div> |
| 66 | 66 | <div class="h-1.5 bg-bg-3 rounded-sm overflow-hidden"> |
| 67 | 67 | <div class="h-full bg-accent rounded-sm transition-[width] duration-300" x-style="'width: ' + getBarWidth(event.count ? event.count : 0) + '%'"></div> |
| 68 | 68 | </div> |
| 69 | <div class="text-[0.6875rem] text-muted text-right" x-text="fmt(event.count)"></div> | |
| 69 | <div class="text-[0.6875rem] text-muted text-right">{{ fmt(event.count) }}</div> | |
| 70 | 70 | </div> |
| 71 | 71 | <div @for-loading class="text-center text-muted py-4 text-[0.8125rem]">Loading...</div> |
| 72 | 72 | <div @for-empty class="text-center text-muted py-4 text-[0.8125rem]">No custom events tracked</div> |
| @@ -128,10 +128,10 @@ onMount(() => { | ||
| 128 | 128 | |
| 129 | 129 | <div x-show="showModal()" class="modal-overlay" @click.self="closeModal"> |
| 130 | 130 | <div class="modal-content bg-bg-2 max-w-[420px] p-6"> |
| 131 | <h3 class="text-lg font-semibold text-text mb-6" x-text="modalTitle()"></h3> | |
| 131 | <h3 class="text-lg font-semibold text-text mb-6">{{ modalTitle() }}</h3> | |
| 132 | 132 | |
| 133 | 133 | <form @submit="handleSubmit"> |
| 134 | <div x-show="error()" class="error-message" x-text="error()"></div> | |
| 134 | <div x-show="error()" class="error-message">{{ error() }}</div> | |
| 135 | 135 | |
| 136 | 136 | <label class="block mb-4 text-[0.8125rem] text-text-2"> |
| 137 | 137 | Name |
| @@ -200,7 +200,7 @@ onMount(() => { | ||
| 200 | 200 | <button type="button" class="btn btn-secondary" @click="closeModal" x-disabled="saving()"> |
| 201 | 201 | Cancel |
| 202 | 202 | </button> |
| 203 | <button type="submit" class="btn btn-primary" x-disabled="saving()" x-text="saving() ? 'Saving...' : 'Save Goal'"></button> | |
| 203 | <button type="submit" class="btn btn-primary" x-disabled="saving()">{{ saving() ? 'Saving...' : 'Save Goal' }}</button> | |
| 204 | 204 | </div> |
| 205 | 205 | </form> |
| 206 | 206 | </div> |
| @@ -92,13 +92,13 @@ window.refreshGoalsPanel = loadGoals | ||
| 92 | 92 | </thead> |
| 93 | 93 | <tbody> |
| 94 | 94 | <tr x-for="goal in goals" class="goal-row"> |
| 95 | <td class="font-medium" x-text="goal.name"></td> | |
| 95 | <td class="font-medium">{{ goal.name }}</td> | |
| 96 | 96 | <td> |
| 97 | <span x-class="getTypeBadgeClass(goal.type)" x-text="goal.type"></span> | |
| 97 | <span x-class="getTypeBadgeClass(goal.type)">{{ goal.type }}</span> | |
| 98 | 98 | </td> |
| 99 | <td class="text-right tabular-nums" x-text="fmt(goal.conversions ? goal.conversions : 0)"></td> | |
| 99 | <td class="text-right tabular-nums">{{ fmt(goal.conversions ? goal.conversions : 0) }}</td> | |
| 100 | 100 | <td class="text-right tabular-nums"> |
| 101 | <span x-show="goal.value" x-text="formatCurrency(goal.value)"></span> | |
| 101 | <span x-show="goal.value">{{ formatCurrency(goal.value) }}</span> | |
| 102 | 102 | <span x-show="!goal.value">-</span> |
| 103 | 103 | </td> |
| 104 | 104 | <td class="text-right whitespace-nowrap"> |
| @@ -74,21 +74,21 @@ effect(() => { | ||
| 74 | 74 | |
| 75 | 75 | <div x-show="kindChips().length > 0" class="flex flex-wrap gap-2 mb-3"> |
| 76 | 76 | <span x-for="chip in kindChips" class="inline-flex items-center gap-1 rounded-full bg-bg-3 px-2 py-0.5 text-[0.6875rem] text-muted"> |
| 77 | <span x-text="chip.label"></span> | |
| 78 | <span class="font-medium text-text" x-text="fmtNum(chip.count)"></span> | |
| 77 | <span>{{ chip.label }}</span> | |
| 78 | <span class="font-medium text-text">{{ fmtNum(chip.count) }}</span> | |
| 79 | 79 | </span> |
| 80 | 80 | </div> |
| 81 | 81 | |
| 82 | 82 | <div class="flex flex-col gap-3"> |
| 83 | 83 | <div x-for="c in clicks" @loading="loading()" class="flex flex-col gap-1"> |
| 84 | 84 | <div class="flex justify-between items-center gap-2"> |
| 85 | <span class="text-[0.8125rem] text-text font-medium truncate" x-text="clickLabel(c)"></span> | |
| 86 | <span class="shrink-0 rounded-sm bg-bg-3 px-1.5 py-0.5 text-[0.625rem] uppercase tracking-wide text-muted" x-text="c.kind"></span> | |
| 85 | <span class="text-[0.8125rem] text-text font-medium truncate">{{ clickLabel(c) }}</span> | |
| 86 | <span class="shrink-0 rounded-sm bg-bg-3 px-1.5 py-0.5 text-[0.625rem] uppercase tracking-wide text-muted">{{ c.kind }}</span> | |
| 87 | 87 | </div> |
| 88 | 88 | <div class="h-1.5 bg-bg-3 rounded-sm overflow-hidden"> |
| 89 | 89 | <div class="h-full bg-accent rounded-sm transition-[width] duration-300" x-style="'width: ' + barWidth(c.count ? c.count : 0) + '%'"></div> |
| 90 | 90 | </div> |
| 91 | <div class="text-[0.6875rem] text-muted text-right" x-text="fmtNum(c.count) + ' clicks'"></div> | |
| 91 | <div class="text-[0.6875rem] text-muted text-right">{{ fmtNum(c.count) + ' clicks' }}</div> | |
| 92 | 92 | </div> |
| 93 | 93 | <div @for-loading class="text-center text-muted py-4 text-[0.8125rem]">Loading...</div> |
| 94 | 94 | <div @for-empty class="text-center text-muted py-4 text-[0.8125rem]">No link clicks tracked yet</div> |
| @@ -40,7 +40,7 @@ effect(() => { | ||
| 40 | 40 | <Icon name="bar-chart-2" size="48" class="text-warning mb-4 mx-auto" /> |
| 41 | 41 | <h3 class="mb-2 text-warning">No analytics data yet</h3> |
| 42 | 42 | <p class="text-text-2 mb-4">Add the tracking script to your website to start collecting data.</p> |
| 43 | <code id="tracking-script" class="block bg-bg p-4 rounded-md text-xs mt-4 text-accent-2 break-all text-left" x-text="trackingSnippet()"></code> | |
| 43 | <code id="tracking-script" class="block bg-bg p-4 rounded-md text-xs mt-4 text-accent-2 break-all text-left">{{ trackingSnippet() }}</code> | |
| 44 | 44 | <div class="flex items-start gap-4 text-left mt-6 pt-6 border-t border-border"> |
| 45 | 45 | <div class="bg-accent text-white w-6 h-6 rounded-full inline-flex items-center justify-center text-xs font-semibold flex-shrink-0">1</div> |
| 46 | 46 | <div class="flex-1"> |
| @@ -38,13 +38,13 @@ onMount(() => { | ||
| 38 | 38 | <tr x-for="page in analytics.pages" @loading="analytics.loading()"> |
| 39 | 39 | <td class="cell-name" :title="page.path"> |
| 40 | 40 | <a x-href="page.path" target="_blank" rel="noopener" class="inline-flex items-center gap-1 hover:text-accent"> |
| 41 | <span x-text="page.path"></span> | |
| 41 | <span>{{ page.path }}</span> | |
| 42 | 42 | <Icon name="external-link" size="10" class="opacity-50" /> |
| 43 | 43 | </a> |
| 44 | 44 | </td> |
| 45 | <td class="cell-value" x-text="fmt(page.entries)"></td> | |
| 46 | <td class="cell-value" x-text="fmt(page.visitors)"></td> | |
| 47 | <td class="cell-value" x-text="fmt(page.views)"></td> | |
| 45 | <td class="cell-value">{{ fmt(page.entries) }}</td> | |
| 46 | <td class="cell-value">{{ fmt(page.visitors) }}</td> | |
| 47 | <td class="cell-value">{{ fmt(page.views) }}</td> | |
| 48 | 48 | </tr> |
| 49 | 49 | <tr @for-loading> |
| 50 | 50 | <td colspan="4" class="text-center text-muted py-4">Loading...</td> |
| @@ -83,13 +83,13 @@ onMount(() => { | ||
| 83 | 83 | class="align-middle rounded-sm" |
| 84 | 84 | @error="e => e.target.style.display = 'none'" |
| 85 | 85 | /> |
| 86 | <span x-text="ref.source ? ref.source : 'Direct'"></span> | |
| 86 | <span>{{ ref.source ? ref.source : 'Direct' }}</span> | |
| 87 | 87 | <Icon name="external-link" size="10" class="opacity-50" /> |
| 88 | 88 | </a> |
| 89 | <span x-show="!isExternalLink(ref.source ? ref.source : 'Direct')" x-text="ref.source ? ref.source : 'Direct'"></span> | |
| 89 | <span x-show="!isExternalLink(ref.source ? ref.source : 'Direct')">{{ ref.source ? ref.source : 'Direct' }}</span> | |
| 90 | 90 | </td> |
| 91 | <td class="cell-value" x-text="fmt(ref.visitors)"></td> | |
| 92 | <td class="cell-value" x-text="fmt(ref.views)"></td> | |
| 91 | <td class="cell-value">{{ fmt(ref.visitors) }}</td> | |
| 92 | <td class="cell-value">{{ fmt(ref.views) }}</td> | |
| 93 | 93 | </tr> |
| 94 | 94 | <tr @for-loading> |
| 95 | 95 | <td colspan="3" class="text-center text-muted py-4">Loading...</td> |
| @@ -205,7 +205,7 @@ onMount(() => { | ||
| 205 | 205 | |
| 206 | 206 | <div x-show="!loading() && error()" class="site-status"> |
| 207 | 207 | <Icon name="alert-triangle" size="32" class="text-error" style="opacity:0.7;margin-bottom:0.75rem" /> |
| 208 | <p x-text="error()"></p> | |
| 208 | <p>{{ error() }}</p> | |
| 209 | 209 | <button class="retry-btn" @click="fetchSites()">Retry</button> |
| 210 | 210 | </div> |
| 211 | 211 | |
| @@ -219,9 +219,9 @@ onMount(() => { | ||
| 219 | 219 | <input type="text" placeholder="Domain (optional)" |
| 220 | 220 | x-value="newDomain()" @input="newDomain.set($event.target.value)" |
| 221 | 221 | class="form-input" /> |
| 222 | <button type="submit" class="create-btn" x-disabled="creating()" x-text="creating() ? 'Creating...' : 'Create'"></button> | |
| 222 | <button type="submit" class="create-btn" x-disabled="creating()">{{ creating() ? 'Creating...' : 'Create' }}</button> | |
| 223 | 223 | </form> |
| 224 | <p x-show="createError()" class="create-error" x-text="createError()"></p> | |
| 224 | <p x-show="createError()" class="create-error">{{ createError() }}</p> | |
| 225 | 225 | </div> |
| 226 | 226 | |
| 227 | 227 | <div x-show="sites().length === 0" class="site-empty"> |
| @@ -237,8 +237,8 @@ onMount(() => { | ||
| 237 | 237 | <Icon name="clock" size="24" /> |
| 238 | 238 | </div> |
| 239 | 239 | <div class="site-info"> |
| 240 | <span class="site-name" x-text="site.name || 'Unnamed'"></span> | |
| 241 | <span class="site-domain" x-text="site.domains?.[0] || site.id"></span> | |
| 240 | <span class="site-name">{{ site.name || 'Unnamed' }}</span> | |
| 241 | <span class="site-domain">{{ site.domains?.[0] || site.id }}</span> | |
| 242 | 242 | </div> |
| 243 | 243 | <Icon name="chevron-right" size="20" class="site-arrow" /> |
| 244 | 244 | </button> |
| @@ -254,12 +254,12 @@ onMount(() => { | ||
| 254 | 254 | <Icon name="globe" size="24" /> |
| 255 | 255 | </div> |
| 256 | 256 | <div class="site-info"> |
| 257 | <span class="site-name" x-text="site.name || 'Unnamed'"></span> | |
| 258 | <span class="site-domain" x-text="site.domains?.[0] || site.id"></span> | |
| 257 | <span class="site-name">{{ site.name || 'Unnamed' }}</span> | |
| 258 | <span class="site-domain">{{ site.domains?.[0] || site.id }}</span> | |
| 259 | 259 | </div> |
| 260 | 260 | <Icon name="chevron-right" size="20" class="site-arrow" /> |
| 261 | 261 | </button> |
| 262 | <p x-show="searchQuery() && filteredSites().length === 0" class="text-xs text-muted mt-2">No projects match "<span x-text="searchQuery()"></span>"</p> | |
| 262 | <p x-show="searchQuery() && filteredSites().length === 0" class="text-xs text-muted mt-2">No projects match "<span></span>"</p> | |
| 263 | 263 | </div> |
| 264 | 264 | </div> |
| 265 | 265 | </div> |
| @@ -145,7 +145,7 @@ onMount(() => { | ||
| 145 | 145 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="stat.icon"/> |
| 146 | 146 | </svg> |
| 147 | 147 | <div class="text-2xl font-bold text-text leading-tight" x-text="statValue(stat.id)">—</div> |
| 148 | <div class="text-xs text-text-2 mt-1 uppercase tracking-wide" x-text="stat.label"></div> | |
| 148 | <div class="text-xs text-text-2 mt-1 uppercase tracking-wide">{{ stat.label }}</div> | |
| 149 | 149 | </div> |
| 150 | 150 | </div> |
| 151 | 151 | |
| @@ -207,9 +207,7 @@ onMount(() => { | ||
| 207 | 207 | <div class="flex gap-1 mb-4"> |
| 208 | 208 | <button x-for="r in dateRangeOptions" |
| 209 | 209 | x-class="rangeBtnClass(r.id)" |
| 210 | @click="setErrorDateRange(r.id)" | |
| 211 | x-text="r.label"> | |
| 212 | </button> | |
| 210 | @click="setErrorDateRange(r.id)">{{ r.label }}</button> | |
| 213 | 211 | </div> |
| 214 | 212 | |
| 215 | 213 | <div x-show="loading()" class="text-center text-text-2 py-10 px-8 bg-bg-2 border border-border rounded-lg flex flex-col items-center gap-2"> |
| @@ -231,8 +229,8 @@ onMount(() => { | ||
| 231 | 229 | <div x-show="!loading() && !error() && errors().length > 0"> |
| 232 | 230 | <div class="severity-grid grid grid-cols-4 gap-2 mb-3"> |
| 233 | 231 | <div x-for="sev in severityKeys" x-class="sevStatClass(sev)"> |
| 234 | <span class="text-lg font-bold text-text" x-text="severityCounts()[sev]"></span> | |
| 235 | <span class="text-[0.6875rem] text-muted uppercase tracking-wider" x-text="severityLabels[sev]"></span> | |
| 232 | <span class="text-lg font-bold text-text">{{ severityCounts()[sev] }}</span> | |
| 233 | <span class="text-[0.6875rem] text-muted uppercase tracking-wider">{{ severityLabels[sev] }}</span> | |
| 236 | 234 | </div> |
| 237 | 235 | </div> |
| 238 | 236 | |
| @@ -275,24 +273,24 @@ onMount(() => { | ||
| 275 | 273 | <div class="flex-1 min-w-0 py-3 px-4 flex flex-col gap-1.5"> |
| 276 | 274 | <div class="flex items-center justify-between gap-2"> |
| 277 | 275 | <div class="flex gap-1.5 items-center flex-wrap"> |
| 278 | <span x-class="badgeSeverityClass(e)" x-text="severityLabels[e.severity] || 'Unknown'"></span> | |
| 279 | <span class="text-[0.625rem] font-semibold py-0.5 px-[7px] rounded uppercase tracking-wide whitespace-nowrap bg-bg-3 text-text-2" x-text="e.category || 'Error'"></span> | |
| 280 | <span x-class="badgeStatusClass(e)" x-text="statusLabels[getErrorStatus(e.fingerprint)] || 'New'"></span> | |
| 276 | <span x-class="badgeSeverityClass(e)">{{ severityLabels[e.severity] || 'Unknown' }}</span> | |
| 277 | <span class="text-[0.625rem] font-semibold py-0.5 px-[7px] rounded uppercase tracking-wide whitespace-nowrap bg-bg-3 text-text-2">{{ e.category || 'Error' }}</span> | |
| 278 | <span x-class="badgeStatusClass(e)">{{ statusLabels[getErrorStatus(e.fingerprint)] || 'New' }}</span> | |
| 281 | 279 | <span x-show="isRegressed(e.fingerprint)" class="text-[0.625rem] font-semibold py-0.5 px-[7px] rounded uppercase tracking-wide whitespace-nowrap border border-error text-error">Regressed</span> |
| 282 | 280 | </div> |
| 283 | 281 | <span class="inline-flex items-center gap-1 text-[0.6875rem] font-semibold text-text-2 whitespace-nowrap"> |
| 284 | 282 | <Icon name="zap" size="12" class="text-muted" /> |
| 285 | <span x-text="e.count"></span> | |
| 283 | <span>{{ e.count }}</span> | |
| 286 | 284 | </span> |
| 287 | 285 | </div> |
| 288 | <div class="text-[0.8125rem] text-text leading-relaxed overflow-hidden text-ellipsis line-clamp-2 break-words" x-text="e.message || 'Unknown error'"></div> | |
| 286 | <div class="text-[0.8125rem] text-text leading-relaxed overflow-hidden text-ellipsis line-clamp-2 break-words">{{ e.message || 'Unknown error' }}</div> | |
| 289 | 287 | <div class="flex gap-3 items-center flex-wrap"> |
| 290 | 288 | <span class="inline-flex items-center gap-1 text-[0.6875rem] text-muted"> |
| 291 | 289 | <Icon name="clock" size="11" class="opacity-60" /> |
| 292 | <span x-text="timeAgo(e.lastSeen)"></span> | |
| 290 | <span>{{ timeAgo(e.lastSeen) }}</span> | |
| 293 | 291 | </span> |
| 294 | <span x-show="e.source" class="inline-flex items-center gap-1 text-[0.6875rem] text-muted font-mono text-text-2" x-text="e.source.split('/').pop() + (e.line ? ':' + e.line : '')"></span> | |
| 295 | <span class="inline-flex items-center gap-1 text-[0.6875rem] text-muted" x-text="(e.browsers || []).join(', ') || 'server'"></span> | |
| 292 | <span x-show="e.source" class="inline-flex items-center gap-1 text-[0.6875rem] text-muted font-mono text-text-2">{{ e.source.split('/').pop() + (e.line ? ':' + e.line : '') }}</span> | |
| 293 | <span class="inline-flex items-center gap-1 text-[0.6875rem] text-muted">{{ (e.browsers || []).join(', ') || 'server' }}</span> | |
| 296 | 294 | </div> |
| 297 | 295 | </div> |
| 298 | 296 | <div class="error-actions flex flex-col gap-0.5 py-2 px-2 opacity-0 transition-opacity duration-150 items-center justify-center group-hover:opacity-100" @click.stop> |
| @@ -61,8 +61,8 @@ onMount(() => { | ||
| 61 | 61 | <h4 class="text-[0.6875rem] uppercase text-muted mb-3 font-medium tracking-wider">Entry Pages</h4> |
| 62 | 62 | <div class="flex flex-col gap-2"> |
| 63 | 63 | <div x-for="page in entryPages" class="bg-bg border border-border py-2 px-3 rounded-md"> |
| 64 | <div class="text-[0.8125rem] text-text truncate" x-text="page.path"></div> | |
| 65 | <div class="text-[0.6875rem] text-muted" x-text="page.count + ' session' + (page.count !== 1 ? 's' : '')"></div> | |
| 64 | <div class="text-[0.8125rem] text-text truncate">{{ page.path }}</div> | |
| 65 | <div class="text-[0.6875rem] text-muted">{{ page.count + ' session' + (page.count !== 1 ? 's' : '') }}</div> | |
| 66 | 66 | </div> |
| 67 | 67 | </div> |
| 68 | 68 | </div> |
| @@ -71,10 +71,10 @@ onMount(() => { | ||
| 71 | 71 | <h4 class="text-[0.6875rem] uppercase text-muted mb-3 font-medium tracking-wider">Top Flows</h4> |
| 72 | 72 | <div class="flex flex-col gap-2"> |
| 73 | 73 | <div x-for="flow in topFlows" class="flex items-center gap-2 text-xs"> |
| 74 | <span class="bg-bg py-1 px-2 rounded truncate max-w-[120px] text-text" x-text="flow.source"></span> | |
| 74 | <span class="bg-bg py-1 px-2 rounded truncate max-w-[120px] text-text">{{ flow.source }}</span> | |
| 75 | 75 | <span class="text-accent">→</span> |
| 76 | <span class="bg-bg py-1 px-2 rounded truncate max-w-[120px] text-text" x-text="flow.target"></span> | |
| 77 | <span class="text-muted ml-auto" x-text="flow.count"></span> | |
| 76 | <span class="bg-bg py-1 px-2 rounded truncate max-w-[120px] text-text">{{ flow.target }}</span> | |
| 77 | <span class="text-muted ml-auto">{{ flow.count }}</span> | |
| 78 | 78 | </div> |
| 79 | 79 | </div> |
| 80 | 80 | </div> |
| @@ -83,9 +83,9 @@ onMount(() => { | ||
| 83 | 83 | <h4 class="text-[0.6875rem] uppercase text-muted mb-3 font-medium tracking-wider">Most Visited</h4> |
| 84 | 84 | <div class="flex flex-col gap-2"> |
| 85 | 85 | <div x-for="(page, index) in mostVisited" class="flex items-center gap-2"> |
| 86 | <span class="w-[18px] h-[18px] bg-accent text-white rounded-full text-[0.6875rem] flex items-center justify-center shrink-0" x-text="index + 1"></span> | |
| 87 | <span class="text-xs truncate flex-1 text-text" x-text="page.path"></span> | |
| 88 | <span class="text-[0.6875rem] text-muted" x-text="page.count"></span> | |
| 86 | <span class="w-[18px] h-[18px] bg-accent text-white rounded-full text-[0.6875rem] flex items-center justify-center shrink-0">{{ index + 1 }}</span> | |
| 87 | <span class="text-xs truncate flex-1 text-text">{{ page.path }}</span> | |
| 88 | <span class="text-[0.6875rem] text-muted">{{ page.count }}</span> | |
| 89 | 89 | </div> |
| 90 | 90 | </div> |
| 91 | 91 | </div> |
| @@ -193,11 +193,11 @@ onMount(() => { | ||
| 193 | 193 | <div x-for="(step, index) in (currentAnalysis?.steps || [])" class="analysis-step-wrapper flex items-center gap-2 flex-1 min-w-[120px]"> |
| 194 | 194 | <div class="flex-1 text-center"> |
| 195 | 195 | <div class="bg-bg-2 border border-border rounded-lg p-4 mb-2"> |
| 196 | <div class="text-2xl font-semibold text-text" x-text="step.visitors"></div> | |
| 197 | <div class="text-[0.6875rem] text-muted mt-1"><span x-text="step.rate"></span>%</div> | |
| 196 | <div class="text-2xl font-semibold text-text">{{ step.visitors }}</div> | |
| 197 | <div class="text-[0.6875rem] text-muted mt-1"><span>{{ step.rate }}</span>%</div> | |
| 198 | 198 | </div> |
| 199 | <div class="text-xs font-medium text-text" x-text="step.path"></div> | |
| 200 | <div x-show="step.dropoff > 0" class="text-[0.6875rem] text-error mt-1"><span x-text="'-' + step.dropoff + ' dropped'"></span></div> | |
| 199 | <div class="text-xs font-medium text-text">{{ step.path }}</div> | |
| 200 | <div x-show="step.dropoff > 0" class="text-[0.6875rem] text-error mt-1"><span>{{ '-' + step.dropoff + ' dropped' }}</span></div> | |
| 201 | 201 | </div> |
| 202 | 202 | <div x-show="index < currentAnalysis().steps.length - 1" class="text-muted text-2xl">→</div> |
| 203 | 203 | </div> |
| @@ -217,7 +217,7 @@ onMount(() => { | ||
| 217 | 217 | |
| 218 | 218 | <div x-for="funnel in funnels" class="bg-bg-2 border border-border rounded-lg p-4 mb-3"> |
| 219 | 219 | <div class="flex justify-between items-center mb-3"> |
| 220 | <h4 class="text-sm font-semibold text-text m-0" x-text="funnel.name"></h4> | |
| 220 | <h4 class="text-sm font-semibold text-text m-0">{{ funnel.name }}</h4> | |
| 221 | 221 | <div class="flex gap-1"> |
| 222 | 222 | <button class="icon-btn" @click="analyzeFunnel(funnel.id)" title="View analysis"> |
| 223 | 223 | <Icon name="bar-chart-2" size="16" /> |
| @@ -229,7 +229,7 @@ onMount(() => { | ||
| 229 | 229 | </div> |
| 230 | 230 | <div class="flex gap-2 flex-wrap items-center"> |
| 231 | 231 | <span x-for="(step, idx) in funnel.steps" class="text-[0.6875rem] py-1 px-2 bg-bg rounded text-text"> |
| 232 | <span x-text="step"></span> | |
| 232 | <span>{{ step }}</span> | |
| 233 | 233 | <span x-show="idx < funnel.steps.length - 1" class="text-accent">→</span> |
| 234 | 234 | </span> |
| 235 | 235 | </div> |
| @@ -101,8 +101,8 @@ onMount(() => { | ||
| 101 | 101 | </svg> |
| 102 | 102 | </div> |
| 103 | 103 | <div class="flex-1 min-w-0"> |
| 104 | <div class="text-sm font-semibold text-text mb-1" x-text="insight.title"></div> | |
| 105 | <div class="text-[0.8125rem] text-muted leading-normal" x-text="insight.description"></div> | |
| 104 | <div class="text-sm font-semibold text-text mb-1">{{ insight.title }}</div> | |
| 105 | <div class="text-[0.8125rem] text-muted leading-normal">{{ insight.description }}</div> | |
| 106 | 106 | </div> |
| 107 | 107 | </div> |
| 108 | 108 | </div> |
| @@ -56,12 +56,12 @@ onMount(() => { | ||
| 56 | 56 | <div x-for="activity in activities" @loading="loading()" class="panel flex items-center gap-4 py-3 px-4"> |
| 57 | 57 | <div class="w-2 h-2 bg-success rounded-full flex-shrink-0"></div> |
| 58 | 58 | <div class="flex-1 min-w-0"> |
| 59 | <div class="text-sm text-text truncate" x-text="activity.path ? activity.path : '/'"></div> | |
| 59 | <div class="text-sm text-text truncate">{{ activity.path ? activity.path : '/' }}</div> | |
| 60 | 60 | <div class="text-[0.6875rem] text-muted"> |
| 61 | <span x-text="(activity.country ? activity.country : 'Unknown') + ' • ' + (activity.device ? activity.device : 'Unknown') + ' • ' + (activity.browser ? activity.browser : 'Unknown')"></span> | |
| 61 | <span>{{ (activity.country ? activity.country : 'Unknown') + ' • ' + (activity.device ? activity.device : 'Unknown') + ' • ' + (activity.browser ? activity.browser : 'Unknown') }}</span> | |
| 62 | 62 | </div> |
| 63 | 63 | </div> |
| 64 | <div class="text-[0.6875rem] text-muted whitespace-nowrap" x-text="timeAgo(activity.timestamp)"></div> | |
| 64 | <div class="text-[0.6875rem] text-muted whitespace-nowrap">{{ timeAgo(activity.timestamp) }}</div> | |
| 65 | 65 | </div> |
| 66 | 66 | <div @for-loading class="panel flex items-center justify-center gap-2 text-muted"> |
| 67 | 67 | <Spinner size="sm" color="primary" /> |
| @@ -140,14 +140,14 @@ onMount(() => { | ||
| 140 | 140 | <div x-show="!loading() && !error() && sessions().length === 0" class="empty-state">No sessions recorded yet.</div> |
| 141 | 141 | <div x-for="session in sessions" class="session-card" @click="viewSession(session.sessionId || session.id)"> |
| 142 | 142 | <div class="flex justify-between items-center mb-2"> |
| 143 | <span class="font-medium text-text" x-text="session.entryPage || session.entryPath || '/'"></span> | |
| 144 | <span class="text-xs text-muted" x-text="timeAgo(session.startTime || session.startedAt)"></span> | |
| 143 | <span class="font-medium text-text">{{ session.entryPage || session.entryPath || '/' }}</span> | |
| 144 | <span class="text-xs text-muted">{{ timeAgo(session.startTime || session.startedAt) }}</span> | |
| 145 | 145 | </div> |
| 146 | 146 | <div class="flex gap-4 text-xs text-muted"> |
| 147 | <span x-text="(session.pageViews || session.pageViewCount || 1) + ' page' + ((session.pageViews || session.pageViewCount || 1) !== 1 ? 's' : '')"></span> | |
| 148 | <span x-class="bouncedClass(session)" x-text="(session.bounced || session.isBounce) ? 'Bounced' : formatDuration(session.duration)"></span> | |
| 149 | <span x-text="session.browser || 'Unknown'"></span> | |
| 150 | <span x-text="session.country || 'Unknown'"></span> | |
| 147 | <span>{{ (session.pageViews || session.pageViewCount || 1) + ' page' + ((session.pageViews || session.pageViewCount || 1) !== 1 ? 's' : '') }}</span> | |
| 148 | <span x-class="bouncedClass(session)">{{ (session.bounced || session.isBounce) ? 'Bounced' : formatDuration(session.duration) }}</span> | |
| 149 | <span>{{ session.browser || 'Unknown' }}</span> | |
| 150 | <span>{{ session.country || 'Unknown' }}</span> | |
| 151 | 151 | </div> |
| 152 | 152 | </div> |
| 153 | 153 | </div> |
| @@ -542,7 +542,7 @@ onMount(() => { | ||
| 542 | 542 | </button> |
| 543 | 543 | </div> |
| 544 | 544 | </div> |
| 545 | <p class="text-xs text-muted mb-2">Paste this one line into the <code class="text-text"><head></code> of every page you want to track (site <code class="text-text" x-text="siteId()"></code>). No cookies, no consent banner needed — visitors are never personally identified.</p> | |
| 545 | <p class="text-xs text-muted mb-2">Paste this one line into the <code class="text-text"><head></code> of every page you want to track (site <code class="text-text">{{ siteId() }}</code>). No cookies, no consent banner needed — visitors are never personally identified.</p> | |
| 546 | 546 | <div class="flex flex-wrap gap-1.5 mb-3"> |
| 547 | 547 | <span class="text-[0.6875rem] px-2 py-0.5 rounded-full bg-bg border border-border text-muted">Page views</span> |
| 548 | 548 | <span class="text-[0.6875rem] px-2 py-0.5 rounded-full bg-bg border border-border text-muted">SPA route changes</span> |
| @@ -551,7 +551,7 @@ onMount(() => { | ||
| 551 | 551 | <span class="text-[0.6875rem] px-2 py-0.5 rounded-full bg-bg border border-border text-muted">Web vitals</span> |
| 552 | 552 | <span class="text-[0.6875rem] px-2 py-0.5 rounded-full bg-bg border border-border text-muted">JS errors</span> |
| 553 | 553 | </div> |
| 554 | <pre class="bg-bg border border-border rounded-md p-3 text-[0.75rem] text-text overflow-x-auto whitespace-pre-wrap break-all font-mono leading-relaxed" x-text="installSnippet()"></pre> | |
| 554 | <pre class="bg-bg border border-border rounded-md p-3 text-[0.75rem] text-text overflow-x-auto whitespace-pre-wrap break-all font-mono leading-relaxed">{{ installSnippet() }}</pre> | |
| 555 | 555 | <p x-show="!installSnippet()" class="text-xs text-muted italic mt-2">Loading snippet…</p> |
| 556 | 556 | <p x-show="verifyResult() === 'pass'" class="text-xs text-success mt-2">Test event received — ingestion is working end to end.</p> |
| 557 | 557 | <p x-show="verifyResult() === 'fail'" class="text-xs text-error mt-2">No event received. Check that the API is reachable from your browser, then try again.</p> |
| @@ -564,7 +564,7 @@ onMount(() => { | ||
| 564 | 564 | </button> |
| 565 | 565 | </div> |
| 566 | 566 | <p class="text-xs text-muted mb-2">Sends beacons to a short, unlisted path that generic blocklists miss. For full resilience, serve it from your own domain — see the <a class="text-primary underline" href="https://github.com/stacksjs/ts-analytics/blob/main/docs/guide/tracking-script.md#ad-blocker-resilience-first-party-proxy" target="_blank" rel="noopener">first-party proxy guide</a>.</p> |
| 567 | <pre class="bg-bg border border-border rounded-md p-3 text-[0.75rem] text-text overflow-x-auto whitespace-pre-wrap break-all font-mono leading-relaxed" x-text="stealthSnippet()"></pre> | |
| 567 | <pre class="bg-bg border border-border rounded-md p-3 text-[0.75rem] text-text overflow-x-auto whitespace-pre-wrap break-all font-mono leading-relaxed">{{ stealthSnippet() }}</pre> | |
| 568 | 568 | </div> |
| 569 | 569 | </div> |
| 570 | 570 | |
| @@ -580,17 +580,17 @@ onMount(() => { | ||
| 580 | 580 | <span x-show="!sdkCopied()">Copy snippet</span> |
| 581 | 581 | <span x-show="sdkCopied()" class="text-success">Copied!</span> |
| 582 | 582 | </button> |
| 583 | <button class="btn btn-secondary text-[0.6875rem] py-1 px-2 hover:text-error" @click="regenerateDsn()" x-disabled="regeneratingDsn()" x-text="regeneratingDsn() ? 'Regenerating…' : 'Regenerate key'"></button> | |
| 583 | <button class="btn btn-secondary text-[0.6875rem] py-1 px-2 hover:text-error" @click="regenerateDsn()" x-disabled="regeneratingDsn()">{{ regeneratingDsn() ? 'Regenerating…' : 'Regenerate key' }}</button> | |
| 584 | 584 | </div> |
| 585 | 585 | </div> |
| 586 | 586 | <p class="text-xs text-muted mb-3">The analytics snippet above already reports uncaught errors. Use this DSN with the standalone SDK when you want error tracking without analytics, or richer capture (breadcrumbs, releases, <code class="text-text">captureException</code>) in your app code.</p> |
| 587 | 587 | <div class="mb-3"> |
| 588 | 588 | <label class="text-xs font-medium text-muted mb-1 block">DSN</label> |
| 589 | <code class="block bg-bg border border-border rounded-md p-3 text-[0.75rem] text-text break-all font-mono" x-text="dsn()"></code> | |
| 589 | <code class="block bg-bg border border-border rounded-md p-3 text-[0.75rem] text-text break-all font-mono">{{ dsn() }}</code> | |
| 590 | 590 | </div> |
| 591 | 591 | <div> |
| 592 | 592 | <label class="text-xs font-medium text-muted mb-1 block">SDK snippet</label> |
| 593 | <pre class="bg-bg border border-border rounded-md p-3 text-[0.75rem] text-text overflow-x-auto whitespace-pre-wrap break-all font-mono leading-relaxed" x-text="sdkSnippet()"></pre> | |
| 593 | <pre class="bg-bg border border-border rounded-md p-3 text-[0.75rem] text-text overflow-x-auto whitespace-pre-wrap break-all font-mono leading-relaxed">{{ sdkSnippet() }}</pre> | |
| 594 | 594 | </div> |
| 595 | 595 | <p x-show="!dsn()" class="text-xs text-muted italic mt-2">Loading DSN…</p> |
| 596 | 596 | </div> |
| @@ -619,8 +619,8 @@ onMount(() => { | ||
| 619 | 619 | <div class="flex flex-col gap-2"> |
| 620 | 620 | <div x-show="settings().apiKey" class="flex justify-between items-center p-2 bg-bg rounded"> |
| 621 | 621 | <div class="flex-1 min-w-0"> |
| 622 | <div class="text-[0.8125rem] font-medium text-text" x-text="settings().apiKey ? settings().apiKey.name : ''"></div> | |
| 623 | <code class="text-[0.6875rem] text-muted break-all" x-text="settings().apiKey ? settings().apiKey.key : ''"></code> | |
| 622 | <div class="text-[0.8125rem] font-medium text-text">{{ settings().apiKey ? settings().apiKey.name : '' }}</div> | |
| 623 | <code class="text-[0.6875rem] text-muted break-all">{{ settings().apiKey ? settings().apiKey.key : '' }}</code> | |
| 624 | 624 | </div> |
| 625 | 625 | <div class="flex items-center gap-1"> |
| 626 | 626 | <button class="icon-btn" @click="copyApiKey()" title="Copy key"> |
| @@ -643,8 +643,8 @@ onMount(() => { | ||
| 643 | 643 | <p x-show="settings().alerts.length === 0" class="text-xs text-muted italic">No alerts configured.</p> |
| 644 | 644 | <div x-for="alert in settings.alerts" class="flex justify-between items-center p-2 bg-bg rounded"> |
| 645 | 645 | <div class="flex-1 min-w-0"> |
| 646 | <div class="text-[0.8125rem] font-medium text-text" x-text="alert.metric"></div> | |
| 647 | <div class="text-[0.6875rem] text-muted" x-text="'Threshold: ' + alert.threshold + ' → ' + alert.email"></div> | |
| 646 | <div class="text-[0.8125rem] font-medium text-text">{{ alert.metric }}</div> | |
| 647 | <div class="text-[0.6875rem] text-muted">{{ 'Threshold: ' + alert.threshold + ' → ' + alert.email }}</div> | |
| 648 | 648 | </div> |
| 649 | 649 | <button class="icon-btn hover:bg-error/15 hover:text-error" @click="deleteAlert(alert.id)" title="Delete">×</button> |
| 650 | 650 | </div> |
| @@ -661,8 +661,8 @@ onMount(() => { | ||
| 661 | 661 | <p x-show="settings().emailReports.length === 0" class="text-xs text-muted italic">No email reports scheduled.</p> |
| 662 | 662 | <div x-for="report in settings.emailReports" class="flex justify-between items-center p-2 bg-bg rounded"> |
| 663 | 663 | <div class="flex-1 min-w-0"> |
| 664 | <div class="text-[0.8125rem] font-medium text-text" x-text="report.email"></div> | |
| 665 | <div class="text-[0.6875rem] text-muted" x-text="report.frequency"></div> | |
| 664 | <div class="text-[0.8125rem] font-medium text-text">{{ report.email }}</div> | |
| 665 | <div class="text-[0.6875rem] text-muted">{{ report.frequency }}</div> | |
| 666 | 666 | </div> |
| 667 | 667 | <button class="icon-btn hover:bg-error/15 hover:text-error" @click="deleteEmailReport(report.id)" title="Delete">×</button> |
| 668 | 668 | </div> |
| @@ -679,8 +679,8 @@ onMount(() => { | ||
| 679 | 679 | <p x-show="settings().uptimeMonitors.length === 0" class="text-xs text-muted italic">No uptime monitors configured.</p> |
| 680 | 680 | <div x-for="monitor in settings.uptimeMonitors" class="flex justify-between items-center p-2 bg-bg rounded"> |
| 681 | 681 | <div class="flex-1 min-w-0"> |
| 682 | <code class="text-[0.6875rem] text-muted break-all" x-text="monitor.url"></code> | |
| 683 | <div class="text-[0.6875rem] text-muted" x-text="'Every ' + monitor.interval + 'min • ' + (monitor.status || 'Pending')"></div> | |
| 682 | <code class="text-[0.6875rem] text-muted break-all">{{ monitor.url }}</code> | |
| 683 | <div class="text-[0.6875rem] text-muted">{{ 'Every ' + monitor.interval + 'min • ' + (monitor.status || 'Pending') }}</div> | |
| 684 | 684 | </div> |
| 685 | 685 | <button class="icon-btn hover:bg-error/15 hover:text-error" @click="deleteUptimeMonitor(monitor.id)" title="Delete">×</button> |
| 686 | 686 | </div> |
| @@ -700,8 +700,8 @@ onMount(() => { | ||
| 700 | 700 | <p x-show="settings().teamMembers.length === 0" class="text-xs text-muted italic">No team members added.</p> |
| 701 | 701 | <div x-for="member in settings.teamMembers" class="flex justify-between items-center p-2 bg-bg rounded"> |
| 702 | 702 | <div class="flex-1 min-w-0"> |
| 703 | <div class="text-[0.8125rem] font-medium text-text" x-text="member.email"></div> | |
| 704 | <div class="text-[0.6875rem] text-muted" x-text="member.role"></div> | |
| 703 | <div class="text-[0.8125rem] font-medium text-text">{{ member.email }}</div> | |
| 704 | <div class="text-[0.6875rem] text-muted">{{ member.role }}</div> | |
| 705 | 705 | </div> |
| 706 | 706 | <button class="icon-btn hover:bg-error/15 hover:text-error" @click="removeTeamMember(member.id)" title="Remove">×</button> |
| 707 | 707 | </div> |
| @@ -718,8 +718,8 @@ onMount(() => { | ||
| 718 | 718 | <p x-show="settings().webhooks.length === 0" class="text-xs text-muted italic">No webhooks configured.</p> |
| 719 | 719 | <div x-for="webhook in settings.webhooks" class="flex justify-between items-center p-2 bg-bg rounded"> |
| 720 | 720 | <div class="flex-1 min-w-0"> |
| 721 | <code class="text-[0.6875rem] text-muted break-all" x-text="webhook.url"></code> | |
| 722 | <div class="text-[0.6875rem] text-muted" x-text="(webhook.events || []).join(', ')"></div> | |
| 721 | <code class="text-[0.6875rem] text-muted break-all">{{ webhook.url }}</code> | |
| 722 | <div class="text-[0.6875rem] text-muted">{{ (webhook.events || []).join(', ') }}</div> | |
| 723 | 723 | </div> |
| 724 | 724 | <button class="icon-btn hover:bg-error/15 hover:text-error" @click="deleteWebhook(webhook.id)" title="Delete">×</button> |
| 725 | 725 | </div> |
| @@ -736,8 +736,8 @@ onMount(() => { | ||
| 736 | 736 | <p x-show="settings().perfBudgets.length === 0" class="text-xs text-muted italic">No performance budgets set.</p> |
| 737 | 737 | <div x-for="budget in settings.perfBudgets" class="flex justify-between items-center p-2 bg-bg rounded"> |
| 738 | 738 | <div class="flex-1 min-w-0"> |
| 739 | <div class="text-[0.8125rem] font-medium text-text" x-text="budget.metric"></div> | |
| 740 | <div class="text-[0.6875rem] text-muted" x-text="'Threshold: ' + budget.threshold + (budget.metric === 'CLS' ? '' : 'ms')"></div> | |
| 739 | <div class="text-[0.8125rem] font-medium text-text">{{ budget.metric }}</div> | |
| 740 | <div class="text-[0.6875rem] text-muted">{{ 'Threshold: ' + budget.threshold + (budget.metric === 'CLS' ? '' : 'ms') }}</div> | |
| 741 | 741 | </div> |
| 742 | 742 | <button class="icon-btn hover:bg-error/15 hover:text-error" @click="deletePerfBudget(budget.id)" title="Delete">×</button> |
| 743 | 743 | </div> |
| @@ -67,18 +67,18 @@ onMount(() => { | ||
| 67 | 67 | <h4 class="text-sm font-semibold text-error mb-2">Performance Budget Violations</h4> |
| 68 | 68 | <div class="flex flex-col gap-2"> |
| 69 | 69 | <div x-for="v in violations" class="text-[0.8125rem] flex gap-2 items-center"> |
| 70 | <span class="font-semibold text-text" x-text="v.metric"></span> | |
| 71 | <span class="text-error" x-text="v.currentValue + (v.metric === 'CLS' ? '' : 'ms')"></span> | |
| 72 | <span class="text-muted" x-text="'(exceeds ' + v.threshold + (v.metric === 'CLS' ? '' : 'ms') + ' by ' + v.exceededBy + (v.metric === 'CLS' ? '' : 'ms') + ')'"></span> | |
| 70 | <span class="font-semibold text-text">{{ v.metric }}</span> | |
| 71 | <span class="text-error">{{ v.currentValue + (v.metric === 'CLS' ? '' : 'ms') }}</span> | |
| 72 | <span class="text-muted">{{ '(exceeds ' + v.threshold + (v.metric === 'CLS' ? '' : 'ms') + ' by ' + v.exceededBy + (v.metric === 'CLS' ? '' : 'ms') + ')' }}</span> | |
| 73 | 73 | </div> |
| 74 | 74 | </div> |
| 75 | 75 | </div> |
| 76 | 76 | |
| 77 | 77 | <div class="vitals-grid"> |
| 78 | 78 | <div x-for="v in vitals" class="vital-card"> |
| 79 | <div class="text-xs font-semibold text-muted uppercase tracking-wider mb-1" x-text="v.metric"></div> | |
| 80 | <div x-class="vitalValueClass(v)" x-text="v.samples > 0 ? formatValue(v.metric, v.p75) : '—'"></div> | |
| 81 | <div class="text-[0.6875rem] text-muted mb-2" x-text="(v.samples || 0) + ' samples'"></div> | |
| 79 | <div class="text-xs font-semibold text-muted uppercase tracking-wider mb-1">{{ v.metric }}</div> | |
| 80 | <div x-class="vitalValueClass(v)">{{ v.samples > 0 ? formatValue(v.metric, v.p75) : '—' }}</div> | |
| 81 | <div class="text-[0.6875rem] text-muted mb-2">{{ (v.samples || 0) + ' samples' }}</div> | |
| 82 | 82 | <div x-show="v.samples > 0" class="h-1 rounded-sm flex overflow-hidden bg-bg"> |
| 83 | 83 | <div class="good" x-style="'width: ' + v.good + '%'"></div> |
| 84 | 84 | <div class="needs-improvement" x-style="'width: ' + v.needsImprovement + '%'"></div> |