Skip to content

Commit 2378a66

Browse files
authored
Merge pull request #145 from geeksblabla/refactor/xstate-survey-machine
Refactor survey form to XState state machine architecture
2 parents 64692b4 + d2f5f73 commit 2378a66

19 files changed

+1911
-498
lines changed

CLAUDE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
66

77
StateOfDev.ma is an annual survey website for software developers in Morocco, created by the GeeksBlaBla Morocco Community. Built with Astro (v4), React, and Firebase, it collects anonymous responses and displays interactive results using charts and visualizations.
88

9+
## Rules
10+
11+
- In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision.
12+
13+
- At the end of each plan, give me a list of unresolved questions to answer, if any. Make the questions extremely concise. Sacrifice grammar for the sake of concision.
14+
15+
- DO NOT write tests unless explicitly requested
16+
17+
- DO NOT run dev server - assume already running
18+
19+
- Add code comments sparingly - focus on "why", not "what". Only add comments if the code is not self-explanatory.
20+
21+
- Your primary method for interacting with GitHub should be the GitHub CLI.
22+
923
## Development Commands
1024

1125
### Essential Commands

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"@types/js-yaml": "^4.0.9",
3434
"@types/react": "^18.3.4",
3535
"@types/react-dom": "^18.3.0",
36+
"@xstate/react": "^6.0.0",
3637
"astro": "5.14.5",
3738
"astro-icon": "^1.1.0",
3839
"class-variance-authority": "^0.7.0",
@@ -50,7 +51,8 @@
5051
"tailwind-merge": "^2.5.2",
5152
"tailwindcss": "^3.4.4",
5253
"tailwindcss-animate": "^1.0.7",
53-
"typescript": "^5.4.5"
54+
"typescript": "^5.4.5",
55+
"xstate": "^5.24.0"
5456
},
5557
"devDependencies": {
5658
"@rollup/plugin-yaml": "^4.1.2",

pnpm-lock.yaml

Lines changed: 39 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/survey/choice.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { type ChangeEvent } from "react";
2+
3+
type ChoiceProps = {
4+
text: string;
5+
id: string;
6+
index: number;
7+
name: string;
8+
required: boolean;
9+
multiple: boolean;
10+
checked: boolean;
11+
onChange: (index: number, checked: boolean) => void;
12+
};
13+
14+
export const Choice = ({
15+
text,
16+
id,
17+
index,
18+
name,
19+
multiple,
20+
checked,
21+
onChange
22+
}: ChoiceProps) => {
23+
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
24+
onChange(index, e.target.checked);
25+
};
26+
27+
return (
28+
<div className="relative w-full overflow-hidden flex items-center bg-card p-3 pl-14 mb-2 cursor-pointer transition-all duration-200 hover:translate-x-1">
29+
<input
30+
className="peer hidden"
31+
type={multiple ? "checkbox" : "radio"}
32+
id={id}
33+
name={name}
34+
value={index}
35+
checked={checked}
36+
onChange={handleChange}
37+
data-testid={id}
38+
/>
39+
<label
40+
className="absolute inset-0 cursor-pointer
41+
peer-checked:border-primary peer-checked:bg-gradient-to-r peer-checked:from-primary/15 peer-checked:to-primary/5
42+
peer-checked:ring-2 peer-checked:ring-primary/30 peer-checked:ring-offset-2 peer-checked:ring-offset-background
43+
border-2 border-input
44+
hover:border-primary/50
45+
transition-all duration-200"
46+
htmlFor={id}
47+
></label>
48+
<div
49+
className="absolute pointer-events-none left-4 h-5 w-5 border-2 border-input bg-muted
50+
peer-checked:border-primary peer-checked:bg-primary peer-checked:shadow-md peer-checked:shadow-primary/50
51+
transition-all duration-200"
52+
></div>
53+
<span
54+
className="pointer-events-none z-10 text-foreground
55+
peer-checked:font-medium
56+
transition-all duration-200"
57+
>
58+
{text}
59+
</span>
60+
</div>
61+
);
62+
};

src/components/survey/exit-popup.astro

