mirror of
https://github.com/furyhawk/bus_sg_app.git
synced 2026-07-20 09:47:58 +00:00
feat: initialize SvelteKit project with bus API explorer
- Add main application styles in app.css - Create basic HTML structure in app.html - Implement server-side hooks for API proxying in hooks.server.js - Set up layout component in +layout.svelte - Develop main page with endpoint explorer and arrival check functionality in +page.svelte - Configure SvelteKit with adapter in svelte.config.js - Set up Vite configuration for SvelteKit in vite.config.js
This commit is contained in:
+26
@@ -0,0 +1,26 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Build artifacts
|
||||
coverage/
|
||||
dist/
|
||||
build/
|
||||
.svelte-kit/
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
|
||||
# Editor settings
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -4,21 +4,36 @@ A lightweight bus transport web app that uses the gateway OpenAPI contract at:
|
||||
|
||||
- http://localhost:8067/openapi.json
|
||||
|
||||
The frontend dynamically loads transport endpoints from the OpenAPI spec and calls them through a local proxy server.
|
||||
The frontend is built with SvelteKit. It dynamically loads transport endpoints from the OpenAPI spec and calls them through a SvelteKit server hook proxy.
|
||||
|
||||
## Run
|
||||
|
||||
1. Ensure your API gateway is running at http://localhost:8067.
|
||||
2. Start this app:
|
||||
2. Install dependencies:
|
||||
|
||||
```bash
|
||||
bun run start
|
||||
bun install
|
||||
```
|
||||
|
||||
3. Open:
|
||||
3. Start this app in development:
|
||||
|
||||
```bash
|
||||
bun run dev
|
||||
```
|
||||
|
||||
4. Open:
|
||||
|
||||
- http://localhost:3000
|
||||
|
||||
## Build and preview
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
bun run preview
|
||||
```
|
||||
|
||||
You can also use `bun run start` to run preview mode.
|
||||
|
||||
## Features
|
||||
|
||||
- Dynamic endpoint explorer built from OpenAPI paths under /api/v1/*
|
||||
@@ -26,6 +41,7 @@ bun run start
|
||||
- Quick bus-arrival panel with BusStopCode and optional ServiceNo
|
||||
- JSON response viewer
|
||||
- Arrival board card view when the response includes Services
|
||||
- Server-side proxying for `/api/*` and `/openapi.json`
|
||||
|
||||
## Config
|
||||
|
||||
@@ -37,5 +53,5 @@ Environment variables:
|
||||
Example:
|
||||
|
||||
```bash
|
||||
API_ORIGIN=http://localhost:8067 PORT=3001 bun run start
|
||||
API_ORIGIN=http://localhost:8067 PORT=3001 bun run dev -- --port 3001
|
||||
```
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bus-sg-app",
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^7.0.1",
|
||||
"@sveltejs/kit": "^2.61.1",
|
||||
"svelte": "^5.56.0",
|
||||
"vite": "^7.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.61.0", "", { "os": "android", "cpu": "arm" }, "sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.61.0", "", { "os": "android", "cpu": "arm64" }, "sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.61.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.61.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.61.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.61.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.61.0", "", { "os": "linux", "cpu": "arm" }, "sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.61.0", "", { "os": "linux", "cpu": "arm" }, "sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.61.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.61.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.61.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.61.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.61.0", "", { "os": "linux", "cpu": "none" }, "sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.61.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.61.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.61.0", "", { "os": "linux", "cpu": "x64" }, "sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.61.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.61.0", "", { "os": "none", "cpu": "arm64" }, "sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.61.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.61.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.61.0", "", { "os": "win32", "cpu": "x64" }, "sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.61.0", "", { "os": "win32", "cpu": "x64" }, "sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.10", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA=="],
|
||||
|
||||
"@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.1", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.61.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@7.1.2", "", { "dependencies": { "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.2" }, "peerDependencies": { "svelte": "^5.46.4", "vite": "^8.0.0-beta.7 || ^8.0.0" } }, "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
|
||||
|
||||
"@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.1", "", {}, "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g=="],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"devalue": ["devalue@5.8.1", "", {}, "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"esrap": ["esrap@2.2.9", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" }, "peerDependencies": { "@typescript-eslint/types": "^8.2.0" }, "optionalPeers": ["@typescript-eslint/types"] }, "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.12", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
|
||||
|
||||
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
|
||||
|
||||
"rollup": ["rollup@4.61.0", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.0", "@rollup/rollup-android-arm64": "4.61.0", "@rollup/rollup-darwin-arm64": "4.61.0", "@rollup/rollup-darwin-x64": "4.61.0", "@rollup/rollup-freebsd-arm64": "4.61.0", "@rollup/rollup-freebsd-x64": "4.61.0", "@rollup/rollup-linux-arm-gnueabihf": "4.61.0", "@rollup/rollup-linux-arm-musleabihf": "4.61.0", "@rollup/rollup-linux-arm64-gnu": "4.61.0", "@rollup/rollup-linux-arm64-musl": "4.61.0", "@rollup/rollup-linux-loong64-gnu": "4.61.0", "@rollup/rollup-linux-loong64-musl": "4.61.0", "@rollup/rollup-linux-ppc64-gnu": "4.61.0", "@rollup/rollup-linux-ppc64-musl": "4.61.0", "@rollup/rollup-linux-riscv64-gnu": "4.61.0", "@rollup/rollup-linux-riscv64-musl": "4.61.0", "@rollup/rollup-linux-s390x-gnu": "4.61.0", "@rollup/rollup-linux-x64-gnu": "4.61.0", "@rollup/rollup-linux-x64-musl": "4.61.0", "@rollup/rollup-openbsd-x64": "4.61.0", "@rollup/rollup-openharmony-arm64": "4.61.0", "@rollup/rollup-win32-arm64-msvc": "4.61.0", "@rollup/rollup-win32-ia32-msvc": "4.61.0", "@rollup/rollup-win32-x64-gnu": "4.61.0", "@rollup/rollup-win32-x64-msvc": "4.61.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="],
|
||||
|
||||
"sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"svelte": ["svelte@5.56.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.9", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-kTXr26t1bchFp28ROrb957LtbujpBmBDibmqMGziVpUs7awBi96TGgX6SovrA8BNoEUDVRK2Fb9FkeYlGspoVg=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"vite": ["vite@7.3.3", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA=="],
|
||||
|
||||
"vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="],
|
||||
|
||||
"zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="],
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -3,8 +3,17 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "Singapore bus transport app powered by OpenAPI gateway",
|
||||
"type": "commonjs",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun server.js"
|
||||
"dev": "vite dev --host --port 3000",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host --port 3000",
|
||||
"start": "vite preview --host --port 3000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^7.0.1",
|
||||
"@sveltejs/kit": "^2.61.1",
|
||||
"svelte": "^5.56.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
-281
@@ -1,281 +0,0 @@
|
||||
const fallbackEndpointOptions = [
|
||||
{ label: "Bus Arrivals", path: "/api/v1/bus-arrival", method: "GET" },
|
||||
{ label: "Bus Services", path: "/api/v1/bus-services", method: "GET" },
|
||||
{ label: "Bus Routes", path: "/api/v1/bus-routes", method: "GET" },
|
||||
{ label: "Bus Stops", path: "/api/v1/bus-stops", method: "GET" },
|
||||
{ label: "Planned Bus Routes", path: "/api/v1/planned-bus-routes", method: "GET" },
|
||||
{ label: "Passenger Volume (Bus)", path: "/api/v1/passenger-volume/bus", method: "GET" },
|
||||
{ label: "Passenger Volume (OD Bus)", path: "/api/v1/passenger-volume/od-bus", method: "GET" }
|
||||
];
|
||||
|
||||
let endpointOptions = [...fallbackEndpointOptions];
|
||||
|
||||
const endpointEl = document.querySelector("#endpoint");
|
||||
const methodEl = document.querySelector("#method");
|
||||
const paramsEl = document.querySelector("#params");
|
||||
const responseEl = document.querySelector("#response");
|
||||
const statusPillEl = document.querySelector("#status-pill");
|
||||
const arrivalBoardEl = document.querySelector("#arrival-board");
|
||||
|
||||
const addParamBtn = document.querySelector("#add-param");
|
||||
const sendRequestBtn = document.querySelector("#send-request");
|
||||
const runArrivalBtn = document.querySelector("#run-arrival");
|
||||
|
||||
const busStopCodeEl = document.querySelector("#bus-stop-code");
|
||||
const serviceNoEl = document.querySelector("#service-no");
|
||||
|
||||
function setStatus(text, state) {
|
||||
statusPillEl.textContent = text;
|
||||
statusPillEl.classList.remove("ok", "error");
|
||||
if (state) {
|
||||
statusPillEl.classList.add(state);
|
||||
}
|
||||
}
|
||||
|
||||
function addParamRow(key = "", value = "") {
|
||||
const template = document.querySelector("#param-template");
|
||||
const row = template.content.firstElementChild.cloneNode(true);
|
||||
|
||||
const keyEl = row.querySelector(".param-key");
|
||||
const valueEl = row.querySelector(".param-value");
|
||||
const removeBtn = row.querySelector(".remove-param");
|
||||
|
||||
keyEl.value = key;
|
||||
valueEl.value = value;
|
||||
|
||||
removeBtn.addEventListener("click", () => row.remove());
|
||||
paramsEl.appendChild(row);
|
||||
}
|
||||
|
||||
function getSelectedEndpoint() {
|
||||
const index = Number(endpointEl.value);
|
||||
return endpointOptions[index] || endpointOptions[0];
|
||||
}
|
||||
|
||||
function titleizePath(path) {
|
||||
return path
|
||||
.replace("/api/v1/", "")
|
||||
.split("/")
|
||||
.map((part) => part.replace(/-/g, " "))
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" - ");
|
||||
}
|
||||
|
||||
async function loadOpenApiEndpoints() {
|
||||
try {
|
||||
const response = await fetch("/openapi.json");
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spec = await response.json();
|
||||
const paths = spec && spec.paths ? spec.paths : {};
|
||||
const discovered = [];
|
||||
|
||||
Object.entries(paths).forEach(([path, methods]) => {
|
||||
if (!path.startsWith("/api/v1/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getMethod = methods && methods.get;
|
||||
if (!getMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = Array.isArray(getMethod.tags) ? getMethod.tags.join(" ").toLowerCase() : "";
|
||||
const include = tags.includes("bus") || tags.includes("passenger") || path.includes("bus");
|
||||
if (!include) {
|
||||
return;
|
||||
}
|
||||
|
||||
discovered.push({
|
||||
label: getMethod.summary || titleizePath(path),
|
||||
path,
|
||||
method: "GET"
|
||||
});
|
||||
});
|
||||
|
||||
if (discovered.length > 0) {
|
||||
endpointOptions = discovered.sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
} catch {
|
||||
// Keep fallback endpoint options when OpenAPI loading fails.
|
||||
}
|
||||
}
|
||||
|
||||
function collectParams() {
|
||||
const rows = [...paramsEl.querySelectorAll(".param-row")];
|
||||
const params = new URLSearchParams();
|
||||
|
||||
rows.forEach((row) => {
|
||||
const key = row.querySelector(".param-key").value.trim();
|
||||
const value = row.querySelector(".param-value").value.trim();
|
||||
if (key) {
|
||||
params.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function minutesUntil(isoValue) {
|
||||
if (!isoValue) {
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
const target = new Date(isoValue);
|
||||
if (Number.isNaN(target.getTime())) {
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
const deltaMs = target.getTime() - Date.now();
|
||||
const mins = Math.max(0, Math.round(deltaMs / 60000));
|
||||
return `${mins} min`;
|
||||
}
|
||||
|
||||
function renderArrivalBoard(payload) {
|
||||
arrivalBoardEl.innerHTML = "";
|
||||
const services = payload && payload.Services;
|
||||
|
||||
if (!Array.isArray(services) || services.length === 0) {
|
||||
arrivalBoardEl.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
services.slice(0, 12).forEach((svc) => {
|
||||
const card = document.createElement("article");
|
||||
card.className = "arrival-card";
|
||||
|
||||
const title = document.createElement("h4");
|
||||
title.textContent = `Service ${svc.ServiceNo || "?"}`;
|
||||
|
||||
const line1 = document.createElement("p");
|
||||
line1.className = "arrival-line";
|
||||
line1.textContent = `Next: ${minutesUntil(svc.NextBus && svc.NextBus.EstimatedArrival)}`;
|
||||
|
||||
const line2 = document.createElement("p");
|
||||
line2.className = "arrival-line";
|
||||
line2.textContent = `Following: ${minutesUntil(
|
||||
svc.NextBus2 && svc.NextBus2.EstimatedArrival
|
||||
)}`;
|
||||
|
||||
card.appendChild(title);
|
||||
card.appendChild(line1);
|
||||
card.appendChild(line2);
|
||||
arrivalBoardEl.appendChild(card);
|
||||
});
|
||||
|
||||
arrivalBoardEl.classList.remove("hidden");
|
||||
}
|
||||
|
||||
async function callEndpoint(path, params) {
|
||||
const query = params.toString();
|
||||
const url = query ? `${path}?${query}` : path;
|
||||
|
||||
setStatus("Loading", "");
|
||||
responseEl.textContent = "Loading...";
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch {
|
||||
payload = { raw: text };
|
||||
}
|
||||
|
||||
responseEl.textContent = JSON.stringify(payload, null, 2);
|
||||
if (response.ok) {
|
||||
setStatus(`OK ${response.status}`, "ok");
|
||||
} else {
|
||||
setStatus(`Error ${response.status}`, "error");
|
||||
}
|
||||
|
||||
renderArrivalBoard(payload);
|
||||
} catch (error) {
|
||||
responseEl.textContent = JSON.stringify(
|
||||
{
|
||||
error: "Request failed",
|
||||
message: error.message
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
setStatus("Network Error", "error");
|
||||
arrivalBoardEl.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function seedDefaultParams() {
|
||||
paramsEl.innerHTML = "";
|
||||
const selected = getSelectedEndpoint();
|
||||
|
||||
if (selected.path === "/api/v1/bus-arrival") {
|
||||
addParamRow("BusStopCode", "83139");
|
||||
addParamRow("ServiceNo", "");
|
||||
return;
|
||||
}
|
||||
|
||||
addParamRow("$skip", "0");
|
||||
addParamRow("$top", "10");
|
||||
}
|
||||
|
||||
function initEndpointOptions() {
|
||||
endpointEl.innerHTML = "";
|
||||
endpointOptions.forEach((item, index) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(index);
|
||||
opt.textContent = `${item.label} (${item.path})`;
|
||||
endpointEl.appendChild(opt);
|
||||
});
|
||||
|
||||
endpointEl.value = "0";
|
||||
methodEl.value = endpointOptions[0].method;
|
||||
seedDefaultParams();
|
||||
}
|
||||
|
||||
addParamBtn.addEventListener("click", () => addParamRow());
|
||||
|
||||
endpointEl.addEventListener("change", () => {
|
||||
const selected = getSelectedEndpoint();
|
||||
methodEl.value = selected.method;
|
||||
seedDefaultParams();
|
||||
});
|
||||
|
||||
sendRequestBtn.addEventListener("click", async () => {
|
||||
const selected = getSelectedEndpoint();
|
||||
const params = collectParams();
|
||||
await callEndpoint(selected.path, params);
|
||||
});
|
||||
|
||||
runArrivalBtn.addEventListener("click", async () => {
|
||||
const params = new URLSearchParams();
|
||||
const busStopCode = busStopCodeEl.value.trim();
|
||||
const serviceNo = serviceNoEl.value.trim();
|
||||
|
||||
if (!busStopCode) {
|
||||
responseEl.textContent = JSON.stringify(
|
||||
{ error: "BusStopCode is required for arrival checks." },
|
||||
null,
|
||||
2
|
||||
);
|
||||
setStatus("Missing BusStopCode", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
params.append("BusStopCode", busStopCode);
|
||||
if (serviceNo) {
|
||||
params.append("ServiceNo", serviceNo);
|
||||
}
|
||||
|
||||
await callEndpoint("/api/v1/bus-arrival", params);
|
||||
});
|
||||
|
||||
async function init() {
|
||||
await loadOpenApiEndpoints();
|
||||
initEndpointOptions();
|
||||
}
|
||||
|
||||
init();
|
||||
@@ -1,88 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Bus SG Control Deck</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Chakra+Petch:wght@600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-grid"></div>
|
||||
<main class="app-shell">
|
||||
<header class="hero">
|
||||
<p class="kicker">Singapore Transit</p>
|
||||
<h1>Bus Control Deck</h1>
|
||||
<p class="subtitle">
|
||||
Live explorer for the bus gateway API at <span id="gateway-origin">localhost:8067</span>.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="panel query-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Endpoint Explorer</h2>
|
||||
<button id="send-request" class="btn-accent">Run Request</button>
|
||||
</div>
|
||||
|
||||
<div class="grid-two">
|
||||
<label>
|
||||
Endpoint
|
||||
<select id="endpoint"></select>
|
||||
</label>
|
||||
<label>
|
||||
Request Method
|
||||
<input id="method" value="GET" readonly />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="params-head">
|
||||
<h3>Query Params</h3>
|
||||
<button id="add-param" class="btn-ghost">Add Param</button>
|
||||
</div>
|
||||
<div id="params" class="params-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel quick-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Quick Arrival Check</h2>
|
||||
<button id="run-arrival" class="btn-accent">Get Arrivals</button>
|
||||
</div>
|
||||
<div class="grid-two">
|
||||
<label>
|
||||
Bus Stop Code
|
||||
<input id="bus-stop-code" placeholder="e.g. 83139" />
|
||||
</label>
|
||||
<label>
|
||||
Service No (optional)
|
||||
<input id="service-no" placeholder="e.g. 14" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">Uses /api/v1/bus-arrival with DataMall-style query parameters.</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>Response</h2>
|
||||
<span id="status-pill" class="status-pill">Idle</span>
|
||||
</div>
|
||||
<div id="arrival-board" class="arrival-board hidden"></div>
|
||||
<pre id="response" class="response">No request yet.</pre>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<template id="param-template">
|
||||
<div class="param-row">
|
||||
<input class="param-key" placeholder="Key" />
|
||||
<input class="param-value" placeholder="Value" />
|
||||
<button class="remove-param" type="button">Remove</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,129 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const http = require("http");
|
||||
const https = require("https");
|
||||
const { URL } = require("url");
|
||||
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
const API_ORIGIN = process.env.API_ORIGIN || "http://localhost:8067";
|
||||
const PUBLIC_DIR = path.join(__dirname, "public");
|
||||
|
||||
const MIME_TYPES = {
|
||||
".html": "text/html; charset=utf-8",
|
||||
".css": "text/css; charset=utf-8",
|
||||
".js": "application/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".ico": "image/x-icon"
|
||||
};
|
||||
|
||||
function sendJson(res, statusCode, payload) {
|
||||
const body = JSON.stringify(payload);
|
||||
res.writeHead(statusCode, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Content-Length": Buffer.byteLength(body)
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function resolveFilePath(urlPathname) {
|
||||
const relative = urlPathname === "/" ? "/index.html" : urlPathname;
|
||||
const safePath = path.normalize(relative).replace(/^\.\.(\/|\\|$)/, "");
|
||||
return path.join(PUBLIC_DIR, safePath);
|
||||
}
|
||||
|
||||
function serveStatic(req, res, pathname) {
|
||||
const filePath = resolveFilePath(pathname);
|
||||
if (!filePath.startsWith(PUBLIC_DIR)) {
|
||||
sendJson(res, 400, { error: "Invalid path" });
|
||||
return;
|
||||
}
|
||||
|
||||
fs.stat(filePath, (statErr, stats) => {
|
||||
if (statErr || !stats.isFile()) {
|
||||
sendJson(res, 404, { error: "Not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
||||
|
||||
res.writeHead(200, { "Content-Type": contentType });
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
});
|
||||
}
|
||||
|
||||
function proxyApi(req, res, parsedUrl) {
|
||||
const targetUrl = new URL(parsedUrl.pathname + parsedUrl.search, API_ORIGIN);
|
||||
const isHttps = targetUrl.protocol === "https:";
|
||||
const transport = isHttps ? https : http;
|
||||
|
||||
const options = {
|
||||
protocol: targetUrl.protocol,
|
||||
hostname: targetUrl.hostname,
|
||||
port: targetUrl.port || (isHttps ? 443 : 80),
|
||||
method: req.method,
|
||||
path: targetUrl.pathname + targetUrl.search,
|
||||
headers: {
|
||||
...req.headers,
|
||||
host: targetUrl.host
|
||||
}
|
||||
};
|
||||
|
||||
const proxyReq = transport.request(options, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode || 502, {
|
||||
...proxyRes.headers,
|
||||
"Access-Control-Allow-Origin": "*"
|
||||
});
|
||||
proxyRes.pipe(res);
|
||||
});
|
||||
|
||||
proxyReq.on("error", (error) => {
|
||||
sendJson(res, 502, {
|
||||
error: "Gateway request failed",
|
||||
message: error.message
|
||||
});
|
||||
});
|
||||
|
||||
if (req.method === "GET" || req.method === "HEAD") {
|
||||
proxyReq.end();
|
||||
return;
|
||||
}
|
||||
|
||||
req.pipe(proxyReq);
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const parsedUrl = new URL(req.url || "/", `http://${req.headers.host}`);
|
||||
|
||||
if (parsedUrl.pathname.startsWith("/api/")) {
|
||||
proxyApi(req, res, parsedUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedUrl.pathname === "/openapi.json") {
|
||||
const targetUrl = new URL("/openapi.json", API_ORIGIN);
|
||||
const transport = targetUrl.protocol === "https:" ? https : http;
|
||||
const proxyReq = transport.request(targetUrl, (proxyRes) => {
|
||||
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
||||
proxyRes.pipe(res);
|
||||
});
|
||||
proxyReq.on("error", () => sendJson(res, 502, { error: "Cannot fetch OpenAPI spec" }));
|
||||
proxyReq.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "GET") {
|
||||
serveStatic(req, res, parsedUrl.pathname);
|
||||
return;
|
||||
}
|
||||
|
||||
sendJson(res, 405, { error: "Method not allowed" });
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Bus SG app is running at http://localhost:${PORT}`);
|
||||
console.log(`Proxying API requests to ${API_ORIGIN}`);
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Chakra+Petch:wght@600;700&display=swap");
|
||||
|
||||
:root {
|
||||
--bg-0: #f7f4ef;
|
||||
--bg-1: #efe6d7;
|
||||
@@ -242,6 +244,7 @@ button:hover {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -0,0 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
const API_ORIGIN = process.env.API_ORIGIN || "http://localhost:8067";
|
||||
|
||||
function buildTargetUrl(pathname, search) {
|
||||
if (pathname === "/openapi.json") {
|
||||
return new URL("/openapi.json", API_ORIGIN);
|
||||
}
|
||||
|
||||
return new URL(`${pathname}${search}`, API_ORIGIN);
|
||||
}
|
||||
|
||||
export async function handle({ event, resolve }) {
|
||||
const { pathname, search } = event.url;
|
||||
const shouldProxy = pathname.startsWith("/api/") || pathname === "/openapi.json";
|
||||
|
||||
if (!shouldProxy) {
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
const targetUrl = buildTargetUrl(pathname, search);
|
||||
|
||||
try {
|
||||
const headers = new Headers(event.request.headers);
|
||||
headers.set("host", targetUrl.host);
|
||||
|
||||
let body;
|
||||
if (event.request.method !== "GET" && event.request.method !== "HEAD") {
|
||||
body = await event.request.arrayBuffer();
|
||||
}
|
||||
|
||||
const upstream = await fetch(targetUrl, {
|
||||
method: event.request.method,
|
||||
headers,
|
||||
body
|
||||
});
|
||||
|
||||
const responseHeaders = new Headers(upstream.headers);
|
||||
if (pathname.startsWith("/api/")) {
|
||||
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
||||
}
|
||||
|
||||
return new Response(upstream.body, {
|
||||
status: upstream.status,
|
||||
statusText: upstream.statusText,
|
||||
headers: responseHeaders
|
||||
});
|
||||
} catch (error) {
|
||||
return Response.json(
|
||||
{
|
||||
error: pathname === "/openapi.json" ? "Cannot fetch OpenAPI spec" : "Gateway request failed",
|
||||
message: error?.message || "Unknown error"
|
||||
},
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
import "../app.css";
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
@@ -0,0 +1,328 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
|
||||
const fallbackEndpointOptions = [
|
||||
{ label: "Bus Arrivals", path: "/api/v1/bus-arrival", method: "GET" },
|
||||
{ label: "Bus Services", path: "/api/v1/bus-services", method: "GET" },
|
||||
{ label: "Bus Routes", path: "/api/v1/bus-routes", method: "GET" },
|
||||
{ label: "Bus Stops", path: "/api/v1/bus-stops", method: "GET" },
|
||||
{ label: "Planned Bus Routes", path: "/api/v1/planned-bus-routes", method: "GET" },
|
||||
{ label: "Passenger Volume (Bus)", path: "/api/v1/passenger-volume/bus", method: "GET" },
|
||||
{
|
||||
label: "Passenger Volume (OD Bus)",
|
||||
path: "/api/v1/passenger-volume/od-bus",
|
||||
method: "GET"
|
||||
}
|
||||
];
|
||||
|
||||
let endpointOptions = [...fallbackEndpointOptions];
|
||||
let selectedEndpointIndex = 0;
|
||||
let method = "GET";
|
||||
let params = [];
|
||||
let responseText = "No request yet.";
|
||||
let statusText = "Idle";
|
||||
let statusState = "";
|
||||
let services = [];
|
||||
|
||||
let busStopCode = "83139";
|
||||
let serviceNo = "";
|
||||
|
||||
function getSelectedEndpoint() {
|
||||
return endpointOptions[selectedEndpointIndex] || endpointOptions[0];
|
||||
}
|
||||
|
||||
function setStatus(text, state = "") {
|
||||
statusText = text;
|
||||
statusState = state;
|
||||
}
|
||||
|
||||
function titleizePath(path) {
|
||||
return path
|
||||
.replace("/api/v1/", "")
|
||||
.split("/")
|
||||
.map((part) => part.replace(/-/g, " "))
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" - ");
|
||||
}
|
||||
|
||||
async function loadOpenApiEndpoints() {
|
||||
try {
|
||||
const response = await fetch("/openapi.json");
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const spec = await response.json();
|
||||
const paths = spec && spec.paths ? spec.paths : {};
|
||||
const discovered = [];
|
||||
|
||||
Object.entries(paths).forEach(([path, methods]) => {
|
||||
if (!path.startsWith("/api/v1/")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getMethod = methods && methods.get;
|
||||
if (!getMethod) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = Array.isArray(getMethod.tags) ? getMethod.tags.join(" ").toLowerCase() : "";
|
||||
const include = tags.includes("bus") || tags.includes("passenger") || path.includes("bus");
|
||||
if (!include) {
|
||||
return;
|
||||
}
|
||||
|
||||
discovered.push({
|
||||
label: getMethod.summary || titleizePath(path),
|
||||
path,
|
||||
method: "GET"
|
||||
});
|
||||
});
|
||||
|
||||
if (discovered.length > 0) {
|
||||
endpointOptions = discovered.sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
} catch {
|
||||
// Keep fallback endpoint options when OpenAPI loading fails.
|
||||
}
|
||||
}
|
||||
|
||||
function seedDefaultParams() {
|
||||
const selected = getSelectedEndpoint();
|
||||
|
||||
if (selected.path === "/api/v1/bus-arrival") {
|
||||
params = [
|
||||
{ key: "BusStopCode", value: "83139" },
|
||||
{ key: "ServiceNo", value: "" }
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
params = [
|
||||
{ key: "$skip", value: "0" },
|
||||
{ key: "$top", value: "10" }
|
||||
];
|
||||
}
|
||||
|
||||
function addParamRow() {
|
||||
params = [...params, { key: "", value: "" }];
|
||||
}
|
||||
|
||||
function removeParamRow(index) {
|
||||
params = params.filter((_, i) => i !== index);
|
||||
}
|
||||
|
||||
function updateParam(index, field, value) {
|
||||
params = params.map((param, i) => {
|
||||
if (i !== index) {
|
||||
return param;
|
||||
}
|
||||
|
||||
return { ...param, [field]: value };
|
||||
});
|
||||
}
|
||||
|
||||
function collectParams() {
|
||||
const search = new URLSearchParams();
|
||||
|
||||
params.forEach((param) => {
|
||||
const key = param.key.trim();
|
||||
const value = param.value.trim();
|
||||
if (key) {
|
||||
search.append(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
function minutesUntil(isoValue) {
|
||||
if (!isoValue) {
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
const target = new Date(isoValue);
|
||||
if (Number.isNaN(target.getTime())) {
|
||||
return "n/a";
|
||||
}
|
||||
|
||||
const deltaMs = target.getTime() - Date.now();
|
||||
const mins = Math.max(0, Math.round(deltaMs / 60000));
|
||||
return `${mins} min`;
|
||||
}
|
||||
|
||||
async function callEndpoint(path, queryParams) {
|
||||
const query = queryParams.toString();
|
||||
const url = query ? `${path}?${query}` : path;
|
||||
|
||||
setStatus("Loading");
|
||||
responseText = "Loading...";
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch {
|
||||
payload = { raw: text };
|
||||
}
|
||||
|
||||
responseText = JSON.stringify(payload, null, 2);
|
||||
if (response.ok) {
|
||||
setStatus(`OK ${response.status}`, "ok");
|
||||
} else {
|
||||
setStatus(`Error ${response.status}`, "error");
|
||||
}
|
||||
|
||||
services = Array.isArray(payload?.Services) ? payload.Services.slice(0, 12) : [];
|
||||
} catch (error) {
|
||||
responseText = JSON.stringify(
|
||||
{
|
||||
error: "Request failed",
|
||||
message: error?.message || "Unknown error"
|
||||
},
|
||||
null,
|
||||
2
|
||||
);
|
||||
setStatus("Network Error", "error");
|
||||
services = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function runRequest() {
|
||||
const selected = getSelectedEndpoint();
|
||||
await callEndpoint(selected.path, collectParams());
|
||||
}
|
||||
|
||||
async function runArrival() {
|
||||
if (!busStopCode.trim()) {
|
||||
responseText = JSON.stringify({ error: "BusStopCode is required for arrival checks." }, null, 2);
|
||||
setStatus("Missing BusStopCode", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const search = new URLSearchParams();
|
||||
search.append("BusStopCode", busStopCode.trim());
|
||||
if (serviceNo.trim()) {
|
||||
search.append("ServiceNo", serviceNo.trim());
|
||||
}
|
||||
|
||||
await callEndpoint("/api/v1/bus-arrival", search);
|
||||
}
|
||||
|
||||
function onEndpointChange() {
|
||||
method = getSelectedEndpoint().method;
|
||||
seedDefaultParams();
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await loadOpenApiEndpoints();
|
||||
selectedEndpointIndex = 0;
|
||||
method = getSelectedEndpoint().method;
|
||||
seedDefaultParams();
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Bus SG Control Deck</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="bg-grid"></div>
|
||||
<main class="app-shell">
|
||||
<header class="hero">
|
||||
<p class="kicker">Singapore Transit</p>
|
||||
<h1>Bus Control Deck</h1>
|
||||
<p class="subtitle">
|
||||
Live explorer for the bus gateway API at
|
||||
<span>localhost:8067</span>.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<section class="panel query-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Endpoint Explorer</h2>
|
||||
<button class="btn-accent" type="button" on:click={runRequest}>Run Request</button>
|
||||
</div>
|
||||
|
||||
<div class="grid-two">
|
||||
<label>
|
||||
Endpoint
|
||||
<select bind:value={selectedEndpointIndex} on:change={onEndpointChange}>
|
||||
{#each endpointOptions as item, index}
|
||||
<option value={index}>{item.label} ({item.path})</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Request Method
|
||||
<input value={method} readonly />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="params-head">
|
||||
<h3>Query Params</h3>
|
||||
<button class="btn-ghost" type="button" on:click={addParamRow}>Add Param</button>
|
||||
</div>
|
||||
|
||||
<div class="params-list">
|
||||
{#each params as param, index}
|
||||
<div class="param-row">
|
||||
<input
|
||||
class="param-key"
|
||||
placeholder="Key"
|
||||
value={param.key}
|
||||
on:input={(event) => updateParam(index, "key", event.currentTarget.value)}
|
||||
/>
|
||||
<input
|
||||
class="param-value"
|
||||
placeholder="Value"
|
||||
value={param.value}
|
||||
on:input={(event) => updateParam(index, "value", event.currentTarget.value)}
|
||||
/>
|
||||
<button class="remove-param" type="button" on:click={() => removeParamRow(index)}>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel quick-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Quick Arrival Check</h2>
|
||||
<button class="btn-accent" type="button" on:click={runArrival}>Get Arrivals</button>
|
||||
</div>
|
||||
<div class="grid-two">
|
||||
<label>
|
||||
Bus Stop Code
|
||||
<input bind:value={busStopCode} placeholder="e.g. 83139" />
|
||||
</label>
|
||||
<label>
|
||||
Service No (optional)
|
||||
<input bind:value={serviceNo} placeholder="e.g. 14" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="hint">Uses /api/v1/bus-arrival with DataMall-style query parameters.</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>Response</h2>
|
||||
<span class={`status-pill ${statusState}`}>{statusText}</span>
|
||||
</div>
|
||||
|
||||
<div class={`arrival-board ${services.length > 0 ? "" : "hidden"}`}>
|
||||
{#each services as service}
|
||||
<article class="arrival-card">
|
||||
<h4>Service {service.ServiceNo || "?"}</h4>
|
||||
<p class="arrival-line">Next: {minutesUntil(service?.NextBus?.EstimatedArrival)}</p>
|
||||
<p class="arrival-line">Following: {minutesUntil(service?.NextBus2?.EstimatedArrival)}</p>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<pre class="response">{responseText}</pre>
|
||||
</section>
|
||||
</main>
|
||||
@@ -0,0 +1,10 @@
|
||||
import adapter from "@sveltejs/adapter-auto";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
Reference in New Issue
Block a user