feat: french kid-friendly room entry
Replace the English Name + Room ID form with two big, no-keyboard
buttons aimed at a 6-year-old:
- "Construire tout seul" joins a unique solo room (solo-<random>).
- "Construire a plusieurs" joins a fixed shared room ("lisael") so two
kids who tap it land together without typing anything.
- An auto name ("Constructeur NNN") keeps self.name non-empty for the
people list.
- A small "Salle perso" link reveals an optional room field for adults.
Also francise the visible chrome: Creer / Modifier modes, the rotate
toggle, the empty-room people label, and the help dialog (which no
longer auto-opens so the builder is playable immediately).
This commit is contained in:
@@ -7,8 +7,10 @@ import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { Cross2Icon, InfoCircledIcon } from "@radix-ui/react-icons";
|
||||
import "./dialogbox.css";
|
||||
|
||||
// The help dialog stays CLOSED by default so the builder is immediately
|
||||
// playable for a child (no overlay to dismiss). The "?" button reopens it.
|
||||
export const DialogBox = () => (
|
||||
<Dialog.Root defaultOpen={true}>
|
||||
<Dialog.Root defaultOpen={false}>
|
||||
<Dialog.Trigger asChild>
|
||||
<button
|
||||
style={{
|
||||
@@ -19,6 +21,7 @@ export const DialogBox = () => (
|
||||
zIndex: "1000000000",
|
||||
}}
|
||||
className="Button violet"
|
||||
aria-label="Aide"
|
||||
>
|
||||
<InfoCircledIcon color="black" transform="scale(2)" />
|
||||
</button>
|
||||
@@ -26,45 +29,38 @@ export const DialogBox = () => (
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="DialogOverlay" />
|
||||
<Dialog.Content className="DialogContent">
|
||||
<Dialog.Title className="DialogTitle">Manual</Dialog.Title>
|
||||
<Dialog.Title className="DialogTitle">Comment jouer</Dialog.Title>
|
||||
<Dialog.Description className="DialogDescription" asChild>
|
||||
<ul>
|
||||
<li>Use left side panel to change the properties of brick</li>
|
||||
<li>
|
||||
You change the pivot point of bricks using{" "}
|
||||
Choisis une <strong>forme</strong> et une <strong>couleur</strong>{" "}
|
||||
en haut, puis clique pour poser une pièce.
|
||||
</li>
|
||||
<li>
|
||||
En haut, <strong>🧱 Créer</strong> pose des pièces,{" "}
|
||||
<strong>✋ Modifier</strong> sert à les choisir, les déplacer ou
|
||||
les effacer.
|
||||
</li>
|
||||
<li>
|
||||
Pour effacer : passe en <strong>✋ Modifier</strong>, clique une
|
||||
pièce (ou maintiens <span className="key">Maj</span> pour en
|
||||
choisir plusieurs) puis appuie sur{" "}
|
||||
<span className="key">Suppr</span>.
|
||||
</li>
|
||||
<li>
|
||||
Tu peux décaler le point de pose avec les touches{" "}
|
||||
<span className="key">A</span> <span className="key">S</span>{" "}
|
||||
<span className="key">W</span> and <span className="key">D</span>{" "}
|
||||
keys or You can use <strong>anchorX</strong> and{" "}
|
||||
<strong>anchorZ</strong> option from the control panel
|
||||
<span className="key">W</span> <span className="key">D</span>.
|
||||
</li>
|
||||
<li>
|
||||
To delete brick, select <strong>Edit</strong> option in control
|
||||
panel then select brick by clicking on brick or hold{" "}
|
||||
<span className="key">Shift</span> to select multiple bricks and
|
||||
use
|
||||
<span className="key">Delete</span> to delete selected bricks
|
||||
</li>
|
||||
<li>
|
||||
Top bar has two options. create Mode which let's you create
|
||||
new bricks while Edit mode let's you edit existing bricks by
|
||||
selecting
|
||||
</li>
|
||||
<li>
|
||||
You can also use <span className="key">CTRL</span> +{" "}
|
||||
<span className="key">Z</span> for undo,{" "}
|
||||
<span className="key">CTRL</span> + <span className="key">R</span>{" "}
|
||||
for redo.
|
||||
</li>
|
||||
<li>
|
||||
Bottom bar has (from left) first button to undo, second to redo,
|
||||
third to delete selected and forth button displays list of all
|
||||
users in room
|
||||
En bas : les deux flèches annulent ou refont, la poubelle efface
|
||||
la sélection, et le dernier bouton montre qui joue avec toi.
|
||||
</li>
|
||||
</ul>
|
||||
</Dialog.Description>
|
||||
|
||||
<Dialog.Close asChild>
|
||||
<button className="IconButton" aria-label="Close">
|
||||
<button className="IconButton" aria-label="Fermer">
|
||||
<Cross2Icon />
|
||||
</button>
|
||||
</Dialog.Close>
|
||||
|
||||
@@ -1,98 +1,74 @@
|
||||
/* eslint-disable react/prop-types */
|
||||
import * as Form from "@radix-ui/react-form";
|
||||
import "./styles.css";
|
||||
import { useRef } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useStore } from "../../../store";
|
||||
import { uID } from "../../../utils";
|
||||
|
||||
const JoinRoomForm = ({ setRoomId }) => {
|
||||
const form = useRef();
|
||||
// Fixed default room so two people who click "à plusieurs" land together
|
||||
// without typing anything.
|
||||
const SHARED_ROOM = "lisael";
|
||||
|
||||
const name = useRef();
|
||||
const room = useRef();
|
||||
function autoName() {
|
||||
return `Constructeur ${Math.floor(Math.random() * 900) + 100}`;
|
||||
}
|
||||
|
||||
export const JoinRoomScreen = ({ setRoomId }) => {
|
||||
const setSelfName = useStore((state) => state.setSelfName);
|
||||
const [showPerso, setShowPerso] = useState(false);
|
||||
const persoRoom = useRef("");
|
||||
|
||||
const submit = (e) => {
|
||||
e.preventDefault();
|
||||
setSelfName(name.current);
|
||||
setRoomId(room.current);
|
||||
const join = (roomId) => {
|
||||
setSelfName(autoName());
|
||||
setRoomId(roomId);
|
||||
};
|
||||
|
||||
const playSolo = () => join(`solo-${uID(6)}`);
|
||||
const playTogether = () => join(SHARED_ROOM);
|
||||
|
||||
const joinPerso = () => {
|
||||
const r = (persoRoom.current || "").trim();
|
||||
join(r ? `perso-${r}` : `solo-${uID(6)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Root ref={form} className="FormRoot" onSubmit={submit}>
|
||||
<Form.Field className="FormField" name="email">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="FormLabel">Name</Form.Label>
|
||||
<Form.Message className="FormMessage" match="valueMissing">
|
||||
Please enter your Name
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
className="Input"
|
||||
type="text"
|
||||
required
|
||||
onChange={(e) => {
|
||||
name.current = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Field className="FormField" name="question">
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Form.Label className="FormLabel">Room ID</Form.Label>
|
||||
<Form.Message className="FormMessage" match="valueMissing">
|
||||
Please enter a room ID
|
||||
</Form.Message>
|
||||
</div>
|
||||
<Form.Control asChild>
|
||||
<input
|
||||
className="Input"
|
||||
type="text"
|
||||
required
|
||||
onChange={(e) => {
|
||||
room.current = e.target.value;
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Field>
|
||||
<Form.Submit asChild>
|
||||
<button className="FormButton" style={{ marginTop: 10 }}>
|
||||
Enter In Room
|
||||
</button>
|
||||
</Form.Submit>
|
||||
</Form.Root>
|
||||
);
|
||||
};
|
||||
<div className="JoinScreen">
|
||||
<div className="JoinCard">
|
||||
<h1 className="JoinTitle">🧱 Blocs Pro</h1>
|
||||
<p className="JoinSubtitle">On construit ?</p>
|
||||
|
||||
export const JoinRoomScreen = ({ setRoomId }) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
boxSizing: "border-box",
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
background: "black",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<JoinRoomForm setRoomId={setRoomId} />
|
||||
<button className="BigButton solo" onClick={playSolo}>
|
||||
<span className="BigButtonEmoji">👷</span>
|
||||
Construire tout seul
|
||||
</button>
|
||||
|
||||
<button className="BigButton together" onClick={playTogether}>
|
||||
<span className="BigButtonEmoji">👫</span>
|
||||
Construire à plusieurs
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="PersoToggle"
|
||||
onClick={() => setShowPerso((v) => !v)}
|
||||
>
|
||||
{showPerso ? "▾ Cacher" : "▸ Salle perso (pour les grands)"}
|
||||
</button>
|
||||
|
||||
{showPerso && (
|
||||
<div className="PersoBox">
|
||||
<input
|
||||
className="PersoInput"
|
||||
type="text"
|
||||
placeholder="Nom de la salle"
|
||||
onChange={(e) => {
|
||||
persoRoom.current = e.target.value;
|
||||
}}
|
||||
/>
|
||||
<button className="PersoGo" onClick={joinPerso}>
|
||||
Entrer
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,95 +1,140 @@
|
||||
@import "@radix-ui/colors/black-alpha.css";
|
||||
@import "@radix-ui/colors/violet.css";
|
||||
@import "@radix-ui/colors/mauve.css";
|
||||
/* Kid-friendly, keyboard-free room entry screen. */
|
||||
|
||||
/* reset */
|
||||
input,
|
||||
textarea,
|
||||
button {
|
||||
.JoinScreen {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: linear-gradient(160deg, #2b6cff 0%, #7b3ff2 100%);
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.JoinCard {
|
||||
width: min(92vw, 420px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 32px 28px;
|
||||
border-radius: 28px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.JoinTitle {
|
||||
margin: 0;
|
||||
font-size: 2.2rem;
|
||||
font-weight: 800;
|
||||
color: #1b1b2b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.JoinSubtitle {
|
||||
margin: 0 0 6px;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
color: #6b6b80;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.BigButton {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.FormRoot {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.FormField {
|
||||
display: grid;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.FormLabel {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 35px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.FormMessage {
|
||||
font-size: 13px;
|
||||
color: white;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.Input,
|
||||
.Textarea {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
|
||||
font-size: 15px;
|
||||
color: white;
|
||||
background-color: gray;
|
||||
box-shadow: 0 0 0 1px var(--black-a9);
|
||||
}
|
||||
.Input:hover,
|
||||
.Textarea:hover {
|
||||
box-shadow: 0 0 0 1px white;
|
||||
}
|
||||
.Input:focus,
|
||||
.Textarea:focus {
|
||||
box-shadow: 0 0 0 2px white;
|
||||
}
|
||||
.Input::selection,
|
||||
.Textarea::selection {
|
||||
background-color: var(--black-a9);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.Input {
|
||||
padding: 0 10px;
|
||||
height: 35px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.Textarea {
|
||||
resize: none;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.FormButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
padding: 0 15px;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
height: 35px;
|
||||
width: 100%;
|
||||
|
||||
background-color: white;
|
||||
color: black;
|
||||
box-shadow: 0 2px 10px var(--black-a7);
|
||||
}
|
||||
.FormButton:hover {
|
||||
background-color: var(--mauve-3);
|
||||
gap: 6px;
|
||||
padding: 22px 18px;
|
||||
border-radius: 22px;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: transform 0.12s ease, box-shadow 0.12s ease;
|
||||
box-shadow: 0 8px 0 rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
.FormButton:focus {
|
||||
box-shadow: 0 0 0 2px black;
|
||||
|
||||
.BigButtonEmoji {
|
||||
font-size: 2.6rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.BigButton:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.BigButton:active {
|
||||
transform: translateY(4px);
|
||||
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.BigButton.solo {
|
||||
background: #ff8a18;
|
||||
}
|
||||
|
||||
.BigButton.together {
|
||||
background: #2ca84a;
|
||||
}
|
||||
|
||||
.PersoToggle {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
margin-top: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #9a9ab0;
|
||||
}
|
||||
|
||||
.PersoToggle:hover {
|
||||
color: #6b6b80;
|
||||
}
|
||||
|
||||
.PersoBox {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.PersoInput {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
color: #1b1b2b;
|
||||
background: #f0f0f6;
|
||||
box-shadow: inset 0 0 0 2px #d8d8e4;
|
||||
}
|
||||
|
||||
.PersoInput:focus {
|
||||
box-shadow: inset 0 0 0 2px #7b3ff2;
|
||||
}
|
||||
|
||||
.PersoGo {
|
||||
all: unset;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
padding: 0 18px;
|
||||
border-radius: 12px;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
background: #2b6cff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.PersoGo:hover {
|
||||
background: #1f57d6;
|
||||
}
|
||||
|
||||
@@ -18,18 +18,18 @@ export const ModeToggleBar = () => {
|
||||
<ToggleGroup.Item
|
||||
className="ToggleGroupItem"
|
||||
value={CREATE_MODE}
|
||||
aria-label={CREATE_MODE}
|
||||
aria-label="Créer"
|
||||
onClick={() => setMode(CREATE_MODE)}
|
||||
>
|
||||
{CREATE_MODE}
|
||||
🧱 Créer
|
||||
</ToggleGroup.Item>
|
||||
<ToggleGroup.Item
|
||||
className="ToggleGroupItem"
|
||||
value={EDIT_MODE}
|
||||
aria-label={EDIT_MODE}
|
||||
aria-label="Modifier"
|
||||
onClick={() => setMode(EDIT_MODE)}
|
||||
>
|
||||
{EDIT_MODE}
|
||||
✋ Modifier
|
||||
</ToggleGroup.Item>
|
||||
</ToggleGroup.Root>
|
||||
);
|
||||
|
||||
@@ -6,16 +6,9 @@ import { CheckIcon } from "@radix-ui/react-icons";
|
||||
import "./styles.css";
|
||||
import { useStore } from "../../../../store";
|
||||
|
||||
const stateMap = {
|
||||
rotate: "rotate",
|
||||
};
|
||||
|
||||
const setterMap = {
|
||||
rotate: "setRotate",
|
||||
};
|
||||
|
||||
export const Checkbox = ({ label = "rotate" }) => {
|
||||
const setValue = useStore((state) => state[setterMap[label]]);
|
||||
export const Checkbox = ({ label = "Tourner" }) => {
|
||||
// Single toggle: rotate the active piece 90° before placing.
|
||||
const setValue = useStore((state) => state.setRotate);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -2,18 +2,13 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React from "react";
|
||||
import "./styles.css";
|
||||
import { SliderWithLabel } from "./Slider";
|
||||
import { Checkbox } from "./Checkbox";
|
||||
import { ColorInput } from "./ColorInput";
|
||||
|
||||
export const Panel = () => {
|
||||
return (
|
||||
<div className="Panel">
|
||||
<SliderWithLabel label="width" max={5} />
|
||||
<SliderWithLabel label="depth" max={5} />
|
||||
<SliderWithLabel label="anchor X" min={-2} defaultValue={0} max={2} />
|
||||
<SliderWithLabel label="anchor Z" min={-2} defaultValue={0} max={2} />
|
||||
<Checkbox />
|
||||
<Checkbox label="Tourner" />
|
||||
<ColorInput />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@ export const PopoverPeopleList = () => {
|
||||
color: "rgba(0, 0, 0, 0.5)",
|
||||
}}
|
||||
>
|
||||
No one else in room
|
||||
Personne d'autre ici
|
||||
</h3>
|
||||
)}
|
||||
{others.map((user) => {
|
||||
|
||||
Reference in New Issue
Block a user