Lines changed: 37 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@ import finImage from "@/assets/fin.jpeg";
1010
aria-labelledby="modalTitle"
1111
aria-modal="true"
1212
>
13-
<div class="bg-white border-4 border-gray-400 max-w-md w-full text-center">
13+
<div
14+
class="bg-card border-4 border-border max-w-md w-full text-center relative"
15+
>
1416
<span
1517
id="closeModal"
16-
class="absolute top-2 right-2 text-gray-500 cursor-pointer"
18+
class="absolute top-2 right-2 text-muted-foreground cursor-pointer hover:text-foreground transition-colors"
1719
aria-label="Close modal">&times;</span
1820
>
1921
<Image
2022
src={finImage}
2123
alt="Funny meme encouraging survey completion"
22-
class="mx-auto border-b-2 border-gray-300"
24+
class="mx-auto border-b-2 border-border"
2325
/>
2426
<button
2527
id="stayButton"
26-
class="bg-emerald-500 text-white font-bold px-4 py-4 w-full border-t-2 border-emerald-600 text-base"
28+
class="bg-primary text-primary-foreground font-bold px-4 py-4 w-full border-t-2 border-primary hover:opacity-90 transition-opacity text-base"
2729
>
2830
😂 مغادي فين، انا نكمل
2931
</button>
@@ -35,32 +37,39 @@ import finImage from "@/assets/fin.jpeg";
3537
let popupShownCount = 0;
3638
const maxPopupShows = 2;
3739

38-
if (body) {
39-
body.addEventListener("mouseleave", function (e: MouseEvent) {
40-
if (popupShownCount < maxPopupShows) {
41-
// Show the custom modal
42-
const modal = document.getElementById("exitModal") as HTMLDivElement;
43-
modal.classList.remove("hidden");
44-
popupShownCount++;
40+
const modal = document.getElementById("exitModal") as HTMLDivElement;
41+
const closeModal = document.getElementById("closeModal") as HTMLSpanElement;
42+
const stayButton = document.getElementById("stayButton") as HTMLButtonElement;
4543

46-
// Close the modal when the user clicks on the close button or stay button
47-
const closeModal = document.getElementById(
48-
"closeModal"
49-
) as HTMLSpanElement;
50-
const stayButton = document.getElementById(
51-
"stayButton"
52-
) as HTMLButtonElement;
53-
closeModal.onclick = stayButton.onclick = function () {
54-
modal.classList.add("hidden");
55-
};
44+
const closeModalHandler = () => {
45+
modal.classList.add("hidden");
46+
};
5647

57-
// Close the modal when the user clicks anywhere outside of the modal
58-
window.onclick = function (event: MouseEvent) {
59-
if (event.target == modal) {
60-
modal.classList.add("hidden");
61-
}
62-
};
63-
}
48+
const outsideClickHandler = (event: MouseEvent) => {
49+
if (event.target === modal) {
50+
modal.classList.add("hidden");
51+
}
52+
};
53+
54+
const mouseLeaveHandler = () => {
55+
if (popupShownCount < maxPopupShows) {
56+
modal.classList.remove("hidden");
57+
popupShownCount++;
58+
}
59+
};
60+
61+
if (body && modal && closeModal && stayButton) {
62+
body.addEventListener("mouseleave", mouseLeaveHandler);
63+
closeModal.addEventListener("click", closeModalHandler);
64+
stayButton.addEventListener("click", closeModalHandler);
65+
window.addEventListener("click", outsideClickHandler);
66+
67+
// Cleanup on page unload
68+
window.addEventListener("beforeunload", () => {
69+
body.removeEventListener("mouseleave", mouseLeaveHandler);
70+
closeModal.removeEventListener("click", closeModalHandler);
71+
stayButton.removeEventListener("click", closeModalHandler);
72+
window.removeEventListener("click", outsideClickHandler);
6473
});
6574
}
6675
</script>

src/components/survey/index.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import workQuestion from "@/survey/3-work.yml";
55
import aiQuestion from "@/survey/4-ai.yml";
66
import techQuestion from "@/survey/5-tech.yml";
77
import communityQuestion from "@/survey/6-community.yml";
8-
import { SurveyForm } from "./survey-form";
8+
import { SurveyApp } from "./survey-app";
99
import ExitPopup from "./exit-popup.astro";
1010
1111
const questions = [
@@ -19,7 +19,7 @@ const questions = [
1919
---
2020

2121
<div id="survey-form" class="min-h-screen min-w-full">
22-
<SurveyForm questions={questions} client:only="react" />
22+
<SurveyApp questions={questions} client:only="react" />
2323
</div>
2424

2525
<ExitPopup />

0 commit comments

Comments
 (0)