diff --git a/src/App.tsx b/src/App.tsx index 6013157..4aee506 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,15 +3,18 @@ import { X, PlusCircle, Share2, Redo2 } from "lucide-react"; import { QRCodeSVG } from "qrcode.react"; import ViewToggle from "./components/ViewToggle"; import { useTranslation } from "react-i18next"; -import Tooth from "./components/Tooth"; import { FEATURES } from "./config"; import LanguageButtons from "./components/LanguageButtons"; import Logo from "./components/Logo"; +import { TeethGrid } from "./components/TeethGrid"; +import { Elastic, ElasticPoint, TimeType } from "./types"; -const teethLayout = [ - [18, 17, 16, 15, 14, 13, 12, 11, 21, 22, 23, 24, 25, 26, 27, 28], - [48, 47, 46, 45, 44, 43, 42, 41, 31, 32, 33, 34, 35, 36, 37, 38], -]; +const teethLayout = { + topLeft: [18, 17, 16, 15, 14, 13, 12, 11], + topRight: [21, 22, 23, 24, 25, 26, 27, 28], + bottomLeft: [48, 47, 46, 45, 44, 43, 42, 41], + bottomRight: [31, 32, 33, 34, 35, 36, 37, 38], +} const ELASTIC_TYPES = [ { id: 0, name: "Rabbit", color: "#FF5555", thickness: 3, icon: "🐰" }, @@ -19,27 +22,6 @@ const ELASTIC_TYPES = [ { id: 2, name: "Fox", color: "#44DD44", thickness: 5, icon: "🦊" }, ]; -type TimeType = "a" | "d" | "n"; - -type Elastic = { - teeth: number[]; - type: number; - - /* - * Which time of day the elastic should be worn - * 24h: 24 hours a day - * daytime: only during the day - * nighttime: only during the night - * (emojis are used for display) - */ - time: TimeType; - - /* - * Placed on the outer side of the teeth or inner side - */ - outer?: boolean; -}; - // Create a mapping for the time options const TIME_OPTIONS: { [key: string]: string } = { a: "24h", @@ -55,7 +37,7 @@ const ElasticPlacer = () => { const { t } = useTranslation(); const [isMirrorView, setIsMirrorView] = useState(false); const [elastics, setElastics] = useState([]); - const [currentElastic, setCurrentElastic] = useState([]); + const [currentElastic, setCurrentElastic] = useState([]); const [currentElasticType, setCurrentElasticType] = useState( ELASTIC_TYPES[0].id ); @@ -77,8 +59,21 @@ const ElasticPlacer = () => { if (savedElastics) { try { let parsedElastics = JSON.parse(savedElastics); + + console.log("parsedElastics", parsedElastics); + // Convert old format (if needed) to numeric IDs: parsedElastics = parsedElastics.map((el: any) => { + + const teeth = el.t?.map((teeth: { t: number, o: boolean }) => { + return { + tooth: teeth.t, + outside: teeth.o, + } + }); + + let type = el.type; + // If el.type is a string name, convert to numeric id if (typeof el.type === "string") { const foundType = @@ -86,7 +81,11 @@ const ElasticPlacer = () => { ELASTIC_TYPES[0]; el.type = foundType.id; } - return el; + return { + ...el, + type, + teeth, + } }); setElastics(parsedElastics); } catch (error) { @@ -115,7 +114,16 @@ const ElasticPlacer = () => { if (initialLoadDone.current) { const params = new URLSearchParams(); if (elastics.length > 0) { - params.set("e", JSON.stringify(elastics)); + + // Convert elastics to compact format + const compactElastics = elastics.map((el) => { + return { + t: el.teeth.map((t) => ({ t: t.tooth, o: t.outside })), + type: el.type, + } + }); + + params.set("e", JSON.stringify(compactElastics)); } if (FEATURES.DISABLE_TEETH && disabledTeeth.length > 0) { params.set("t", JSON.stringify(disabledTeeth)); @@ -123,9 +131,8 @@ const ElasticPlacer = () => { if (FEATURES.MIRROR_VIEW) { params.set("m", isMirrorView ? "1" : "0"); } - const newUrl = `${window.location.origin}${window.location.pathname}${ - params.toString() ? "?" + params.toString() : "" - }`; + const newUrl = `${window.location.origin}${window.location.pathname}${params.toString() ? "?" + params.toString() : "" + }`; window.history.pushState({ path: newUrl }, "", newUrl); setShareUrl(newUrl); } @@ -142,13 +149,13 @@ const ElasticPlacer = () => { const toothAlreadyInElastic = useCallback( (toothNumber: number) => { - return elastics.some((elastic) => elastic.teeth.includes(toothNumber)); + return elastics.some((elastic) => elastic.teeth.some(t => t.tooth === toothNumber)); }, [elastics] ); const handleToothClick = useCallback( - (number: number) => { + (number: number, outside: boolean) => { if ( !FEATURES.ALLOW_MULTIPLE_ELASTICS_PER_TOOTH && toothAlreadyInElastic(number) @@ -157,11 +164,12 @@ const ElasticPlacer = () => { } if (!FEATURES.DISABLE_TEETH || !disabledTeeth.includes(number)) { - setCurrentElastic((prev) => - prev.includes(number) - ? prev.filter((n) => n !== number) - : [...prev, number] - ); + setCurrentElastic((prev) => { + const newElastic = { tooth: number, outside }; + return prev.some((t) => t.tooth === number) + ? prev.filter((t) => t.tooth !== number) + : [...prev, newElastic]; + }); } }, [disabledTeeth, toothAlreadyInElastic] @@ -174,7 +182,7 @@ const ElasticPlacer = () => { ? prev.filter((n) => n !== number) : [...prev, number] ); - setCurrentElastic((prev) => prev.filter((n) => n !== number)); + setCurrentElastic((prev) => prev.filter((n) => n.tooth !== number)); } }, []); @@ -208,8 +216,9 @@ const ElasticPlacer = () => { setShareUrl(window.location.origin + window.location.pathname); }, []); - const setToothRef = useCallback((number: number, ref: HTMLButtonElement) => { - toothRefs.current[number] = ref; + const setToothRef = useCallback((number: number, outside: boolean, ref: HTMLButtonElement) => { + const toothRefKey = outside ? number * -1 : number; + toothRefs.current[toothRefKey] = ref; }, []); const drawElastics = useCallback(() => { @@ -230,15 +239,18 @@ const ElasticPlacer = () => { ELASTIC_TYPES.find((et) => et.id === elastic.type) || ELASTIC_TYPES[0]; const svgRect = svgRef.current.getBoundingClientRect(); const points = elastic.teeth - .map((toothNumber) => { - const rect = toothRefs.current[toothNumber]?.getBoundingClientRect(); + .map((elasticPoint) => { + const toothRefKey = elasticPoint.outside ? elasticPoint.tooth * -1 : elasticPoint.tooth; + const rect = toothRefs.current[toothRefKey]?.getBoundingClientRect(); if (!rect) return null; const baseX = rect.left - svgRect.left + rect.width / 2; - const x = isMirrorView ? svgRect.width - baseX : baseX; + let x = isMirrorView ? svgRect.width - baseX : baseX; + let y = rect.top - svgRect.top + rect.height / 2; + return { x, - y: rect.top - svgRect.top + rect.height / 2, + y, }; }) .filter((point) => point !== null) as { x: number; y: number }[]; @@ -349,106 +361,89 @@ const ElasticPlacer = () => { return (
-
+

{t("title")}

- {/* View Toggle */} - {FEATURES.MIRROR_VIEW && ( - setIsMirrorView((prev) => !prev)} - /> - )} - - {/* Legend for Teeth */} - {(FEATURES.HIGHLIGHT_SPECIAL_TEETH || FEATURES.DISABLE_TEETH) && ( -
- {FEATURES.HIGHLIGHT_SPECIAL_TEETH && ( - <> +
+ + {/* View Toggle */} + {FEATURES.MIRROR_VIEW && ( + setIsMirrorView((prev) => !prev)} + /> + )} + + {/* Legend for Teeth */} + {(FEATURES.HIGHLIGHT_SPECIAL_TEETH || FEATURES.DISABLE_TEETH) && ( +
+ {FEATURES.HIGHLIGHT_SPECIAL_TEETH && ( + <> +
+
+ {t("legend.middleIncisors")} +
+
+
+ {t("legend.canines")} +
+ + )} + {FEATURES.DISABLE_TEETH && (
-
- {t("legend.middleIncisors")} +
+ {t("legend.disabledTeeth")}
-
-
- {t("legend.canines")} + )} +
+ )} + + {/* Legend for Elastics */} +
+
+ {ELASTIC_TYPES.map((etype) => ( +
+ + + + {etype.icon} + + {t("legend.elasticType", { type: etype.name })} +
- - )} - {FEATURES.DISABLE_TEETH && ( -
-
- {t("legend.disabledTeeth")} -
- )} + ))} +
- )} - {/* Legend for Elastics */} -
-
- {ELASTIC_TYPES.map((etype) => ( -
- - - - {etype.icon} - - {t("legend.elasticType", { type: etype.name })} - -
- ))} -
-
-
- - {teethLayout.map((row, rowIndex) => ( -
- {row.map((tooth) => ( - - ))} -
- ))} -
-
+ {/* Teeth Grid */} +
@@ -465,11 +460,10 @@ const ElasticPlacer = () => { : { borderColor: etype.color } } className={`px-4 py-2 rounded border - ${ - currentElasticType === etype.id - ? "text-white border-black" - : "bg-white text-black" - }`} + ${currentElasticType === etype.id + ? "text-white border-black" + : "bg-white text-black" + }`} title={t("elastics.typeOption", { type: etype.name })} > {etype.icon && ( @@ -491,11 +485,10 @@ const ElasticPlacer = () => { @@ -505,9 +498,8 @@ const ElasticPlacer = () => { + + {/* outer half */} +
); }); diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..1186949 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,25 @@ +export type TimeType = "a" | "d" | "n"; + +export type ElasticPoint = { + tooth: number; + outside: boolean; +} + +export type Elastic = { + teeth: ElasticPoint[]; + type: number; + + /* + * Which time of day the elastic should be worn + * 24h: 24 hours a day + * daytime: only during the day + * nighttime: only during the night + * (emojis are used for display) + */ + time: TimeType; + + /* + * Placed on the outer side of the teeth or inner side + */ + outer?: boolean; +}; \ No newline at end of file diff --git a/src/util/class-names.ts b/src/util/class-names.ts new file mode 100644 index 0000000..16153de --- /dev/null +++ b/src/util/class-names.ts @@ -0,0 +1,3 @@ +export function classNames(...classes: (string | boolean | undefined)[]): string { + return classes.filter(Boolean).join(' ') +} \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js index 99d31a5..e8c6456 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -9,6 +9,14 @@ export default { fontFamily: { sans: ["Montserrat", "Open Sans", "sans-serif"], }, + spacing: { + 21: '5.25rem', + 22: '5.5rem', + 34: '8.5rem', + 35: '8.75rem', + 37: '9.25rem', + 38: '9.5rem', + }, }, }, plugins: [],