tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
291
fork
atom
a tool for shared writing and social publishing
291
fork
atom
overview
issues
27
pulls
pipelines
added pollBlock
cozylittle.house
1 year ago
20e72693
d650d51b
+276
-18
6 changed files
expand all
collapse all
unified
split
app
globals.css
components
Blocks
Block.tsx
BlockCommands.tsx
PollBlock.tsx
RSVPBlock
index.tsx
src
replicache
attributes.ts
-15
app/globals.css
···
229
229
@apply outline-transparent;
230
230
}
231
231
232
232
-
.text-with-outline {
233
233
-
position: relative;
234
234
-
-webkit-text-stroke: 1px purple;
235
235
-
z-index: 1;
236
236
-
237
237
-
::before {
238
238
-
content: attr(data-text);
239
239
-
position: absolute;
240
240
-
top: 0;
241
241
-
left: 0;
242
242
-
color: blue;
243
243
-
z-index: 0;
244
244
-
}
245
245
-
}
246
246
-
247
232
.pwa-padding {
248
233
padding-top: max(calc(env(safe-area-inset-top) - 8px)) !important;
249
234
}
+2
-1
components/Blocks/Block.tsx
···
23
23
import { RSVPBlock } from "./RSVPBlock";
24
24
import { elementId } from "src/utils/elementId";
25
25
import { ButtonBlock } from "./ButtonBlock";
26
26
+
import { PollBlock } from "./PollBlock";
26
27
27
28
export type Block = {
28
29
factID: string;
···
71
72
);
72
73
73
74
let [areYouSure, setAreYouSure] = useState(false);
74
74
-
75
75
useEffect(() => {
76
76
if (!selected) {
77
77
setAreYouSure(false);
···
176
176
datetime: DateTimeBlock,
177
177
rsvp: RSVPBlock,
178
178
button: ButtonBlock,
179
179
+
poll: PollBlock,
179
180
};
180
181
181
182
export const BlockMultiselectIndicator = (props: BlockProps) => {
+9
components/Blocks/BlockCommands.tsx
···
204
204
createBlockWithType(rep, props, "mailbox");
205
205
},
206
206
},
207
207
+
{
208
208
+
name: "Poll",
209
209
+
icon: <BlockMailboxSmall />,
210
210
+
type: "block",
211
211
+
onSelect: async (rep, props) => {
212
212
+
let entity;
213
213
+
createBlockWithType(rep, props, "poll");
214
214
+
},
215
215
+
},
207
216
208
217
// EVENT STUFF
209
218
+262
components/Blocks/PollBlock.tsx
···
1
1
+
import { useUIState } from "src/useUIState";
2
2
+
import { BlockProps } from "./Block";
3
3
+
import { ButtonPrimary, ButtonSecondary } from "components/Buttons";
4
4
+
import { useEffect, useState } from "react";
5
5
+
import { Input } from "components/Input";
6
6
+
import { CheckTiny, CloseTiny, InfoSmall } from "components/Icons";
7
7
+
import { Separator } from "components/Layout";
8
8
+
import { useEntitySetContext } from "components/EntitySetProvider";
9
9
+
import { theme } from "tailwind.config";
10
10
+
11
11
+
export const PollBlock = (props: BlockProps) => {
12
12
+
let isSelected = useUIState((s) =>
13
13
+
s.selectedBlocks.find((b) => b.value === props.entityID),
14
14
+
);
15
15
+
let { permissions } = useEntitySetContext();
16
16
+
17
17
+
let [pollState, setPollState] = useState<"editing" | "voting" | "results">(
18
18
+
!permissions.write ? "voting" : "editing",
19
19
+
);
20
20
+
21
21
+
let [pollOptions, setPollOptions] = useState<
22
22
+
{ value: string; votes: number }[]
23
23
+
>([
24
24
+
{ value: "hello", votes: 2 },
25
25
+
{ value: "hi", votes: 4 },
26
26
+
]);
27
27
+
28
28
+
let totalVotes = pollOptions.reduce((sum, option) => sum + option.votes, 0);
29
29
+
30
30
+
let highestVotes = Math.max(...pollOptions.map((option) => option.votes));
31
31
+
let winningIndexes = pollOptions.reduce<number[]>(
32
32
+
(indexes, option, index) => {
33
33
+
if (option.votes === highestVotes) indexes.push(index);
34
34
+
return indexes;
35
35
+
},
36
36
+
[],
37
37
+
);
38
38
+
39
39
+
return (
40
40
+
<div
41
41
+
className={`poll flex flex-col gap-2 p-3 w-full
42
42
+
${isSelected ? "block-border-selected " : "block-border"}`}
43
43
+
style={{
44
44
+
backgroundColor:
45
45
+
"color-mix(in oklab, rgb(var(--accent-1)), rgb(var(--bg-page)) 85%)",
46
46
+
}}
47
47
+
>
48
48
+
{pollState === "editing" && totalVotes > 0 && (
49
49
+
<div className="text-sm italic text-tertiary">
50
50
+
You can't edit options people already voted for!
51
51
+
</div>
52
52
+
)}
53
53
+
54
54
+
{/* Empty state if no options yet */}
55
55
+
{(pollOptions.every((option) => option.value === "") ||
56
56
+
pollOptions.length === 0) &&
57
57
+
pollState !== "editing" && (
58
58
+
<div className="text-center italic text-tertiary text-sm">
59
59
+
no options yet...
60
60
+
</div>
61
61
+
)}
62
62
+
63
63
+
{pollOptions.map((option, index) => (
64
64
+
<PollOption
65
65
+
key={index}
66
66
+
state={pollState}
67
67
+
setState={setPollState}
68
68
+
optionName={option.value}
69
69
+
setOptionName={(newValue) => {
70
70
+
setPollOptions((oldOptions) => {
71
71
+
let newOptions = [...oldOptions];
72
72
+
newOptions[index] = {
73
73
+
value: newValue,
74
74
+
votes: oldOptions[index].votes,
75
75
+
};
76
76
+
return newOptions;
77
77
+
});
78
78
+
}}
79
79
+
votes={option.votes}
80
80
+
setVotes={(newVotes) => {
81
81
+
setPollOptions((oldOptions) => {
82
82
+
let newOptions = [...oldOptions];
83
83
+
newOptions[index] = {
84
84
+
value: oldOptions[index].value,
85
85
+
votes: newVotes,
86
86
+
};
87
87
+
return newOptions;
88
88
+
});
89
89
+
}}
90
90
+
totalVotes={totalVotes}
91
91
+
winner={winningIndexes.includes(index)}
92
92
+
removeOption={() => {
93
93
+
setPollOptions((oldOptions) => {
94
94
+
let newOptions = [...oldOptions];
95
95
+
newOptions.splice(index, 1);
96
96
+
return newOptions;
97
97
+
});
98
98
+
}}
99
99
+
/>
100
100
+
))}
101
101
+
{!permissions.write ? null : pollState === "editing" ? (
102
102
+
<>
103
103
+
<AddPollOptionButton
104
104
+
addPollOption={() => {
105
105
+
setPollOptions([...pollOptions, { value: "", votes: 0 }]);
106
106
+
}}
107
107
+
/>
108
108
+
<hr className="border-border" />
109
109
+
<ButtonPrimary
110
110
+
className="place-self-end"
111
111
+
onMouseDown={() => {
112
112
+
setPollState("voting");
113
113
+
// TODO: Currently, the options are updated onChange in thier inputs in PollOption.
114
114
+
// However, they should instead be updated when this save button is clicked!
115
115
+
}}
116
116
+
>
117
117
+
Save <CheckTiny />
118
118
+
</ButtonPrimary>
119
119
+
</>
120
120
+
) : (
121
121
+
<div className="flex justify-end gap-2">
122
122
+
<EditPollOptionsButton state={pollState} setState={setPollState} />
123
123
+
<Separator classname="h-6" />
124
124
+
<PollStateToggle setPollState={setPollState} pollState={pollState} />
125
125
+
</div>
126
126
+
)}
127
127
+
</div>
128
128
+
);
129
129
+
};
130
130
+
131
131
+
const PollOption = (props: {
132
132
+
state: "editing" | "voting" | "results";
133
133
+
setState: (state: "editing" | "voting" | "results") => void;
134
134
+
optionName: string;
135
135
+
setOptionName: (optionName: string) => void;
136
136
+
votes: number;
137
137
+
setVotes: (votes: number) => void;
138
138
+
totalVotes: number;
139
139
+
winner: boolean;
140
140
+
removeOption: () => void;
141
141
+
}) => {
142
142
+
let [inputValue, setInputValue] = useState(props.optionName);
143
143
+
return props.state === "editing" ? (
144
144
+
<div className="flex gap-2 items-center">
145
145
+
<Input
146
146
+
type="text"
147
147
+
className="pollOptionInput w-full input-with-border"
148
148
+
placeholder="Option here..."
149
149
+
disabled={props.votes > 0}
150
150
+
value={inputValue}
151
151
+
onChange={(e) => {
152
152
+
setInputValue(e.target.value);
153
153
+
props.setOptionName(e.target.value);
154
154
+
}}
155
155
+
onKeyDown={(e) => {
156
156
+
if (e.key === "Backspace" && !e.currentTarget.value) {
157
157
+
e.preventDefault();
158
158
+
props.removeOption();
159
159
+
}
160
160
+
}}
161
161
+
/>
162
162
+
163
163
+
<button
164
164
+
disabled={props.votes > 0}
165
165
+
className="text-accent-contrast disabled:text-border"
166
166
+
onMouseDown={() => {
167
167
+
props.removeOption();
168
168
+
}}
169
169
+
>
170
170
+
<CloseTiny />
171
171
+
</button>
172
172
+
</div>
173
173
+
) : props.optionName === "" ? null : props.state === "voting" ? (
174
174
+
<div className="flex gap-2 items-center">
175
175
+
<ButtonSecondary
176
176
+
className={`pollOption grow max-w-full`}
177
177
+
onClick={() => {
178
178
+
props.setState("results");
179
179
+
props.setVotes(props.votes + 1);
180
180
+
}}
181
181
+
>
182
182
+
{props.optionName}
183
183
+
</ButtonSecondary>
184
184
+
</div>
185
185
+
) : (
186
186
+
<div
187
187
+
className={`pollResult relative grow py-0.5 px-2 border-accent-contrast rounded-md overflow-hidden ${props.winner ? "font-bold border-2" : "border"}`}
188
188
+
>
189
189
+
<div
190
190
+
style={{
191
191
+
WebkitTextStroke: `${props.winner ? "6px" : "6px"} ${theme.colors["bg-page"]}`,
192
192
+
paintOrder: "stroke fill",
193
193
+
}}
194
194
+
className={`pollResultContent text-accent-contrast relative flex gap-2 justify-between z-10`}
195
195
+
>
196
196
+
<div className="grow max-w-full truncate">{props.optionName}</div>
197
197
+
<div>{props.votes}</div>
198
198
+
</div>
199
199
+
<div
200
200
+
className={`pollResultBG absolute bg-bg-page w-full top-0 bottom-0 left-0 right-0 flex flex-row z-0`}
201
201
+
>
202
202
+
<div
203
203
+
className={`bg-accent-contrast rounded-[2px] m-0.5`}
204
204
+
style={{
205
205
+
maskImage: "var(--hatchSVG)",
206
206
+
maskRepeat: "repeat repeat",
207
207
+
208
208
+
...(props.votes === 0
209
209
+
? { width: "4px" }
210
210
+
: { flexBasis: `${(props.votes / props.totalVotes) * 100}%` }),
211
211
+
}}
212
212
+
/>
213
213
+
<div />
214
214
+
</div>
215
215
+
</div>
216
216
+
);
217
217
+
};
218
218
+
219
219
+
const AddPollOptionButton = (props: { addPollOption: () => void }) => {
220
220
+
return (
221
221
+
<button
222
222
+
className="pollAddOption w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast"
223
223
+
onClick={() => {
224
224
+
props.addPollOption();
225
225
+
}}
226
226
+
>
227
227
+
Add an Option
228
228
+
</button>
229
229
+
);
230
230
+
};
231
231
+
232
232
+
const EditPollOptionsButton = (props: {
233
233
+
state: "editing" | "voting" | "results";
234
234
+
setState: (state: "editing" | "voting" | "results") => void;
235
235
+
}) => {
236
236
+
return (
237
237
+
<button
238
238
+
className="pollEditOptions w-fit flex gap-2 items-center justify-start text-sm text-accent-contrast"
239
239
+
onClick={() => {
240
240
+
props.setState("editing");
241
241
+
}}
242
242
+
>
243
243
+
Edit Options{" "}
244
244
+
</button>
245
245
+
);
246
246
+
};
247
247
+
248
248
+
const PollStateToggle = (props: {
249
249
+
setPollState: (pollState: "editing" | "voting" | "results") => void;
250
250
+
pollState: "editing" | "voting" | "results";
251
251
+
}) => {
252
252
+
return (
253
253
+
<button
254
254
+
className="text-sm text-accent-contrast sm:hover:underline"
255
255
+
onMouseDown={() => {
256
256
+
props.setPollState(props.pollState === "voting" ? "results" : "voting");
257
257
+
}}
258
258
+
>
259
259
+
{props.pollState === "voting" ? "See Results" : "Back to Poll"}
260
260
+
</button>
261
261
+
);
262
262
+
};
+1
-1
components/Blocks/RSVPBlock/index.tsx
···
209
209
<RSVPBackground />
210
210
<div className=" relative flex flex-col gap-1 sm:gap-2 z-[1] justify-center w-fit mx-auto">
211
211
<div
212
212
-
className=" w-fit text-xl text-center text-accent-2 text-with-outline"
212
212
+
className=" w-fit text-xl text-center text-accent-2"
213
213
style={{
214
214
WebkitTextStroke: `3px ${theme.colors["accent-1"]}`,
215
215
textShadow: `-4px 3px 0 ${theme.colors["accent-1"]}`,
+2
-1
src/replicache/attributes.ts
···
269
269
| "link"
270
270
| "mailbox"
271
271
| "embed"
272
272
-
| "button";
272
272
+
| "button"
273
273
+
| "poll";
273
274
};
274
275
"canvas-pattern-union": {
275
276
type: "canvas-pattern-union";