summaryrefslogtreecommitdiff
path: root/assets/js/iine.js
blob: a41153fa0b23e0fef8de3bae23ac37b0162730d6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
(() => {
  const SUPABASE_URL = 'https://vhiweeypifbwacashxjz.supabase.co';
  const SUPABASE_KEY = 'sb_publishable_EoB7MFJhCmb6PiAk-GPJ4w_PGhQ44Ru';
  const DEFAULT_ICON = 'organ';
  const FALLBACK_LABEL = 'Send your appreciation';
  const DEFAULT_LABELS = {
      organ: 'Offer up your heart.'
  };

  const BUILTIN_ICONS = {
      organ: '<svg class="dark:invert" width="16px" height="16px" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path fill="#000000" d="M289.188 22.72c-6.5.162-12.792 2.26-17.594 7.124-.483.488-.916.983-1.344 1.5l-.188-.188c-17.848 22.67-35.71 51.21-45.718 83.094-11.197 35.67-12.8 74.75 5.687 114.78l-16.968 7.845c-14.305-30.974-17.807-61.916-14.53-90.97-8.475 4.99-16.412 11.178-23.688 18.314-22.368-29.2-49.978-56.593-76.5-76-1.404-1.4-2.996-2.655-4.813-3.69-3.378-1.922-6.98-2.86-10.624-3.03-.38-.018-.745-.032-1.125-.03-.38 0-.774.016-1.155.03-5.33.203-10.67 1.93-15.438 4.563-8.72 4.816-16.545 12.77-22.5 22.937-5.954 10.168-9.036 20.86-8.906 30.78.132 9.922 3.896 20.06 12.907 25.19.368.208.753.375 1.126.56l-.126.25c27.298 16.83 63.364 39.376 86.626 67.626-6.582 22.99-7.837 46.735-2.625 67.406 15.303 60.707 62.425 115.8 113.03 150.657 25.304 17.427 51.403 29.785 74.22 35.5 22.816 5.713 41.874 4.546 53.906-2.658 27.152-16.25 45.328-45.636 56.312-80.53-6.824 4.01-14.074 7.32-21.625 9.874-35.777 12.102-78.105 8.732-113.624-10.062-35.52-18.794-64.105-53.724-70.53-103.22l18.53-2.405c5.673 43.69 29.907 72.772 60.75 89.092 30.844 16.32 68.41 19.18 98.875 8.875 30.465-10.304 53.67-32.64 59.033-68.906 4.434-29.985-3.865-70.527-33.844-121.374-19.14 2.18-38.67 6.368-56.626 11.563l-5.188-17.94c12.73-3.682 26.192-6.87 39.875-9.28 2.63.22 5.328.024 8.033-.72 1.02-.28 1.996-.63 2.937-1.03.032-.005.062-.027.094-.03-.005-.008.004-.025 0-.032 8.222-3.55 13.437-11.472 15.906-20.032 2.76-9.57 2.698-20.724-.28-32.125-2.98-11.4-8.362-21.14-15.47-28.06-5.33-5.193-12.214-9.005-19.53-9.44-2.236-.13-4.497.065-6.783.626l-.093-.406c-16.036 5.84-33.733 13.757-51 24.125-8.947 11.378-15.964 22.483-20.5 33.375-7.047 16.92-8.512 33.12-2.438 51.438l-17.72 5.906c-7.46-22.5-5.505-44.333 2.908-64.53 7.425-17.828 19.73-34.594 34.656-51.376l-.156-.157c.622-.507 1.232-1.036 1.812-1.624 7.286-7.377 8.24-18.11 5.844-27.78-2.395-9.67-8.032-19.274-16.313-27.657l-.062-.032c-8.27-8.358-17.777-14.095-27.375-16.532-2.404-.61-4.868-.992-7.344-1.125-.464-.024-.942-.055-1.406-.06-.435-.007-.88-.012-1.313 0zm.656 18.624c.36 0 .755 0 1.156.03 1.07.084 2.28.308 3.656.657 5.51 1.4 12.564 5.332 18.72 11.564 6.154 6.23 10.07 13.385 11.468 19.03 1.398 5.647.36 8.778-1 10.157-1.362 1.38-4.27 2.37-9.78.97-5.512-1.4-12.565-5.362-18.72-11.594-6.155-6.23-10.07-13.385-11.47-19.03-1.398-5.647-.36-8.747 1-10.126.895-.905 2.445-1.648 4.97-1.656zm-208.156 58.78c1.09.03 1.962.298 2.593.657 1.685.96 3.395 3.504 3.47 9.19.075 5.684-1.918 13.503-6.344 21.06-4.426 7.56-10.314 13.22-15.406 16.032-5.092 2.813-8.378 2.615-10.063 1.657-1.684-.96-3.393-3.504-3.468-9.19-.076-5.684 1.916-13.535 6.343-21.093 4.426-7.557 10.314-13.187 15.406-16 2.545-1.406 4.643-2.064 6.31-2.25.418-.046.794-.072 1.157-.062zm109.937 1.282c-13 .075-26.444 5.487-34 14.125 6.848 7.143 13.47 14.445 19.78 21.876 7.846-5.784 16.262-10.73 25.19-14.594 1.14-4.79 2.448-9.514 3.905-14.156.432-1.375.887-2.73 1.344-4.094-4.97-2.18-10.548-3.188-16.22-3.156zm203.125 15.78c.272-.02.57-.02.875 0 1.826.14 4.258 1.214 7.313 4.19 4.072 3.966 8.222 10.9 10.437 19.374 2.215 8.474 2.018 16.63.406 22.22-1.61 5.588-4.068 7.798-5.936 8.31-1.87.514-4.865-.095-8.938-4.06-4.072-3.968-8.223-10.902-10.437-19.376-2.216-8.474-2.02-16.63-.408-22.22 1.612-5.588 4.07-7.798 5.938-8.31.234-.066.478-.106.75-.126z"/></svg>',
  };

  const formatCount = (count) => ` ${count}`;

  const getAccessibleLabel = (button, iconValue) => {
    if (button.dataset.ariaLabel) {
      return button.dataset.ariaLabel;
    }
    if (button.dataset.label) {
      return button.dataset.label;
    }
    return DEFAULT_LABELS[iconValue] || FALLBACK_LABEL;
  };

  const toggleFill = (icon, filled) => {
    const svg = icon.querySelector("svg");
    if (svg) {
      const iconType = icon.closest('.iine-button')?.dataset.icon || DEFAULT_ICON;
      if (iconType === 'upvote') {
        svg.setAttribute("stroke-width", filled ? "3" : "2");
      } else {
        const paths = svg.querySelectorAll("path, polygon, circle");
        paths.forEach((path) => {
          path.setAttribute("fill", filled ? "currentColor" : "none");
        });
      }
    }
  };

  document.addEventListener("DOMContentLoaded", () => {
    const iineButtons = document.querySelectorAll(".iine-button");
    if (iineButtons.length === 0) {
      return;
    }
    const slugs = Array.from(iineButtons).map(
      (button) => button.dataset.slug || window.location.pathname
    );
    const uniqueSlugs = [...new Set(slugs)];
    const counts = new Map();
    const fetchCounts = async () => {
      if (uniqueSlugs.length === 0) {
        return;
      }
      try {
        const response = await fetch(
          `${SUPABASE_URL}/rest/v1/rpc/get_hits`,
          {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              apikey: SUPABASE_KEY,
              Authorization: `Bearer ${SUPABASE_KEY}`,
            },
            body: JSON.stringify({
              page_slugs: uniqueSlugs,
            }),
          }
        );
        if (!response.ok) throw new Error("Failed to fetch counts");
        const countsData = await response.json();
        if (countsData && typeof countsData === 'object' && !countsData.error) {
          for (const slug in countsData) {
            const count = countsData[slug];
            counts.set(slug, typeof count === 'number' ? count : 0);
          }
        }
      } catch (error) {
        console.error("Error fetching iine counts:", error);
      }
    };

    const initializeButtons = async () => {
      await fetchCounts();

      iineButtons.forEach((button) => {
        const slug = button.dataset.slug || window.location.pathname;
        const storageKey = `iine-clicked-${slug}`;
        let icon = button.querySelector(".icon");
        let counter = button.querySelector(".counter");

        if (!icon) {
          // Clear fallback content (e.g. <noscript> text) before building JS structure.
          button.textContent = "";
          icon = document.createElement("span");
          icon.className = "icon";
          button.appendChild(icon);
        }

        if (!counter) {
          counter = document.createElement("span");
          counter.className = "counter";
          button.appendChild(counter);
        }

        let count = counts.get(slug) || 0;
        counter.textContent = formatCount(count);

        const iconValue = button.dataset.icon || DEFAULT_ICON;

        if (!button.getAttribute('aria-label')) {
          const accessibleLabel = getAccessibleLabel(button, iconValue);
          button.setAttribute('aria-label', accessibleLabel);
          button.setAttribute('title', accessibleLabel);
        }
        button.removeAttribute('aria-hidden');
        const isPressed = !!localStorage.getItem(storageKey);
        button.setAttribute('aria-pressed', isPressed.toString());

        const descriptionId = `iine-description-${slug.replace(/\W/g, '-')}`;
        let description = document.getElementById(descriptionId);
        if (!description) {
          description = document.createElement('span');
          description.id = descriptionId;
          description.style.display = 'none';
          description.textContent = 'Already clicked.';
          document.body.appendChild(description);
        }

        if (BUILTIN_ICONS[iconValue]) {
          icon.innerHTML = BUILTIN_ICONS[iconValue];
        } else {
          icon.textContent = iconValue;
        }

        if (localStorage.getItem(storageKey)) {
          button.classList.add("clicked");
          button.setAttribute('aria-disabled', 'true');
          button.setAttribute('aria-describedby', descriptionId);
          if (BUILTIN_ICONS[iconValue]) {
            toggleFill(icon, true);
          }
        }

        const handleInteraction = async (event) => {
          if (localStorage.getItem(storageKey) || button.getAttribute('aria-disabled') === 'true') {
            return;
          }
          if (event) {
            event.preventDefault();
          }
          button.setAttribute('aria-disabled', 'true');
          localStorage.setItem(storageKey, "true");
          count++;
          button.classList.add("clicked");
          button.setAttribute('aria-pressed', 'true');
          button.setAttribute('aria-describedby', descriptionId);
          counter.textContent = formatCount(count);
          if (BUILTIN_ICONS[iconValue]) {
            toggleFill(icon, true);
          }
          fetch(`${SUPABASE_URL}/rest/v1/rpc/increment_hits`, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
              apikey: SUPABASE_KEY,
              Authorization: `Bearer ${SUPABASE_KEY}`,
            },
            body: JSON.stringify({ page_slug: slug }),
          }).catch(error => {
            console.error("Failed to increment page hits:", error);
          });
        };
        // Progressive enhancement: if button is in a form, intercept form submission.
        // This allows no-JS users to submit forms while JS users get smooth interactions.
        const form = button.closest('form');
        if (form) {
          form.addEventListener('submit', (event) => {
            event.preventDefault();
            handleInteraction(event);
          });
        } else {
          button.addEventListener("click", handleInteraction);
        }
        button.addEventListener("keydown", (event) => {
          if (event.key === "Enter" || event.key === " ") {
            event.preventDefault();
            handleInteraction(event);
          }
        });
      });
    };

    initializeButtons();
  });
})();