├── 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 |
23 |
24 | Login to Github to vote
25 |
26 |
You have voted on [count(hasVoted)] proposals
27 |
28 |
29 |
30 |
31 | 0
32 | votes
33 |
34 |
Vote[if(hasVoted, 'd')]
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 |
--------------------------------------------------------------------------------