@empoweredvote/ev-ui 0.6.1 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -2890,6 +2890,94 @@ var evContext = {
2890
2890
  async clear() {
2891
2891
  return send({ type: "ev-context:clear" });
2892
2892
  },
2893
+ /**
2894
+ * Read the userId-stamped authed slice for the given user.
2895
+ *
2896
+ * Stored shape:
2897
+ * { compass?, address?, verdicts?, authed?: { userId, compass?, address?, verdicts? } }
2898
+ *
2899
+ * - Returns null if there is no stored value, no `authed` body, or the stored
2900
+ * `authed.userId` does not match the requested `userId` (mismatch = inert).
2901
+ * - Otherwise returns `{ compass, address, verdicts }` with undefined keys
2902
+ * omitted.
2903
+ *
2904
+ * Used by logged-in consumers to do SWR-style hydration: render this slice
2905
+ * synchronously on mount, then replace silently when the API responds.
2906
+ */
2907
+ async getAuthedSlice(userId) {
2908
+ if (!userId) return null;
2909
+ const current = await this.get();
2910
+ if (!current || !current.authed || typeof current.authed !== "object") return null;
2911
+ if (current.authed.userId !== userId) return null;
2912
+ const out = {};
2913
+ if (current.authed.compass !== void 0) out.compass = current.authed.compass;
2914
+ if (current.authed.address !== void 0) out.address = current.authed.address;
2915
+ if (current.authed.verdicts !== void 0) out.verdicts = current.authed.verdicts;
2916
+ if (current.authed.promotionDismissed !== void 0) {
2917
+ out.promotionDismissed = current.authed.promotionDismissed;
2918
+ }
2919
+ return out;
2920
+ },
2921
+ /**
2922
+ * Mirror an authed write into the userId-stamped `authed` slice.
2923
+ *
2924
+ * - `patch` may contain any subset of `{ compass, address, verdicts }`.
2925
+ * Unrecognized keys are dropped.
2926
+ * - If the existing `authed.userId` matches `userId`, the patch is merged
2927
+ * with the prior authed body (per-domain shallow merge — the patch's
2928
+ * compass/address/verdicts replace the prior values).
2929
+ * - If it does not match (user switch), the prior authed body is stomped.
2930
+ * - Guest top-level keys (compass, address, verdicts at the root) are
2931
+ * preserved untouched.
2932
+ *
2933
+ * Returns false on falsy `userId` or empty patch; otherwise returns the
2934
+ * underlying `set()` result.
2935
+ */
2936
+ async setAuthedSlice(userId, patch) {
2937
+ if (!userId) return false;
2938
+ if (!patch || typeof patch !== "object") return false;
2939
+ const allowed = {};
2940
+ if (patch.compass !== void 0) allowed.compass = patch.compass;
2941
+ if (patch.address !== void 0) allowed.address = patch.address;
2942
+ if (patch.verdicts !== void 0) allowed.verdicts = patch.verdicts;
2943
+ if (patch.promotionDismissed !== void 0) {
2944
+ allowed.promotionDismissed = patch.promotionDismissed;
2945
+ }
2946
+ if (Object.keys(allowed).length === 0) return false;
2947
+ const current = await this.get() || {};
2948
+ const priorAuthed = current.authed && current.authed.userId === userId ? current.authed : null;
2949
+ const nextAuthed = { userId };
2950
+ if (priorAuthed) {
2951
+ if (priorAuthed.compass !== void 0) nextAuthed.compass = priorAuthed.compass;
2952
+ if (priorAuthed.address !== void 0) nextAuthed.address = priorAuthed.address;
2953
+ if (priorAuthed.verdicts !== void 0) nextAuthed.verdicts = priorAuthed.verdicts;
2954
+ if (priorAuthed.promotionDismissed !== void 0) {
2955
+ nextAuthed.promotionDismissed = priorAuthed.promotionDismissed;
2956
+ }
2957
+ }
2958
+ if (allowed.compass !== void 0) nextAuthed.compass = allowed.compass;
2959
+ if (allowed.address !== void 0) nextAuthed.address = allowed.address;
2960
+ if (allowed.verdicts !== void 0) nextAuthed.verdicts = allowed.verdicts;
2961
+ if (allowed.promotionDismissed !== void 0 && allowed.promotionDismissed !== null && typeof allowed.promotionDismissed === "object") {
2962
+ const prior = priorAuthed && priorAuthed.promotionDismissed && typeof priorAuthed.promotionDismissed === "object" ? priorAuthed.promotionDismissed : {};
2963
+ nextAuthed.promotionDismissed = { ...prior, ...allowed.promotionDismissed };
2964
+ }
2965
+ return this.set({ ...current, authed: nextAuthed });
2966
+ },
2967
+ /**
2968
+ * Remove the `authed` slice entirely (preserving guest top-level keys).
2969
+ *
2970
+ * Provided for completeness — consumers in the v2026.4.5 wiring plan must
2971
+ * NOT call this on logout. The slice is intended to go inert via userId
2972
+ * mismatch, so a re-login as the same user can rehydrate from cache.
2973
+ */
2974
+ async clearAuthedSlice() {
2975
+ const current = await this.get();
2976
+ if (!current || typeof current !== "object") return true;
2977
+ if (!("authed" in current)) return true;
2978
+ const { authed: _omit, ...rest } = current;
2979
+ return this.set(rest);
2980
+ },
2893
2981
  /**
2894
2982
  * Subscribe to live updates. Fires when this tab or any other tab
2895
2983
  * (any subdomain) writes to the store. Returns an unsubscribe function.
@@ -5642,8 +5730,167 @@ function computeTierCoverage(topics) {
5642
5730
  return counts;
5643
5731
  }
5644
5732
 
5733
+ // src/useEvContextPromotion.js
5734
+ import { useEffect as useEffect9, useState as useState15, useCallback, useRef as useRef3 } from "react";
5735
+ function useEvContextPromotion({
5736
+ domain,
5737
+ isLoggedIn,
5738
+ userId,
5739
+ apiData,
5740
+ apiWriter,
5741
+ enabled = true
5742
+ }) {
5743
+ const [shouldPrompt, setShouldPrompt] = useState15(false);
5744
+ const [payload, setPayload] = useState15(null);
5745
+ const [status, setStatus] = useState15("idle");
5746
+ const [error, setError] = useState15(null);
5747
+ const apiDataRef = useRef3(apiData);
5748
+ apiDataRef.current = apiData;
5749
+ const userIdRef = useRef3(userId);
5750
+ userIdRef.current = userId;
5751
+ const isLoggedInRef = useRef3(isLoggedIn);
5752
+ isLoggedInRef.current = isLoggedIn;
5753
+ const enabledRef = useRef3(enabled);
5754
+ enabledRef.current = enabled;
5755
+ const domainRef = useRef3(domain);
5756
+ domainRef.current = domain;
5757
+ const apiWriterRef = useRef3(apiWriter);
5758
+ apiWriterRef.current = apiWriter;
5759
+ const detect = useCallback(async () => {
5760
+ const _enabled = enabledRef.current;
5761
+ const _isLoggedIn = isLoggedInRef.current;
5762
+ const _userId = userIdRef.current;
5763
+ const _apiData = apiDataRef.current;
5764
+ const _domain = domainRef.current;
5765
+ if (!_enabled || !_isLoggedIn || !_userId) {
5766
+ setShouldPrompt(false);
5767
+ setPayload(null);
5768
+ return;
5769
+ }
5770
+ if (!isApiEmpty(_domain, _apiData)) {
5771
+ setShouldPrompt(false);
5772
+ setPayload(null);
5773
+ return;
5774
+ }
5775
+ try {
5776
+ const slice = await evContext.getAuthedSlice(_userId);
5777
+ if (slice && slice.promotionDismissed && slice.promotionDismissed[_domain] === true) {
5778
+ setShouldPrompt(false);
5779
+ setPayload(null);
5780
+ return;
5781
+ }
5782
+ } catch {
5783
+ }
5784
+ let guest = null;
5785
+ try {
5786
+ guest = await evContext.get();
5787
+ } catch {
5788
+ guest = null;
5789
+ }
5790
+ if (isGuestPopulated(_domain, guest)) {
5791
+ setShouldPrompt(true);
5792
+ setPayload(guest[_domain]);
5793
+ } else {
5794
+ setShouldPrompt(false);
5795
+ setPayload(null);
5796
+ }
5797
+ }, []);
5798
+ useEffect9(() => {
5799
+ detect();
5800
+ }, [detect, isLoggedIn, userId, apiData, enabled, domain]);
5801
+ useEffect9(() => {
5802
+ const unsub = evContext.subscribe(() => {
5803
+ detect();
5804
+ });
5805
+ return unsub;
5806
+ }, [detect]);
5807
+ const promote = useCallback(async () => {
5808
+ const _userId = userIdRef.current;
5809
+ const _domain = domainRef.current;
5810
+ const _writer = apiWriterRef.current;
5811
+ const _payload = payload;
5812
+ if (!_userId || !_payload || typeof _writer !== "function") return;
5813
+ setStatus("saving");
5814
+ setError(null);
5815
+ try {
5816
+ await _writer(_payload);
5817
+ try {
5818
+ await evContext.setAuthedSlice(_userId, { [_domain]: _payload });
5819
+ } catch {
5820
+ }
5821
+ try {
5822
+ await evContext.setAuthedSlice(_userId, {
5823
+ promotionDismissed: { [_domain]: false }
5824
+ });
5825
+ } catch {
5826
+ }
5827
+ setStatus("saved");
5828
+ setShouldPrompt(false);
5829
+ } catch (err) {
5830
+ setStatus("error");
5831
+ setError(err instanceof Error ? err : new Error(String(err)));
5832
+ }
5833
+ }, [payload]);
5834
+ const dismiss = useCallback(async () => {
5835
+ const _userId = userIdRef.current;
5836
+ const _domain = domainRef.current;
5837
+ if (!_userId) return;
5838
+ try {
5839
+ await evContext.setAuthedSlice(_userId, {
5840
+ promotionDismissed: { [_domain]: true }
5841
+ });
5842
+ } catch {
5843
+ }
5844
+ setShouldPrompt(false);
5845
+ }, []);
5846
+ return { shouldPrompt, payload, promote, dismiss, status, error };
5847
+ }
5848
+ function isApiEmpty(domain, apiData) {
5849
+ if (apiData === null || apiData === void 0) return true;
5850
+ if (domain === "compass") {
5851
+ if (typeof apiData !== "object") return false;
5852
+ if (Array.isArray(apiData)) return apiData.length === 0;
5853
+ if (apiData.answers && typeof apiData.answers === "object") {
5854
+ return Object.keys(apiData.answers).length === 0;
5855
+ }
5856
+ return Object.keys(apiData).length === 0;
5857
+ }
5858
+ if (domain === "address") {
5859
+ if (typeof apiData === "string") return apiData.trim().length === 0;
5860
+ if (typeof apiData !== "object") return false;
5861
+ const a = apiData;
5862
+ const hasAddr = typeof a.formatted === "string" && a.formatted.length > 0;
5863
+ const hasAddrAlt = typeof a.addr === "string" && a.addr.length > 0;
5864
+ const hasLat = typeof a.lat === "number" || typeof a.latitude === "number";
5865
+ return !(hasAddr || hasAddrAlt || hasLat);
5866
+ }
5867
+ if (domain === "verdicts") {
5868
+ if (typeof apiData !== "object") return false;
5869
+ if (Array.isArray(apiData)) return apiData.length === 0;
5870
+ return Object.keys(apiData).length === 0;
5871
+ }
5872
+ return false;
5873
+ }
5874
+ function isGuestPopulated(domain, fullEvContext) {
5875
+ if (!fullEvContext || typeof fullEvContext !== "object") return false;
5876
+ const slice = fullEvContext[domain];
5877
+ if (!slice || typeof slice !== "object") return false;
5878
+ if (domain === "compass") {
5879
+ const ans = slice.answers || slice.a;
5880
+ if (ans && typeof ans === "object") return Object.keys(ans).length > 0;
5881
+ return false;
5882
+ }
5883
+ if (domain === "address") {
5884
+ return typeof slice.formatted === "string" && slice.formatted.length > 0 || typeof slice.addr === "string" && slice.addr.length > 0;
5885
+ }
5886
+ if (domain === "verdicts") {
5887
+ return Object.keys(slice).length > 0;
5888
+ }
5889
+ return false;
5890
+ }
5891
+
5645
5892
  // src/StanceAccordion.jsx
5646
- import React29, { useState as useState15, useRef as useRef3, useCallback, useEffect as useEffect9 } from "react";
5893
+ import React29, { useState as useState16, useRef as useRef4, useCallback as useCallback2, useEffect as useEffect10 } from "react";
5647
5894
 
5648
5895
  // src/Favicon.jsx
5649
5896
  import React28 from "react";
@@ -5704,11 +5951,11 @@ function StanceAccordion({
5704
5951
  apiUrl = "https://api.empowered.vote"
5705
5952
  }) {
5706
5953
  var _a;
5707
- const [expandedTopicId, setExpandedTopicId] = useState15(null);
5708
- const [loadingId, setLoadingId] = useState15(null);
5709
- const [showAll, setShowAll] = useState15(false);
5710
- const contextCache = useRef3(/* @__PURE__ */ new Map());
5711
- const quotesCache = useRef3(null);
5954
+ const [expandedTopicId, setExpandedTopicId] = useState16(null);
5955
+ const [loadingId, setLoadingId] = useState16(null);
5956
+ const [showAll, setShowAll] = useState16(false);
5957
+ const contextCache = useRef4(/* @__PURE__ */ new Map());
5958
+ const quotesCache = useRef4(null);
5712
5959
  const polAnswerMap = {};
5713
5960
  if (polAnswers) {
5714
5961
  polAnswers.forEach((a) => {
@@ -5767,7 +6014,7 @@ function StanceAccordion({
5767
6014
  quotesCache.current = [];
5768
6015
  }
5769
6016
  }
5770
- const handleToggle = useCallback(
6017
+ const handleToggle = useCallback2(
5771
6018
  async (topicId) => {
5772
6019
  if (expandedTopicId === topicId) {
5773
6020
  setExpandedTopicId(null);
@@ -5784,7 +6031,7 @@ function StanceAccordion({
5784
6031
  },
5785
6032
  [expandedTopicId, politicianId, apiUrl]
5786
6033
  );
5787
- useEffect9(() => {
6034
+ useEffect10(() => {
5788
6035
  if (initialExpandedTopicId && topics && topics.length > 0) {
5789
6036
  handleToggle(String(initialExpandedTopicId));
5790
6037
  }
@@ -6061,6 +6308,7 @@ export {
6061
6308
  spacing,
6062
6309
  textStyles,
6063
6310
  tierColors,
6311
+ useEvContextPromotion,
6064
6312
  useMediaQuery,
6065
6313
  zIndex
6066
6314
  };