|
2 | 2 | import { onMount } from "svelte"; |
3 | 3 |
|
4 | 4 | import logo from "/logo.svg"; |
5 | | - import RadioGroup from "./lib/RadioGroup.svelte"; |
6 | | - import { |
7 | | - createAnswer, |
8 | | - getAnswer, |
9 | | - getQuestion, |
10 | | - getSummary, |
11 | | - updateAnswer, |
12 | | - waitUntilLive, |
13 | | - type Answer, |
14 | | - type Problem, |
15 | | - type Question, |
16 | | - type Summary, |
17 | | - type UpdateAnswerPayload, |
18 | | - } from "./lib/api"; |
| 5 | + import { getQuestion, waitUntilLive, type Question } from "./lib/api"; |
19 | 6 | import Loading from "./lib/Loading.svelte"; |
20 | 7 | import Error from "./lib/Error.svelte"; |
21 | | - import Submit from "./lib/Submit.svelte"; |
22 | | - import Comment from "./lib/Comment.svelte"; |
23 | | - import BarChart from "./lib/BarChart.svelte"; |
24 | 8 | import Footer from "./lib/Footer.svelte"; |
25 | 9 | import Share from "./views/Share.svelte"; |
26 | | -
|
27 | | - let loading = $state(true); |
28 | | - let view = $state<string>("form"); |
29 | | - let error = $state<Problem | null>(null); |
| 10 | + import { initializePath, parsePath, updatePath } from "./lib/path.svelte"; |
| 11 | + import Link from "./lib/Link.svelte"; |
| 12 | + import { getStatus, setError, setLoading } from "./lib/status.svelte"; |
| 13 | + import Summary from "./views/Summary.svelte"; |
| 14 | + import Form from "./views/Form.svelte"; |
| 15 | +
|
| 16 | + let { loading, error } = $derived.by(getStatus); |
| 17 | + let view = $derived.by(() => parsePath().view); |
30 | 18 | let question = $state<Question | null>(null); |
31 | | - let answer = $state<Answer | null>(null); |
32 | | - let summary = $state<Summary | null>(null); |
33 | | -
|
34 | | - onMount(() => { |
35 | | - const initAnswer = async () => { |
36 | | - try { |
37 | | - // Wait until the server has started. |
38 | | - await waitUntilLive(); |
39 | | -
|
40 | | - // Parse the question key from URL path or URL query parameters. |
41 | | - const pathComponents = window.location.pathname.split("/").slice(1); |
42 | | - const query = new URLSearchParams(window.location.search); |
43 | | -
|
44 | | - const key = pathComponents[0] || query.get("key"); |
45 | | - // Replace path if query parameter is used. This is for backwards compatibility. |
46 | | - if (key && !pathComponents[0]) { |
47 | | - window.history.replaceState({}, "", `/${key}`); |
48 | | - } |
49 | | - if (!key) { |
50 | | - error = { |
51 | | - status: 404, |
52 | | - title: "Question not found", |
53 | | - }; |
54 | | - return; |
55 | | - } |
56 | | -
|
57 | | - view = pathComponents[1] || "form"; |
58 | | - if (["form", "share", "summary"].includes(view) === false) { |
59 | | - error = { |
60 | | - status: 404, |
61 | | - title: "Page not found", |
62 | | - }; |
63 | | - return; |
64 | | - } |
65 | | -
|
66 | | - // Fetch the question using the key. |
67 | | - const qr = await getQuestion(key); |
68 | | - if (qr.error) { |
69 | | - error = qr.error; |
70 | | - return; |
71 | | - } |
72 | | - question = qr.data; |
73 | | -
|
74 | | - // Initialize new answer or get existings answer. |
75 | | - const id = query.get("id"); |
76 | | - if (view === "form") { |
77 | | - const ar = id ? await getAnswer(id) : await createAnswer(key); |
78 | | - if (ar.error) { |
79 | | - error = ar.error; |
80 | | - return; |
81 | | - } |
82 | | - answer = ar.data; |
83 | | - window.history.replaceState({}, "", `/${key}?id=${answer.id}`); |
84 | | - } |
85 | | -
|
86 | | - // Fetch the summary. |
87 | | - if (view === "summary") { |
88 | | - fetchSummary(key); |
89 | | - } |
90 | | - } catch (err) { |
91 | | - error = { |
92 | | - status: 500, |
93 | | - title: "Failed to initialize feedback form.", |
94 | | - }; |
95 | | - } finally { |
96 | | - loading = false; |
97 | | - } |
98 | | - }; |
99 | | -
|
100 | | - window.addEventListener("popstate", (event) => { |
101 | | - initAnswer(); |
102 | | - }); |
103 | | -
|
104 | | - initAnswer(); |
105 | | - }); |
106 | | -
|
107 | | - const handleChange = async (payload: UpdateAnswerPayload) => { |
108 | | - if (!answer) { |
109 | | - return; |
110 | | - } |
111 | 19 |
|
| 20 | + const initializeQuestion = async () => { |
112 | 21 | try { |
113 | | - const ar = await updateAnswer(answer?.id, payload); |
114 | | - if (ar.error) { |
115 | | - error = ar.error; |
| 22 | + // Wait until the server has started. |
| 23 | + await waitUntilLive(); |
| 24 | +
|
| 25 | + const { key } = parsePath(); |
| 26 | + if (!key) { |
| 27 | + setError({ |
| 28 | + status: 404, |
| 29 | + title: "Question not found", |
| 30 | + }); |
116 | 31 | return; |
117 | 32 | } |
118 | | - answer = ar.data; |
119 | | - } catch (_) { |
120 | | - error = { |
121 | | - status: 500, |
122 | | - title: "Failed to update feedback.", |
123 | | - }; |
124 | | - } |
125 | | - }; |
126 | 33 |
|
127 | | - const fetchSummary = async (key?: string) => { |
128 | | - loading = true; |
129 | | - try { |
130 | | - const sr = await getSummary(key ?? ""); |
131 | | - if (sr.error) { |
132 | | - error = sr.error; |
| 34 | + // Fetch the question using the key. |
| 35 | + const qr = await getQuestion(key); |
| 36 | + if (qr.error) { |
| 37 | + setError(qr.error); |
133 | 38 | return; |
134 | 39 | } |
135 | | - summary = sr.data; |
136 | | - } catch (_) { |
137 | | - error = { |
| 40 | + question = qr.data; |
| 41 | + } catch (err) { |
| 42 | + setError({ |
138 | 43 | status: 500, |
139 | | - title: "Failed to fetch summary.", |
140 | | - }; |
| 44 | + title: "Failed to initialize feedback application.", |
| 45 | + }); |
141 | 46 | } finally { |
142 | | - loading = false; |
| 47 | + setLoading(false); |
143 | 48 | } |
144 | 49 | }; |
145 | 50 |
|
146 | | - const handleSubmit = async () => { |
147 | | - loading = true; |
148 | | - await handleChange({ submit: true }); |
149 | | - view = "summary"; |
150 | | - window.history.pushState({ id: answer?.id }, "", `/${answer?.key}/summary`); |
151 | | - await fetchSummary(answer?.key); |
152 | | - loading = false; |
153 | | - }; |
| 51 | + onMount(() => { |
| 52 | + initializePath(); |
| 53 | + initializeQuestion(); |
| 54 | +
|
| 55 | + window.addEventListener("popstate", () => { |
| 56 | + updatePath(); |
| 57 | + }); |
| 58 | + }); |
| 59 | +
|
| 60 | + $effect(() => { |
| 61 | + if (["form", "share", "summary"].includes(view) === false) { |
| 62 | + setError({ |
| 63 | + status: 404, |
| 64 | + title: "Page not found", |
| 65 | + }); |
| 66 | + return; |
| 67 | + } |
| 68 | + }); |
154 | 69 | </script> |
155 | 70 |
|
156 | 71 | <header> |
|
164 | 79 | <Loading /> |
165 | 80 | {:else if error} |
166 | 81 | <Error {error} /> |
167 | | - {:else if question && view === "form"} |
168 | | - <fieldset> |
169 | | - <legend>{question.choice_text}</legend> |
170 | | - <RadioGroup |
171 | | - items={question.choices} |
172 | | - name={question.type} |
173 | | - onChange={(value) => handleChange({ value })} |
174 | | - value={answer?.value} |
175 | | - /> |
176 | | - </fieldset> |
177 | | - {#if answer?.value != undefined} |
178 | | - {#if question?.with_comment} |
179 | | - <Comment |
180 | | - label={question?.comment_text} |
181 | | - onChange={(comment) => handleChange({ comment })} |
182 | | - value={answer?.comment} |
183 | | - /> |
184 | | - {/if} |
185 | | - <Submit onSubmit={handleSubmit} /> |
186 | | - {/if} |
187 | | - {:else if question && view === "summary" && summary} |
188 | | - <div class="summary"> |
189 | | - <p>Thank you for your feedback!</p> |
190 | | - <BarChart choices={question.choices} {summary} /> |
191 | | - </div> |
| 82 | + {/if} |
| 83 | + {#if question && view === "form"} |
| 84 | + <Form {question} /> |
| 85 | + {:else if question && view === "summary"} |
| 86 | + <Summary {question} /> |
192 | 87 | {:else if question && view === "share"} |
193 | 88 | <Share {question} /> |
194 | 89 | {/if} |
| 90 | + {#if question} |
| 91 | + <div class="links"> |
| 92 | + {#if view !== "form"} |
| 93 | + <Link target={`/${question.key}`}>Submit an answer</Link> |
| 94 | + {/if} |
| 95 | + {#if view !== "share"} |
| 96 | + <Link target={`/${question.key}/share`}>Share the URL</Link> |
| 97 | + {/if} |
| 98 | + {#if view !== "summary"} |
| 99 | + {#if view === "share"} |
| 100 | + <Link target={`/${question.key}/summary`}>Hide the QR code</Link> |
| 101 | + {:else} |
| 102 | + <Link target={`/${question.key}/summary`}>View answer summary</Link> |
| 103 | + {/if} |
| 104 | + {/if} |
| 105 | + </div> |
| 106 | + {/if} |
195 | 107 | </main> |
196 | 108 | <Footer /> |
197 | 109 |
|
|
228 | 140 | flex: 1; |
229 | 141 | } |
230 | 142 |
|
231 | | - fieldset { |
232 | | - appearance: none; |
233 | | - border: none; |
234 | | - margin: 0; |
235 | | - padding: 0; |
236 | | - } |
237 | | -
|
238 | | - legend { |
239 | | - appearance: none; |
240 | | - margin: 1rem 0; |
241 | | - padding: 0; |
| 143 | + .links { |
| 144 | + color: var(--color-secondary); |
| 145 | + display: flex; |
| 146 | + gap: 1rem; |
| 147 | + margin: 3rem 0 2rem; |
| 148 | + /* Disable margins from collapsing */ |
| 149 | + padding-top: 0.05px; |
242 | 150 | } |
243 | 151 | </style> |
0 commit comments