├── demo.png ├── node_helper.js ├── package.json ├── LICENSE ├── README.md ├── MMM-NouncilVotes.css └── MMM-NouncilVotes.js /demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XppaiCyberr/MMM-NouncilVotes/HEAD/demo.png -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | const NodeHelper = require("node_helper"); 2 | 3 | module.exports = NodeHelper.create({ 4 | start: function() { 5 | console.log("Starting node helper for: " + this.name); 6 | }, 7 | 8 | socketNotificationReceived: function(notification, payload) { 9 | // Handle socket notifications from the module here if needed in the future 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmm-nouncilvotes", 3 | "version": "1.0.1", 4 | "description": "A MagicMirror module to display Nouncil voting participation data", 5 | "main": "MMM-NouncilVotes.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "magicmirror", 11 | "module", 12 | "nouncil", 13 | "voting", 14 | "participation" 15 | ], 16 | "author": "XppaiCyber", 17 | "license": "MIT", 18 | "dependencies": {}, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/XppaiCyberr/MMM-NouncilVotes.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/XppaiCyberr/MMM-NouncilVotes/issues" 25 | }, 26 | "homepage": "https://github.com/XppaiCyberr/MMM-NouncilVotes#readme" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 XppaiCyber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMM-NouncilVotes 2 | 3 | A MagicMirror module that displays Nouncil voting participation data in a clean, informative interface. 4 | 5 |  6 | 7 | ## Features 8 | 9 | - Real-time Nouncil voting participation tracking 10 | - Configurable display options 11 | - Pagination and cycling of voter data 12 | - Highlighting of top voters 13 | - Responsive and visually appealing design 14 | 15 | ## Installation 16 | 17 | 1. Navigate to your MagicMirror's `modules` directory 18 | 2. Clone this repository: 19 | ```bash 20 | git clone https://github.com/XppaiCyberr/MMM-NouncilVotes.git 21 | ``` 22 | 3. Install dependencies: 23 | ```bash 24 | cd MMM-NouncilVotes 25 | npm install 26 | ``` 27 | 28 | ## Configuration 29 | 30 | Add the module to your `config.js` file: 31 | 32 | ```javascript 33 | { 34 | module: 'MMM-NouncilVotes', 35 | position: 'top_right', 36 | config: { 37 | updateInterval: 5 * 60 * 1000, // Update every 5 minutes 38 | maxEntries: 10, // Number of entries visible at once 39 | cycleInterval: 15 * 1000, // Cycle through data every 15 seconds 40 | showTwitter: false, // Show Twitter handles 41 | showWallet: false, // Show wallet addresses 42 | minParticipationRate: 0, // Minimum participation rate to show 43 | highlightTopVoters: true, // Highlight top 3 voters 44 | animationSpeed: 2000, // Speed of transitions 45 | showLastUpdated: true // Show when data was last updated 46 | } 47 | } 48 | ``` 49 | 50 | ## Configuration Options 51 | 52 | | Option | Description | Default | Type | 53 | |--------|-------------|---------|------| 54 | | `updateInterval` | How often to fetch new data (in milliseconds) | `5 * 60 * 1000` | Number | 55 | | `maxEntries` | Number of entries to display per page | `10` | Number | 56 | | `cycleInterval` | Time between page changes (in milliseconds) | `15 * 1000` | Number | 57 | | `showTwitter` | Display Twitter handles | `false` | Boolean | 58 | | `showWallet` | Display wallet addresses | `false` | Boolean | 59 | | `minParticipationRate` | Minimum participation rate to display (0-100) | `0` | Number | 60 | | `highlightTopVoters` | Highlight top 3 voters | `true` | Boolean | 61 | | `animationSpeed` | Speed of UI transitions (in milliseconds) | `2000` | Number | 62 | | `showLastUpdated` | Show when data was last updated | `true` | Boolean | 63 | 64 | ## Dependencies 65 | 66 | - MagicMirror 67 | - Fetch API support 68 | 69 | ## API 70 | 71 | This module uses the Nouncil API at `https://api.nouncil.wtf` to retrieve voting participation data. 72 | 73 | ## Contributing 74 | 75 | Contributions are welcome! Please submit pull requests or open issues on the GitHub repository. 76 | 77 | 78 | ## Credits 79 | 80 | Created by XppaiCyber 81 | -------------------------------------------------------------------------------- /MMM-NouncilVotes.css: -------------------------------------------------------------------------------- 1 | .mmm-nouncilvotes { 2 | width: 100%; 3 | max-width: 350px; 4 | margin: 0 auto; 5 | font-family: 'Roboto Condensed', 'Roboto', 'Helvetica Neue', Arial, sans-serif; 6 | color: #eee; 7 | background-color: rgba(0, 0, 0, 0.2); 8 | border-radius: 8px; 9 | padding: 10px; 10 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); 11 | font-size: 0.9em; 12 | } 13 | 14 | /* Header styling */ 15 | .mmm-nouncilvotes .module-header { 16 | font-size: 1em; 17 | font-weight: bold; 18 | text-align: center; 19 | margin-bottom: 10px; 20 | text-transform: uppercase; 21 | letter-spacing: 1px; 22 | color: #fff; 23 | text-shadow: 0 0 8px rgba(255, 255, 255, 0.5); 24 | } 25 | 26 | /* Table container with scrolling ability if needed */ 27 | .mmm-nouncilvotes .table-container { 28 | width: 100%; 29 | overflow: hidden; 30 | } 31 | 32 | .mmm-nouncilvotes table { 33 | width: 100%; 34 | border-spacing: 0; 35 | border-collapse: separate; 36 | table-layout: fixed; 37 | border-radius: 8px; 38 | overflow: hidden; 39 | } 40 | 41 | /* Header cells */ 42 | .mmm-nouncilvotes th { 43 | text-align: left; 44 | padding: 8px 10px; 45 | background-color: rgba(30, 30, 30, 0.7); 46 | font-weight: bold; 47 | text-transform: uppercase; 48 | font-size: 0.7em; 49 | letter-spacing: 1px; 50 | color: #fff; 51 | border-bottom: 2px solid #444; 52 | } 53 | 54 | .mmm-nouncilvotes th:first-child { 55 | width: 50%; /* Username column takes half the space */ 56 | border-top-left-radius: 8px; 57 | } 58 | 59 | .mmm-nouncilvotes th:nth-child(2) { 60 | width: 20%; /* Votes column takes 20% space */ 61 | text-align: center; 62 | } 63 | 64 | .mmm-nouncilvotes th:last-child { 65 | width: 30%; /* Participation column takes 30% space */ 66 | border-top-right-radius: 8px; 67 | } 68 | 69 | /* Table cells */ 70 | .mmm-nouncilvotes td { 71 | padding: 6px 10px; 72 | vertical-align: middle; 73 | border-bottom: 1px solid rgba(255, 255, 255, 0.1); 74 | transition: background-color 0.3s ease; 75 | font-size: 0.75em; 76 | } 77 | 78 | .mmm-nouncilvotes .align-left { 79 | text-align: left; 80 | white-space: nowrap; 81 | overflow: hidden; 82 | text-overflow: ellipsis; 83 | } 84 | 85 | /* Row styling */ 86 | .mmm-nouncilvotes tr { 87 | transition: transform 0.2s ease, background-color 0.3s ease; 88 | } 89 | 90 | .mmm-nouncilvotes tbody tr:nth-child(even) { 91 | background-color: rgba(255, 255, 255, 0.05); 92 | } 93 | 94 | .mmm-nouncilvotes tbody tr:hover { 95 | background-color: rgba(255, 255, 255, 0.15); 96 | transform: translateX(5px); 97 | } 98 | 99 | /* Top voters highlight */ 100 | .mmm-nouncilvotes .top-voter { 101 | font-weight: bold; 102 | } 103 | 104 | .mmm-nouncilvotes .rank-1 { 105 | background-color: rgba(218, 165, 32, 0.2) !important; /* Gold */ 106 | } 107 | 108 | .mmm-nouncilvotes .rank-2 { 109 | background-color: rgba(192, 192, 192, 0.2) !important; /* Silver */ 110 | } 111 | 112 | .mmm-nouncilvotes .rank-3 { 113 | background-color: rgba(205, 127, 50, 0.2) !important; /* Bronze */ 114 | } 115 | 116 | /* Rank number styling */ 117 | .mmm-nouncilvotes .rank { 118 | font-weight: bold; 119 | margin-right: 5px; 120 | } 121 | 122 | /* Twitter handle styling */ 123 | .mmm-nouncilvotes .twitter { 124 | color: #1DA1F2; 125 | opacity: 0.8; 126 | font-size: 0.8em; 127 | } 128 | 129 | /* Progress bar styling */ 130 | .mmm-nouncilvotes .progress-bar { 131 | height: 4px; 132 | background-color: rgba(255, 255, 255, 0.1); 133 | border-radius: 3px; 134 | margin-top: 3px; 135 | overflow: hidden; 136 | } 137 | 138 | .mmm-nouncilvotes .progress-fill { 139 | height: 100%; 140 | border-radius: 3px; 141 | transition: width 1s ease-in-out; 142 | } 143 | 144 | .mmm-nouncilvotes .high-participation { 145 | background: linear-gradient(to right, #4CAF50, #8BC34A); 146 | } 147 | 148 | .mmm-nouncilvotes .medium-participation { 149 | background: linear-gradient(to right, #FFC107, #FFEB3B); 150 | } 151 | 152 | .mmm-nouncilvotes .low-participation { 153 | background: linear-gradient(to right, #FF5722, #F44336); 154 | } 155 | 156 | /* Pagination indicator */ 157 | .mmm-nouncilvotes .pagination { 158 | text-align: center; 159 | margin-top: 6px; 160 | font-size: 0.7em; 161 | color: rgba(255, 255, 255, 0.7); 162 | } 163 | 164 | /* Last updated timestamp */ 165 | .mmm-nouncilvotes .last-updated { 166 | text-align: right; 167 | margin-top: 6px; 168 | font-size: 0.7em; 169 | font-style: italic; 170 | color: rgba(255, 255, 255, 0.5); 171 | } 172 | 173 | /* Loading screen */ 174 | .mmm-nouncilvotes .loading-container { 175 | text-align: center; 176 | padding: 12px; 177 | color: rgba(255, 255, 255, 0.7); 178 | } 179 | 180 | .mmm-nouncilvotes .loading-spinner { 181 | display: inline-block; 182 | width: 20px; 183 | height: 20px; 184 | border: 2px solid rgba(255, 255, 255, 0.3); 185 | border-radius: 50%; 186 | border-top-color: #fff; 187 | animation: spin 1s ease-in-out infinite; 188 | margin-bottom: 6px; 189 | } 190 | 191 | .mmm-nouncilvotes .loading-text { 192 | font-style: italic; 193 | font-size: 0.8em; 194 | } 195 | 196 | @keyframes spin { 197 | to { transform: rotate(360deg); } 198 | } 199 | 200 | /* Animation for page transitions */ 201 | @keyframes fadeIn { 202 | from { opacity: 0; transform: translateY(10px); } 203 | to { opacity: 1; transform: translateY(0); } 204 | } 205 | 206 | .mmm-nouncilvotes tbody tr { 207 | animation: fadeIn 0.5s ease-out forwards; 208 | } 209 | 210 | /* Staggered animation for rows */ 211 | .mmm-nouncilvotes tbody tr:nth-child(1) { animation-delay: 0.05s; } 212 | .mmm-nouncilvotes tbody tr:nth-child(2) { animation-delay: 0.1s; } 213 | .mmm-nouncilvotes tbody tr:nth-child(3) { animation-delay: 0.15s; } 214 | .mmm-nouncilvotes tbody tr:nth-child(4) { animation-delay: 0.2s; } 215 | .mmm-nouncilvotes tbody tr:nth-child(5) { animation-delay: 0.25s; } 216 | .mmm-nouncilvotes tbody tr:nth-child(6) { animation-delay: 0.3s; } 217 | .mmm-nouncilvotes tbody tr:nth-child(7) { animation-delay: 0.35s; } 218 | .mmm-nouncilvotes tbody tr:nth-child(8) { animation-delay: 0.4s; } 219 | .mmm-nouncilvotes tbody tr:nth-child(9) { animation-delay: 0.45s; } 220 | .mmm-nouncilvotes tbody tr:nth-child(10) { animation-delay: 0.5s; } 221 | 222 | /* Smooth transition for page changes */ 223 | .mmm-nouncilvotes.mm-animate { 224 | transition: opacity 0.5s ease-in-out; 225 | } 226 | -------------------------------------------------------------------------------- /MMM-NouncilVotes.js: -------------------------------------------------------------------------------- 1 | Module.register("MMM-NouncilVotes", { 2 | defaults: { 3 | updateInterval: 5 * 60 * 1000, // Update every 5 minutes 4 | maxEntries: 10, // Number of entries visible at once 5 | cycleInterval: 15 * 1000, // Cycle through data every 10 seconds 6 | showTwitter: false, // Whether to show Twitter handles 7 | showWallet: false, // Whether to show wallet addresses 8 | minParticipationRate: 0, // Minimum participation rate to show (0-100) 9 | highlightTopVoters: true, // Whether to highlight top 3 voters 10 | animationSpeed: 2000, // Speed of transitions in milliseconds 11 | showLastUpdated: true // Whether to show when data was last updated 12 | }, 13 | 14 | start: function() { 15 | Log.info("Starting module: " + this.name); 16 | this.voters = []; 17 | this.loaded = false; 18 | this.currentPage = 0; 19 | this.lastUpdated = null; 20 | this.scheduleUpdate(); 21 | }, 22 | 23 | getStyles: function() { 24 | return ["MMM-NouncilVotes.css"]; 25 | }, 26 | 27 | getDom: function() { 28 | const wrapper = document.createElement("div"); 29 | wrapper.className = "mmm-nouncilvotes"; 30 | 31 | if (!this.loaded) { 32 | const loadingDiv = document.createElement("div"); 33 | loadingDiv.className = "loading-container"; 34 | loadingDiv.innerHTML = "