diff --git a/dist/index.html b/dist/index.html index 485fd63..cb12139 100644 --- a/dist/index.html +++ b/dist/index.html @@ -8208,6 +8208,42 @@ function AddLocationDialog($$anchor, $$props) { } delegate(["click", "input"]); //#endregion +//#region src/lib/router.js +/** +* Lightweight hash router for the app's tab views. +* +* Tabs are routable via the URL hash so a user can share/copy a URL for a +* specific tab (e.g. #/radar) and use browser back/forward between tabs. +* There is no nested route structure — just a single top-level view segment. +*/ +var VALID_VIEWS = [ + "current", + "hourly", + "daily", + "detail", + "radar" +]; +var DEFAULT_VIEW = "current"; +/** +* Parse a location hash into a view id, falling back to the default view. +* Accepts '#/hourly', 'hourly', '#hourly', '', '#/unknown', etc. +* +* @param {string} [hash] - window.location.hash +* @returns {string} one of VALID_VIEWS +*/ +function parseHash(hash = "") { + const seg = String(hash).replace(/^#\/?/, "").split("/")[0]; + return VALID_VIEWS.includes(seg) ? seg : DEFAULT_VIEW; +} +/** +* Convert a view id into its URL hash representation. +* @param {string} view - one of VALID_VIEWS +* @returns {string} e.g. '#/radar' +*/ +function viewToHash(view) { + return `#/${view}`; +} +//#endregion //#region src/App.svelte var root = /* @__PURE__ */ from_html(``); var root_1 = /* @__PURE__ */ from_html(`📍 Current`); @@ -8219,7 +8255,21 @@ var root_6 = /* @__PURE__ */ from_html(`
< var root_7 = /* @__PURE__ */ from_html(`

`); function App($$anchor, $$props) { push($$props, true); - let viewMode = /* @__PURE__ */ state("current"); + let viewMode = /* @__PURE__ */ state(proxy(parseHash(typeof window !== "undefined" ? window.location.hash : ""))); + /** Switch the active tab and reflect it in the URL hash. */ + function setView(v) { + set(viewMode, v, true); + if (typeof window !== "undefined") window.location.hash = viewToHash(v); + } + user_effect(() => { + if (typeof window === "undefined") return; + const onHash = () => { + const v = parseHash(window.location.hash); + if (v !== get(viewMode)) set(viewMode, v, true); + }; + window.addEventListener("hashchange", onHash); + return () => window.removeEventListener("hashchange", onHash); + }); let initDone = /* @__PURE__ */ state(false); let geoError = /* @__PURE__ */ state(""); user_effect(() => { @@ -8449,16 +8499,16 @@ function App($$anchor, $$props) { classes_9 = set_class(button_13, 1, "tab-btn svelte-1n46o8q", null, classes_9, { active: get(viewMode) === "detail" }); classes_10 = set_class(button_14, 1, "tab-btn svelte-1n46o8q", null, classes_10, { active: get(viewMode) === "radar" }); }); - delegated("click", button_5, () => set(viewMode, "current")); - delegated("click", button_6, () => set(viewMode, "hourly")); - delegated("click", button_7, () => set(viewMode, "daily")); - delegated("click", button_8, () => set(viewMode, "detail")); - delegated("click", button_9, () => set(viewMode, "radar")); - delegated("click", button_10, () => set(viewMode, "current")); - delegated("click", button_11, () => set(viewMode, "hourly")); - delegated("click", button_12, () => set(viewMode, "daily")); - delegated("click", button_13, () => set(viewMode, "detail")); - delegated("click", button_14, () => set(viewMode, "radar")); + delegated("click", button_5, () => setView("current")); + delegated("click", button_6, () => setView("hourly")); + delegated("click", button_7, () => setView("daily")); + delegated("click", button_8, () => setView("detail")); + delegated("click", button_9, () => setView("radar")); + delegated("click", button_10, () => setView("current")); + delegated("click", button_11, () => setView("hourly")); + delegated("click", button_12, () => setView("daily")); + delegated("click", button_13, () => setView("detail")); + delegated("click", button_14, () => setView("radar")); append($$anchor, fragment); }; if_block(node_8, ($$render) => { diff --git a/src/App.svelte b/src/App.svelte index e25e7b7..a7dbfb0 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -10,8 +10,32 @@ import NotificationBell from './components/NotificationBell.svelte' import SettingsDialog from './components/SettingsDialog.svelte' import AddLocationDialog from './components/AddLocationDialog.svelte' + import { parseHash, viewToHash } from './lib/router.js' + + // Hash-routable tab: initialize from the URL hash so a shared link like + // #/radar opens straight to that tab. + let viewMode = $state(parseHash(typeof window !== 'undefined' ? window.location.hash : '')) + + /** Switch the active tab and reflect it in the URL hash. */ + function setView(v) { + viewMode = v + if (typeof window !== 'undefined') { + window.location.hash = viewToHash(v) + } + } + + // Keep viewMode in sync with the hash — covers browser back/forward and a + // manually edited URL. Guarded so we never re-enter on the same value. + $effect(() => { + if (typeof window === 'undefined') return + const onHash = () => { + const v = parseHash(window.location.hash) + if (v !== viewMode) viewMode = v + } + window.addEventListener('hashchange', onHash) + return () => window.removeEventListener('hashchange', onHash) + }) - let viewMode = $state('current') // 'current' | 'hourly' | 'daily' | 'detail' | 'radar' let initDone = $state(false) let geoError = $state('') @@ -186,37 +210,37 @@
- - - - - + + + + +
diff --git a/src/lib/router.js b/src/lib/router.js new file mode 100644 index 0000000..4191ccd --- /dev/null +++ b/src/lib/router.js @@ -0,0 +1,32 @@ +/** + * Lightweight hash router for the app's tab views. + * + * Tabs are routable via the URL hash so a user can share/copy a URL for a + * specific tab (e.g. #/radar) and use browser back/forward between tabs. + * There is no nested route structure — just a single top-level view segment. + */ + +export const VALID_VIEWS = ['current', 'hourly', 'daily', 'detail', 'radar'] +export const DEFAULT_VIEW = 'current' + +/** + * Parse a location hash into a view id, falling back to the default view. + * Accepts '#/hourly', 'hourly', '#hourly', '', '#/unknown', etc. + * + * @param {string} [hash] - window.location.hash + * @returns {string} one of VALID_VIEWS + */ +export function parseHash(hash = '') { + const clean = String(hash).replace(/^#\/?/, '') + const seg = clean.split('/')[0] + return VALID_VIEWS.includes(seg) ? seg : DEFAULT_VIEW +} + +/** + * Convert a view id into its URL hash representation. + * @param {string} view - one of VALID_VIEWS + * @returns {string} e.g. '#/radar' + */ +export function viewToHash(view) { + return `#/${view}` +} diff --git a/tests/lib/router.test.js b/tests/lib/router.test.js new file mode 100644 index 0000000..3a6c2aa --- /dev/null +++ b/tests/lib/router.test.js @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest' +import { parseHash, viewToHash, VALID_VIEWS, DEFAULT_VIEW } from '../../src/lib/router.js' + +describe('parseHash', () => { + it('parses a standard hash route', () => { + expect(parseHash('#/hourly')).toBe('hourly') + expect(parseHash('#/radar')).toBe('radar') + }) + + it('handles a bare segment without a leading slash', () => { + expect(parseHash('daily')).toBe('daily') + expect(parseHash('#daily')).toBe('daily') + }) + + it('falls back to the default view for empty or unknown hashes', () => { + expect(parseHash('')).toBe(DEFAULT_VIEW) + expect(parseHash('#')).toBe(DEFAULT_VIEW) + expect(parseHash('#/nope')).toBe(DEFAULT_VIEW) + expect(parseHash('#/current')).toBe('current') + }) + + it('ignores a trailing path after the view segment', () => { + expect(parseHash('#/detail/sub')).toBe('detail') + }) +}) + +describe('viewToHash', () => { + it('round-trips a valid view', () => { + for (const v of VALID_VIEWS) { + expect(viewToHash(v)).toBe(`#/${v}`) + expect(parseHash(viewToHash(v))).toBe(v) + } + }) +})