├── LICENSE ├── favicon.svg ├── index.html ├── mavoice.js └── style.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lea Verou 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 | -------------------------------------------------------------------------------- /favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 📣 3 | 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MaVoice 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
16 |
17 |

18 | MaVoice 19 | [url('repo') or 'mavoweb/mavo'] 20 |

21 | New proposal ↗️ 22 |
23 |
24 | 25 |
26 |

You have voted on [count(hasVoted)] proposals

27 | 28 |
29 |
30 |
31 | 0 32 | votes 33 |
34 | 35 |
36 |
37 | 38 | 39 | 40 |

Title

41 |
42 |
Description
43 | 46 |
47 |
48 |
49 | 50 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /mavoice.js: -------------------------------------------------------------------------------- 1 | // Check if current user has voted 2 | document.addEventListener("mv-login", async evt => { 3 | let app = Mavo.all.mavoice; 4 | await app.dataLoaded; 5 | 6 | let repo = app.root.children.repo.value; 7 | let [owner, repoName] = repo.split("/"); 8 | let labels = app.root.children.labels.value; 9 | 10 | let query = `query { 11 | repository(owner:"${owner}", name:"${repoName}") { 12 | issues(first:100, states:OPEN, labels:"${labels}", orderBy:{field: COMMENTS, direction: DESC}) { 13 | edges { 14 | node { 15 | number 16 | hasVoted: reactions(content:THUMBS_UP) { 17 | viewerHasReacted 18 | } 19 | } 20 | } 21 | } 22 | } 23 | }`; 24 | 25 | let json = await app.storage.get(Mavo.Backend.Github.apiDomain + "graphql#" + query); 26 | 27 | let arr = await json.repository.issues.edges.map(n => n.node); 28 | 29 | arr.forEach(n => { 30 | n.hasVoted = n.hasVoted.viewerHasReacted; 31 | var issue = $("#issue" + n.number); 32 | 33 | if (issue) { 34 | let node = Mavo.Node.get(issue); 35 | node.children.hasVoted.value = n.hasVoted; 36 | } 37 | }); 38 | }); 39 | 40 | // Sort data properly before rendering, since the API can't sort by reactions 41 | // and Mavo's sort plugin is borked 42 | Mavo.hooks.add("render-start", ({data}) => { 43 | if (data) { 44 | data = data.sort((a, b) => b.reactions["+1"] - a.reactions["+1"]); 45 | } 46 | }) 47 | 48 | // Makes the vote button work 49 | $.delegate(document, "click", ".votes button", evt => { 50 | var app = Mavo.all.mavoice; 51 | var github = app.storage; 52 | 53 | var issue = evt.target.closest("article"); 54 | var node = Mavo.Node.get(issue); 55 | var url = new URL($(".content > a", issue).href); 56 | url.pathname = "/repos" + url.pathname + "/reactions"; 57 | url.hostname = "api." + url.hostname; 58 | 59 | var hasVoted = evt.target.classList.contains("pressed"); 60 | 61 | app.inProgress = "Voting"; 62 | var headers = { 63 | headers: { 64 | "Accept": "application/vnd.github.squirrel-girl-preview" 65 | } 66 | }; 67 | 68 | github.login() 69 | .then(() => github.request(url, {"content": "+1"}, "POST", headers)) 70 | .then(data => { 71 | if (hasVoted) { 72 | // Remove vote 73 | return github.request(Mavo.Backend.Github.apiDomain + "reactions/" + data.id, null, "DELETE", headers).then(() => { 74 | node.children.votes.value--; 75 | node.children.hasVoted.value = false; 76 | app.inProgress = false; 77 | }); 78 | } 79 | else { 80 | // Created vote 81 | node.children.votes.value++; 82 | node.children.hasVoted.value = true; 83 | app.inProgress = false; 84 | } 85 | }); 86 | }) 87 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap'); 2 | 3 | * { 4 | font-family: inherit; 5 | } 6 | 7 | body { 8 | max-width: 60em; 9 | margin: 1em auto; 10 | padding: 1em; 11 | font: 100%/1.5 Inter, Helvetica Neue, -apple-system, sans-serif; 12 | } 13 | 14 | 15 | @media (max-width: 600px) { 16 | body { 17 | font-size: 90%; 18 | } 19 | } 20 | 21 | a { 22 | color: hsl(220 40% 50%); 23 | } 24 | 25 | header { 26 | margin: 1em 0; 27 | } 28 | 29 | @media (min-width: 600px) { 30 | header { 31 | display: grid; 32 | grid-template-columns: 1fr auto; 33 | } 34 | } 35 | 36 | header > .button { 37 | display: block; 38 | margin: auto .1em; 39 | border-radius: .3rem; 40 | text-decoration: none; 41 | font-size: 150%; 42 | } 43 | 44 | h1 { 45 | position: relative; 46 | font-size: 400%; 47 | margin: 0; 48 | } 49 | 50 | h1 [property=repo] { 51 | display: block; 52 | font-size: 50%; 53 | letter-spacing: -.02em; 54 | } 55 | 56 | h1 [property=repo]:not(:hover) { 57 | text-decoration: none; 58 | color: inherit; 59 | } 60 | 61 | h1::before { 62 | content: url(favicon.svg); 63 | position: absolute; 64 | top: 0; 65 | right: 100%; 66 | width: 1.1em; 67 | height: 1.1em; 68 | } 69 | 70 | h1, h2, h3 { 71 | letter-spacing: -.04em; 72 | line-height: 1; 73 | } 74 | 75 | button:not(.mv-ui), 76 | .button { 77 | background: linear-gradient(hsla(0,0%,100%,.5), transparent) hsl(220, 20%, 70%) border-box; 78 | border: 1px solid rgba(0,0,0,.2); 79 | box-shadow: inset 0 1px 0 0 hsla(0,0%,100%,.5), 0 1px 2px 0 rgba(0,0,0,.1); 80 | text-shadow: 0 1px 0 hsla(0,0%,100%,.5); 81 | font-weight: bold; 82 | cursor: pointer; 83 | padding: .3em .5em; 84 | box-sizing: border-box; 85 | color: black; 86 | } 87 | 88 | button:not(.mv-ui):hover, 89 | button:not(.mv-ui):active, 90 | .button:hover, 91 | .button:active, 92 | button:not(.mv-ui).pressed { 93 | background: hsl(210, 64%, 51%); 94 | color: white; 95 | text-shadow: 0 -1px 1px #2361a4 96 | } 97 | 98 | button:not(.mv-ui):active, 99 | .button:active, 100 | button:not(.mv-ui).pressed { 101 | box-shadow: inset 0 0 6px 3px #1657b5, 0 1px 0 0 #fff; 102 | } 103 | 104 | [property=issues] { 105 | display: flex; 106 | } 107 | 108 | [property=issues] + [property=issues] { 109 | margin-top: 3em; 110 | } 111 | 112 | .votes { 113 | display: flex; 114 | flex-flow: column; 115 | min-width: 6em; 116 | margin-right: 1em; 117 | } 118 | 119 | .votes > * { 120 | border-radius: .3rem; 121 | } 122 | 123 | .vote-count { 124 | padding: .5em; 125 | border: 1px solid hsla(0,0%,0%,.2); 126 | background: hsl(220, 60%, 99%); 127 | box-shadow: rgba(0,0,0,.1) 0 1px 1px; 128 | text-align: center; 129 | } 130 | 131 | [property="votes"] { 132 | display: block; 133 | font-size: 250%; 134 | line-height: 1; 135 | font-weight: bold; 136 | } 137 | 138 | .votes button { 139 | width: 100%; 140 | margin-top: .3em; 141 | font-size: 80%; 142 | } 143 | 144 | .votes button.mv-empty:not([mv-mode=edit]) { 145 | display: block; 146 | } 147 | 148 | [property=issues] .content { 149 | flex: 1; 150 | } 151 | 152 | .content > a:not(:hover) { 153 | text-decoration: none; 154 | } 155 | 156 | .content h2 { 157 | margin-top: 0; 158 | } 159 | 160 | [property=issues] footer { 161 | font-size: 80%; 162 | color: rgba(0,0,0,.5); 163 | } 164 | 165 | body > footer { 166 | margin-top: 1em; 167 | padding-top: 1em; 168 | border-top: 1px solid rgb(0 0 0 / .1); 169 | } 170 | 171 | [property=body] img, 172 | [property=body] video { 173 | max-width: 100%; 174 | } 175 | 176 | #vote-stats { 177 | margin: 1em 0; 178 | padding: .3em; 179 | text-align: center; 180 | background: hsl(220 40% 95%); 181 | } 182 | --------------------------------------------------------------------------------