Export, Import, Remove, Duplicate and preview tactics #120

Merged
maxime.batista merged 5 commits from home/tactic-management into master 1 year ago

3
.gitignore vendored

@ -23,3 +23,6 @@ dist-ssr
*.sw?
package-lock.json
stats.html

@ -12,6 +12,7 @@
"eslint-plugin-react-refresh": "^0.4.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-drag-drop-files": "^2.3.10",
"react-draggable": "^4.4.6",
"react-router-dom": "^6.22.0",
"typescript": "^5.2.2",
@ -34,6 +35,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^24.0.0",
"prettier": "^3.1.0",
"rollup-plugin-visualizer": "^5.12.0",
"typescript": "^5.2.2",
"vite-plugin-svgr": "^4.1.0",
"vitest": "^1.3.1"

@ -23,7 +23,6 @@ import {
import { BASE } from "./Constants.ts"
import { Authentication, Fetcher } from "./app/Fetcher.ts"
import { User } from "./model/User.ts"
import { VisualizerPage } from "./pages/VisualizerPage.tsx"
const HomePage = lazy(() => import("./pages/HomePage.tsx"))
const LoginPage = lazy(() => import("./pages/LoginPage.tsx"))
@ -33,6 +32,7 @@ const CreateTeamPage = lazy(() => import("./pages/CreateTeamPage.tsx"))
const TeamPanelPage = lazy(() => import("./pages/TeamPanel.tsx"))
const NewTacticPage = lazy(() => import("./pages/NewTacticPage.tsx"))
const Editor = lazy(() => import("./pages/Editor.tsx"))
const VisualizerPage = lazy(() => import("./pages/VisualizerPage.tsx"))
const Settings = lazy(() => import("./pages/Settings.tsx"))
const TOKEN_REFRESH_INTERVAL_MS = 60 * 1000
@ -148,7 +148,6 @@ export default function App() {
element={suspense(
<LoggedInPage>
<VisualizerPage guestMode={false} />
,
</LoggedInPage>,
)}
/>

@ -1,77 +1,77 @@
<svg width="567" height="269" viewBox="0 0 567 269" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="73" y1="24" x2="495" y2="24" stroke="black" stroke-width="2"/>
<line x1="494" y1="23" x2="494" y2="247" stroke="black" stroke-width="2"/>
<line x1="495" y1="248" x2="73" y2="248" stroke="black" stroke-width="2"/>
<line x1="72" y1="249" x2="72" y2="23" stroke="black" stroke-width="2"/>
<line x1="283.5" y1="23" x2="283.5" y2="247" stroke="black"/>
<g filter="url(#filter0_i_3_2)">
<circle cx="283.5" cy="135.5" r="27" stroke="black"/>
<svg width="424" height="226" viewBox="0 0 424 226" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 1H424" stroke="black" stroke-width="2"/>
<path d="M423 0V224" stroke="black" stroke-width="2"/>
<path d="M424 225H2" stroke="black" stroke-width="2"/>
<path d="M1 226V0" stroke="black" stroke-width="2"/>
<path d="M212.5 0V224" stroke="black"/>
<g filter="url(#filter0_i_2_68)">
<path d="M212.5 139.5C227.412 139.5 239.5 127.412 239.5 112.5C239.5 97.5883 227.412 85.5 212.5 85.5C197.588 85.5 185.5 97.5883 185.5 112.5C185.5 127.412 197.588 139.5 212.5 139.5Z" stroke="black"/>
</g>
<line x1="73" y1="99.5" x2="158" y2="99.5" stroke="black"/>
<line x1="73" y1="100.5" x2="158" y2="100.5" stroke="black"/>
<path d="M158.5 172V100" stroke="black"/>
<path d="M158 99.5H159" stroke="black"/>
<path d="M158 172.5H159" stroke="black"/>
<line x1="158" y1="172.5" x2="73" y2="172.5" stroke="black"/>
<line x1="158" y1="171.5" x2="73" y2="171.5" stroke="black"/>
<line x1="73" y1="37.5" x2="139" y2="37.5" stroke="black"/>
<line x1="73" y1="233.5" x2="139" y2="233.5" stroke="black"/>
<g filter="url(#filter1_i_3_2)">
<path d="M158.5 163C161.98 163 165.426 162.315 168.641 160.983C171.856 159.651 174.778 157.699 177.238 155.238C179.699 152.778 181.651 149.856 182.983 146.641C184.315 143.426 185 139.98 185 136.5C185 133.02 184.315 129.574 182.983 126.359C181.651 123.144 179.699 120.222 177.238 117.762C174.778 115.301 171.856 113.349 168.641 112.017C165.426 110.685 161.98 110 158.5 110L158.5 136.5L158.5 163Z" stroke="black"/>
<path d="M2 76.5H87" stroke="black"/>
<path d="M2 77.5H87" stroke="black"/>
<path d="M87.5 149V77" stroke="black"/>
<path d="M87 76.5H88" stroke="black"/>
<path d="M87 149.5H88" stroke="black"/>
<path d="M87 149.5H2" stroke="black"/>
<path d="M87 148.5H2" stroke="black"/>
<path d="M2 14.5H68" stroke="black"/>
<path d="M2 210.5H68" stroke="black"/>
<g filter="url(#filter1_i_2_68)">
<path d="M87.5 140C90.98 140 94.426 139.315 97.641 137.983C100.856 136.651 103.778 134.699 106.238 132.238C108.699 129.778 110.651 126.856 111.983 123.641C113.315 120.426 114 116.98 114 113.5C114 110.02 113.315 106.574 111.983 103.359C110.651 100.144 108.699 97.222 106.238 94.762C103.778 92.301 100.856 90.349 97.641 89.017C94.426 87.685 90.98 87 87.5 87V113.5V140Z" stroke="black"/>
</g>
<g filter="url(#filter2_i_3_2)">
<path d="M158.5 110C155.02 110 151.574 110.685 148.359 112.017C145.144 113.349 142.222 115.301 139.762 117.762C137.301 120.222 135.349 123.144 134.017 126.359C132.685 129.574 132 133.02 132 136.5C132 139.98 132.685 143.426 134.017 146.641C135.349 149.856 137.301 152.778 139.762 155.238C142.222 157.699 145.144 159.651 148.359 160.983C151.574 162.315 155.02 163 158.5 163" stroke="black" stroke-dasharray="4 4"/>
<g filter="url(#filter2_i_2_68)">
<path d="M87.5 87C84.02 87 80.574 87.685 77.359 89.017C74.144 90.349 71.222 92.301 68.762 94.762C66.301 97.222 64.349 100.144 63.017 103.359C61.685 106.574 61 110.02 61 113.5C61 116.98 61.685 120.426 63.017 123.641C64.349 126.856 66.301 129.778 68.762 132.238C71.222 134.699 74.144 136.651 77.359 137.983C80.574 139.315 84.02 140 87.5 140" stroke="black" stroke-dasharray="4 4"/>
</g>
<line x1="135.5" y1="177" x2="135.5" y2="172" stroke="black"/>
<line x1="123.5" y1="177" x2="123.5" y2="172" stroke="black"/>
<line x1="111.5" y1="177" x2="111.5" y2="172" stroke="black"/>
<line x1="99.5" y1="177" x2="99.5" y2="172" stroke="black"/>
<line x1="135.5" y1="100" x2="135.5" y2="95" stroke="black"/>
<line x1="123.5" y1="100" x2="123.5" y2="95" stroke="black"/>
<line x1="111.5" y1="100" x2="111.5" y2="95" stroke="black"/>
<line x1="99.5" y1="100" x2="99.5" y2="95" stroke="black"/>
<path d="M140.212 233.607C159.054 225.612 175.149 211.967 186.427 194.431C197.705 176.895 203.649 156.271 203.497 135.213C203.345 114.155 197.104 93.6242 185.574 76.2645C174.045 58.9047 157.755 45.5096 138.799 37.8066" stroke="black"/>
<path d="M140 233.5H141" stroke="black"/>
<path d="M139 233.5H140" stroke="black"/>
<path d="M90.5 118.5C95.0041 118.5 99.3266 120.34 102.516 123.621C105.706 126.901 107.5 131.354 107.5 136C107.5 140.646 105.706 145.099 102.516 148.379C99.3266 151.66 95.0041 153.5 90.5 153.5" stroke="black"/>
<circle cx="87.5" cy="136.5" r="3" stroke="black"/>
<line x1="83.5" y1="149" x2="83.5" y2="123" stroke="black"/>
<line y1="-0.5" x2="85" y2="-0.5" transform="matrix(-1 0 0 1 494 100)" stroke="black"/>
<line y1="-0.5" x2="85" y2="-0.5" transform="matrix(-1 0 0 1 494 101)" stroke="black"/>
<path d="M408.5 172V100" stroke="black"/>
<path d="M409 99.5H408" stroke="black"/>
<path d="M409 172.5H408" stroke="black"/>
<line y1="-0.5" x2="85" y2="-0.5" transform="matrix(1 0 0 -1 409 172)" stroke="black"/>
<line y1="-0.5" x2="85" y2="-0.5" transform="matrix(1 0 0 -1 409 171)" stroke="black"/>
<line y1="-0.5" x2="66" y2="-0.5" transform="matrix(-1 0 0 1 494 38)" stroke="black"/>
<line y1="-0.5" x2="66" y2="-0.5" transform="matrix(-1 0 0 1 494 234)" stroke="black"/>
<g filter="url(#filter3_i_3_2)">
<path d="M408.5 163C405.02 163 401.574 162.315 398.359 160.983C395.144 159.651 392.222 157.699 389.762 155.238C387.301 152.778 385.349 149.856 384.017 146.641C382.685 143.426 382 139.98 382 136.5C382 133.02 382.685 129.574 384.017 126.359C385.349 123.144 387.301 120.222 389.762 117.762C392.222 115.301 395.144 113.349 398.359 112.017C401.574 110.685 405.02 110 408.5 110L408.5 136.5L408.5 163Z" stroke="black"/>
<path d="M64.5 154V149" stroke="black"/>
<path d="M52.5 154V149" stroke="black"/>
<path d="M40.5 154V149" stroke="black"/>
<path d="M28.5 154V149" stroke="black"/>
<path d="M64.5 77V72" stroke="black"/>
<path d="M52.5 77V72" stroke="black"/>
<path d="M40.5 77V72" stroke="black"/>
<path d="M28.5 77V72" stroke="black"/>
<path d="M69.212 210.607C88.054 202.612 104.149 188.967 115.427 171.431C126.705 153.895 132.649 133.271 132.497 112.213C132.345 91.155 126.104 70.6242 114.574 53.2645C103.045 35.9047 86.755 22.5096 67.799 14.8066" stroke="black"/>
<path d="M69 210.5H70" stroke="black"/>
<path d="M68 210.5H69" stroke="black"/>
<path d="M19.5 95.5C24.0041 95.5 28.3266 97.34 31.516 100.621C34.706 103.901 36.5 108.354 36.5 113C36.5 117.646 34.706 122.099 31.516 125.379C28.3266 128.66 24.0041 130.5 19.5 130.5" stroke="black"/>
<path d="M16.5 116.5C18.1569 116.5 19.5 115.157 19.5 113.5C19.5 111.843 18.1569 110.5 16.5 110.5C14.8431 110.5 13.5 111.843 13.5 113.5C13.5 115.157 14.8431 116.5 16.5 116.5Z" stroke="black"/>
<path d="M12.5 126V100" stroke="black"/>
<path d="M423 76.5H338" stroke="black"/>
<path d="M423 77.5H338" stroke="black"/>
<path d="M337.5 149V77" stroke="black"/>
<path d="M338 76.5H337" stroke="black"/>
<path d="M338 149.5H337" stroke="black"/>
<path d="M338 149.5H423" stroke="black"/>
<path d="M338 148.5H423" stroke="black"/>
<path d="M423 14.5H357" stroke="black"/>
<path d="M423 210.5H357" stroke="black"/>
<g filter="url(#filter3_i_2_68)">
<path d="M337.5 140C334.02 140 330.574 139.315 327.359 137.983C324.144 136.651 321.222 134.699 318.762 132.238C316.301 129.778 314.349 126.856 313.017 123.641C311.685 120.426 311 116.98 311 113.5C311 110.02 311.685 106.574 313.017 103.359C314.349 100.144 316.301 97.222 318.762 94.762C321.222 92.301 324.144 90.349 327.359 89.017C330.574 87.685 334.02 87 337.5 87V113.5V140Z" stroke="black"/>
</g>
<g filter="url(#filter4_i_3_2)">
<path d="M408.5 110C411.98 110 415.426 110.685 418.641 112.017C421.856 113.349 424.778 115.301 427.238 117.762C429.699 120.222 431.651 123.144 432.983 126.359C434.315 129.574 435 133.02 435 136.5C435 139.98 434.315 143.426 432.983 146.641C431.651 149.856 429.699 152.778 427.238 155.238C424.778 157.699 421.856 159.651 418.641 160.983C415.426 162.315 411.98 163 408.5 163" stroke="black" stroke-dasharray="4 4"/>
<g filter="url(#filter4_i_2_68)">
<path d="M337.5 87C340.98 87 344.426 87.685 347.641 89.017C350.856 90.349 353.778 92.301 356.238 94.762C358.699 97.222 360.651 100.144 361.983 103.359C363.315 106.574 364 110.02 364 113.5C364 116.98 363.315 120.426 361.983 123.641C360.651 126.856 358.699 129.778 356.238 132.238C353.778 134.699 350.856 136.651 347.641 137.983C344.426 139.315 340.98 140 337.5 140" stroke="black" stroke-dasharray="4 4"/>
</g>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 431 177)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 443 177)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 455 177)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 467 177)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 431 100)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 443 100)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 455 100)" stroke="black"/>
<line y1="-0.5" x2="5" y2="-0.5" transform="matrix(4.37114e-08 -1 -1 -4.37114e-08 467 100)" stroke="black"/>
<path d="M426.788 233.607C407.946 225.612 391.851 211.967 380.573 194.431C369.295 176.895 363.351 156.271 363.503 135.213C363.655 114.155 369.896 93.6242 381.426 76.2645C392.955 58.9047 409.245 45.5096 428.201 37.8066" stroke="black"/>
<path d="M427 233.5H426" stroke="black"/>
<path d="M428 233.5H427" stroke="black"/>
<g filter="url(#filter5_i_3_2)">
<path d="M476.5 118.5C471.996 118.5 467.673 120.34 464.484 123.621C461.294 126.901 459.5 131.354 459.5 136C459.5 140.646 461.294 145.099 464.484 148.379C467.673 151.66 471.996 153.5 476.5 153.5" stroke="black"/>
<path d="M360.5 154L360.5 149" stroke="black"/>
<path d="M372.5 154L372.5 149" stroke="black"/>
<path d="M384.5 154L384.5 149" stroke="black"/>
<path d="M396.5 154L396.5 149" stroke="black"/>
<path d="M360.5 77L360.5 72" stroke="black"/>
<path d="M372.5 77L372.5 72" stroke="black"/>
<path d="M384.5 77L384.5 72" stroke="black"/>
<path d="M396.5 77L396.5 72" stroke="black"/>
<path d="M355.788 210.607C336.946 202.612 320.851 188.967 309.573 171.431C298.295 153.895 292.351 133.271 292.503 112.213C292.655 91.155 298.896 70.6242 310.426 53.2645C321.955 35.9047 338.245 22.5096 357.201 14.8066" stroke="black"/>
<path d="M356 210.5H355" stroke="black"/>
<path d="M357 210.5H356" stroke="black"/>
<g filter="url(#filter5_i_2_68)">
<path d="M405.5 95.5C400.996 95.5 396.673 97.34 393.484 100.621C390.294 103.901 388.5 108.354 388.5 113C388.5 117.646 390.294 122.099 393.484 125.379C396.673 128.66 400.996 130.5 405.5 130.5" stroke="black"/>
</g>
<circle cx="3.5" cy="3.5" r="3" transform="matrix(-1 0 0 1 483 133)" stroke="black"/>
<line y1="-0.5" x2="26" y2="-0.5" transform="matrix(0 -1 -1 0 483 149)" stroke="black"/>
<path d="M138 37.5H139" stroke="black"/>
<path d="M139.225 38.1519C139.221 38.1187 139.099 38.1364 139.073 38.135C138.98 38.1302 138.889 38.0674 138.794 38.0674C138.755 38.0674 138.672 38.0388 138.633 38.0252C138.581 38.0065 138.52 37.9719 138.465 37.9661C138.4 37.9593 138.366 37.9109 138.312 37.8854C138.275 37.8677 138.202 37.8366 138.177 37.8056C138.161 37.7847 138.063 37.7566 138.033 37.7371C137.962 37.6922 137.879 37.6309 137.806 37.5944" stroke="black" stroke-width="0.5"/>
<path d="M139.926 37.9883C139.906 37.9888 139.887 37.9922 139.867 37.9922C139.838 37.9922 139.812 37.978 139.787 37.9622C139.682 37.8946 139.599 37.7958 139.504 37.7151C139.388 37.6172 139.275 37.5101 139.135 37.4512C139.051 37.4159 138.965 37.3877 138.879 37.3594C138.85 37.3498 138.785 37.3113 138.754 37.332" stroke="black" stroke-width="0.5"/>
<path d="M408.5 116.5C406.843 116.5 405.5 115.157 405.5 113.5C405.5 111.843 406.843 110.5 408.5 110.5C410.157 110.5 411.5 111.843 411.5 113.5C411.5 115.157 410.157 116.5 408.5 116.5Z" stroke="black"/>
<path d="M412.5 126V100" stroke="black"/>
<path d="M67 14.5H68" stroke="black"/>
<path d="M68.225 15.1519C68.221 15.1187 68.099 15.1364 68.073 15.135C67.98 15.1302 67.889 15.0674 67.794 15.0674C67.755 15.0674 67.672 15.0388 67.633 15.0252C67.581 15.0065 67.52 14.9719 67.465 14.9661C67.4 14.9593 67.366 14.9109 67.312 14.8854C67.275 14.8677 67.202 14.8366 67.177 14.8056C67.161 14.7847 67.063 14.7566 67.033 14.7371C66.962 14.6922 66.879 14.6309 66.806 14.5944" stroke="black" stroke-width="0.5"/>
<path d="M68.926 14.9883C68.906 14.9888 68.887 14.9922 68.867 14.9922C68.838 14.9922 68.812 14.978 68.787 14.9622C68.682 14.8946 68.599 14.7958 68.504 14.7151C68.388 14.6172 68.275 14.5101 68.135 14.4512C68.051 14.4159 67.965 14.3877 67.879 14.3594C67.85 14.3498 67.785 14.3113 67.754 14.332" stroke="black" stroke-width="0.5"/>
<defs>
<filter id="filter0_i_3_2" x="256" y="108" width="55" height="59" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter0_i_2_68" x="185" y="85" width="55" height="59" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -79,9 +79,9 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2_68"/>
</filter>
<filter id="filter1_i_3_2" x="158" y="109.5" width="27.5" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter1_i_2_68" x="87" y="86.5" width="27.5" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -89,9 +89,9 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2_68"/>
</filter>
<filter id="filter2_i_3_2" x="131.5" y="109.5" width="27" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter2_i_2_68" x="60.5" y="86.5" width="27" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -99,9 +99,9 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2_68"/>
</filter>
<filter id="filter3_i_3_2" x="381.5" y="109.5" width="27.5" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter3_i_2_68" x="310.5" y="86.5" width="27.5" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -109,9 +109,9 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2_68"/>
</filter>
<filter id="filter4_i_3_2" x="408.5" y="109.5" width="27" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter4_i_2_68" x="337.5" y="86.5" width="27" height="58" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -119,9 +119,9 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2_68"/>
</filter>
<filter id="filter5_i_3_2" x="459" y="118" width="17.5" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter5_i_2_68" x="388" y="95" width="17.5" height="40" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -129,7 +129,7 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_2"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2_68"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

@ -1,45 +1,45 @@
<svg width="269" height="309" viewBox="0 0 269 309" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="24" y1="236" x2="24" y2="26" stroke="black" stroke-width="2"/>
<line x1="248" y1="25" x2="248" y2="236" stroke="black" stroke-width="2"/>
<line x1="249" y1="237" x2="23" y2="237" stroke="black" stroke-width="2"/>
<line x1="23" y1="25.5" x2="247" y2="25.5" stroke="black"/>
<g filter="url(#filter0_i_3_4)">
<path d="M108.5 26C108.5 33.0313 111.347 39.7727 116.411 44.7417C121.476 49.7103 128.342 52.5 135.5 52.5C142.658 52.5 149.524 49.7103 154.588 44.7417C159.653 39.7727 162.5 33.0313 162.5 26" stroke="black"/>
<svg width="226" height="213" viewBox="0 0 226 213" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 211V1" stroke="black" stroke-width="2"/>
<path d="M225 0V211" stroke="black" stroke-width="2"/>
<path d="M226 212H0" stroke="black" stroke-width="2"/>
<path d="M0 0.5H224" stroke="black"/>
<g filter="url(#filter0_i_2_106)">
<path d="M85.5 1C85.5 8.0313 88.347 14.7727 93.411 19.7417C98.476 24.7103 105.342 27.5 112.5 27.5C119.658 27.5 126.524 24.7103 131.588 19.7417C136.653 14.7727 139.5 8.0313 139.5 1" stroke="black"/>
</g>
<line x1="99.5" y1="236" x2="99.5" y2="151" stroke="black"/>
<line x1="100.5" y1="236" x2="100.5" y2="151" stroke="black"/>
<path d="M172 150.5H100" stroke="black"/>
<path d="M99.5 151V150" stroke="black"/>
<path d="M172.5 151V150" stroke="black"/>
<line x1="172.5" y1="151" x2="172.5" y2="236" stroke="black"/>
<line x1="171.5" y1="151" x2="171.5" y2="236" stroke="black"/>
<line x1="37.5" y1="236" x2="37.5" y2="170" stroke="black"/>
<line x1="233.5" y1="236" x2="233.5" y2="170" stroke="black"/>
<g filter="url(#filter1_i_3_4)">
<path d="M163 150.5C163 147.02 162.315 143.574 160.983 140.359C159.651 137.144 157.699 134.222 155.238 131.762C152.778 129.301 149.856 127.349 146.641 126.017C143.426 124.685 139.98 124 136.5 124C133.02 124 129.574 124.685 126.359 126.017C123.144 127.349 120.222 129.301 117.762 131.762C115.301 134.222 113.349 137.144 112.017 140.359C110.685 143.574 110 147.02 110 150.5L136.5 150.5L163 150.5Z" stroke="black"/>
<path d="M76.5 211V126" stroke="black"/>
<path d="M77.5 211V126" stroke="black"/>
<path d="M149 125.5H77" stroke="black"/>
<path d="M76.5 126V125" stroke="black"/>
<path d="M149.5 126V125" stroke="black"/>
<path d="M149.5 126V211" stroke="black"/>
<path d="M148.5 126V211" stroke="black"/>
<path d="M14.5 211V145" stroke="black"/>
<path d="M210.5 211V145" stroke="black"/>
<g filter="url(#filter1_i_2_106)">
<path d="M140 125.5C140 122.02 139.315 118.574 137.983 115.359C136.651 112.144 134.699 109.222 132.238 106.762C129.778 104.301 126.856 102.349 123.641 101.017C120.426 99.685 116.98 99 113.5 99C110.02 99 106.574 99.685 103.359 101.017C100.144 102.349 97.222 104.301 94.762 106.762C92.301 109.222 90.349 112.144 89.017 115.359C87.685 118.574 87 122.02 87 125.5H113.5H140Z" stroke="black"/>
</g>
<g filter="url(#filter2_i_3_4)">
<path d="M110 150.5C110 153.98 110.685 157.426 112.017 160.641C113.349 163.856 115.301 166.778 117.762 169.238C120.222 171.699 123.144 173.651 126.359 174.983C129.574 176.315 133.02 177 136.5 177C139.98 177 143.426 176.315 146.641 174.983C149.856 173.651 152.778 171.699 155.238 169.238C157.699 166.778 159.651 163.856 160.983 160.641C162.315 157.426 163 153.98 163 150.5" stroke="black" stroke-dasharray="4 4"/>
<g filter="url(#filter2_i_2_106)">
<path d="M87 125.5C87 128.98 87.685 132.426 89.017 135.641C90.349 138.856 92.301 141.778 94.762 144.238C97.222 146.699 100.144 148.651 103.359 149.983C106.574 151.315 110.02 152 113.5 152C116.98 152 120.426 151.315 123.641 149.983C126.856 148.651 129.778 146.699 132.238 144.238C134.699 141.778 136.651 138.856 137.983 135.641C139.315 132.426 140 128.98 140 125.5" stroke="black" stroke-dasharray="4 4"/>
</g>
<line x1="177" y1="173.5" x2="172" y2="173.5" stroke="black"/>
<line x1="177" y1="185.5" x2="172" y2="185.5" stroke="black"/>
<line x1="177" y1="197.5" x2="172" y2="197.5" stroke="black"/>
<line x1="177" y1="209.5" x2="172" y2="209.5" stroke="black"/>
<line x1="100" y1="173.5" x2="95" y2="173.5" stroke="black"/>
<line x1="100" y1="185.5" x2="95" y2="185.5" stroke="black"/>
<line x1="100" y1="197.5" x2="95" y2="197.5" stroke="black"/>
<line x1="100" y1="209.5" x2="95" y2="209.5" stroke="black"/>
<path d="M233.607 168.788C225.612 149.946 211.967 133.851 194.431 122.573C176.895 111.295 156.271 105.351 135.213 105.503C114.155 105.655 93.6242 111.896 76.2645 123.426C58.9047 134.955 45.5096 151.245 37.8066 170.201" stroke="black"/>
<path d="M233.5 169V168" stroke="black"/>
<path d="M233.5 170V169" stroke="black"/>
<path d="M118.5 218.5C118.5 213.996 120.34 209.673 123.621 206.484C126.901 203.294 131.354 201.5 136 201.5C140.646 201.5 145.099 203.294 148.379 206.484C151.66 209.673 153.5 213.996 153.5 218.5" stroke="black"/>
<circle cx="136.5" cy="221.5" r="3" transform="rotate(-90 136.5 221.5)" stroke="black"/>
<line x1="149" y1="225.5" x2="123" y2="225.5" stroke="black"/>
<path d="M37.5 171V170" stroke="black"/>
<path d="M38.1519 169.775C38.1187 169.779 38.1364 169.901 38.135 169.927C38.1302 170.02 38.0674 170.111 38.0674 170.206C38.0674 170.245 38.0388 170.328 38.0252 170.367C38.0065 170.419 37.9719 170.48 37.9661 170.535C37.9593 170.6 37.9109 170.634 37.8854 170.688C37.8677 170.725 37.8366 170.798 37.8056 170.823C37.7847 170.839 37.7566 170.937 37.7371 170.967C37.6922 171.038 37.6309 171.121 37.5944 171.194" stroke="black" stroke-width="0.5"/>
<path d="M37.9883 169.074C37.9888 169.094 37.9922 169.113 37.9922 169.133C37.9922 169.162 37.978 169.188 37.9622 169.213C37.8946 169.318 37.7958 169.401 37.7151 169.496C37.6172 169.612 37.5101 169.725 37.4512 169.865C37.4159 169.949 37.3877 170.035 37.3594 170.121C37.3498 170.15 37.3113 170.215 37.332 170.246" stroke="black" stroke-width="0.5"/>
<path d="M154 148.5H149" stroke="black"/>
<path d="M154 160.5H149" stroke="black"/>
<path d="M154 172.5H149" stroke="black"/>
<path d="M154 184.5H149" stroke="black"/>
<path d="M77 148.5H72" stroke="black"/>
<path d="M77 160.5H72" stroke="black"/>
<path d="M77 172.5H72" stroke="black"/>
<path d="M77 184.5H72" stroke="black"/>
<path d="M210.607 143.788C202.612 124.946 188.967 108.851 171.431 97.573C153.895 86.295 133.271 80.351 112.213 80.503C91.155 80.655 70.6242 86.896 53.2645 98.426C35.9047 109.955 22.5096 126.245 14.8066 145.201" stroke="black"/>
<path d="M210.5 144V143" stroke="black"/>
<path d="M210.5 145V144" stroke="black"/>
<path d="M95.5 193.5C95.5 188.996 97.34 184.673 100.621 181.484C103.901 178.294 108.354 176.5 113 176.5C117.646 176.5 122.099 178.294 125.379 181.484C128.66 184.673 130.5 188.996 130.5 193.5" stroke="black"/>
<path d="M116.5 196.5C116.5 194.843 115.157 193.5 113.5 193.5C111.843 193.5 110.5 194.843 110.5 196.5C110.5 198.157 111.843 199.5 113.5 199.5C115.157 199.5 116.5 198.157 116.5 196.5Z" stroke="black"/>
<path d="M126 200.5H100" stroke="black"/>
<path d="M14.5 146V145" stroke="black"/>
<path d="M15.1519 144.775C15.1187 144.779 15.1364 144.901 15.135 144.927C15.1302 145.02 15.0674 145.111 15.0674 145.206C15.0674 145.245 15.0388 145.328 15.0252 145.367C15.0065 145.419 14.9719 145.48 14.9661 145.535C14.9593 145.6 14.9109 145.634 14.8854 145.688C14.8677 145.725 14.8366 145.798 14.8056 145.823C14.7847 145.839 14.7566 145.937 14.7371 145.967C14.6922 146.038 14.6309 146.121 14.5944 146.194" stroke="black" stroke-width="0.5"/>
<path d="M14.9883 144.074C14.9888 144.094 14.9922 144.113 14.9922 144.133C14.9922 144.162 14.978 144.188 14.9622 144.213C14.8946 144.318 14.7958 144.401 14.7151 144.496C14.6172 144.612 14.5101 144.725 14.4512 144.865C14.4159 144.949 14.3877 145.035 14.3594 145.121C14.3498 145.15 14.3113 145.215 14.332 145.246" stroke="black" stroke-width="0.5"/>
<defs>
<filter id="filter0_i_3_4" x="108" y="26" width="55" height="31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter0_i_2_106" x="85" y="1" width="55" height="31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -47,9 +47,9 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_4"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2_106"/>
</filter>
<filter id="filter1_i_3_4" x="109.5" y="123.5" width="54" height="31.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter1_i_2_106" x="86.5" y="98.5" width="54" height="31.5" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -57,9 +57,9 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_4"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_2_106"/>
</filter>
<filter id="filter2_i_3_4" x="109.5" y="150.5" width="54" height="31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<filter id="filter2_i_2_106" x="86.5" y="125.5" width="54" height="31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
@ -67,7 +67,6 @@
<feGaussianBlur stdDeviation="2"/>
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_3_4"/>
</filter>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 B

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd" viewBox="0 0 456 511.82"><path fill="#FD3B3B" d="M48.42 140.13h361.99c17.36 0 29.82 9.78 28.08 28.17l-30.73 317.1c-1.23 13.36-8.99 26.42-25.3 26.42H76.34c-13.63-.73-23.74-9.75-25.09-24.14L20.79 168.99c-1.74-18.38 9.75-28.86 27.63-28.86zM24.49 38.15h136.47V28.1c0-15.94 10.2-28.1 27.02-28.1h81.28c17.3 0 27.65 11.77 27.65 28.01v10.14h138.66c.57 0 1.11.07 1.68.13 10.23.93 18.15 9.02 18.69 19.22.03.79.06 1.39.06 2.17v42.76c0 5.99-4.73 10.89-10.62 11.19-.54 0-1.09.03-1.63.03H11.22c-5.92 0-10.77-4.6-11.19-10.38 0-.72-.03-1.47-.03-2.23v-39.5c0-10.93 4.21-20.71 16.82-23.02 2.53-.45 5.09-.37 7.67-.37zm83.78 208.38c-.51-10.17 8.21-18.83 19.53-19.31 11.31-.49 20.94 7.4 21.45 17.57l8.7 160.62c.51 10.18-8.22 18.84-19.53 19.32-11.32.48-20.94-7.4-21.46-17.57l-8.69-160.63zm201.7-1.74c.51-10.17 10.14-18.06 21.45-17.57 11.32.48 20.04 9.14 19.53 19.31l-8.66 160.63c-.52 10.17-10.14 18.05-21.46 17.57-11.31-.48-20.04-9.14-19.53-19.32l8.67-160.62zm-102.94.87c0-10.23 9.23-18.53 20.58-18.53 11.34 0 20.58 8.3 20.58 18.53v160.63c0 10.23-9.24 18.53-20.58 18.53-11.35 0-20.58-8.3-20.58-18.53V245.66z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1 @@
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m408 112h-224a72 72 0 0 0 -72 72v224a72 72 0 0 0 72 72h224a72 72 0 0 0 72-72v-224a72 72 0 0 0 -72-72zm-32.45 200h-63.55v63.55c0 8.61-6.62 16-15.23 16.43a16 16 0 0 1 -16.77-15.98v-64h-63.55c-8.61 0-16-6.62-16.43-15.23a16 16 0 0 1 15.98-16.77h64v-63.55c0-8.61 6.62-16 15.23-16.43a16 16 0 0 1 16.77 15.98v64h64a16 16 0 0 1 16 16.77c-.42 8.61-7.84 15.23-16.45 15.23z"/><path d="m395.88 80a72.12 72.12 0 0 0 -67.88-48h-224a72 72 0 0 0 -72 72v224a72.12 72.12 0 0 0 48 67.88v-235.88a80 80 0 0 1 80-80z"/></svg>

After

Width:  |  Height:  |  Size: 575 B

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8.00421C13.5523 8.00421 14 8.45192 14 9.00418V12.0042C14 13.1088 13.1046 14.0042 12 14.0042H2C0.89543 14.0042 0 13.1088 0 12.0042V9.00418C0 8.45192 0.44772 8.00421 1 8.00421C1.55228 8.00421 2 8.45192 2 9.00418V12.0042H12V9.00418C12 8.45192 12.4477 8.00421 13 8.00421ZM7 0.589996L10.7071 4.2971C11.0976 4.68763 11.0976 5.32079 10.7071 5.71132C10.3166 6.10184 9.6834 6.10184 9.2929 5.71132L8 4.41842V9.00418C8 9.55648 7.55228 10.0042 7 10.0042C6.44772 10.0042 6 9.55648 6 9.00418V4.41842L4.70711 5.71132C4.31658 6.10184 3.68342 6.10184 3.29289 5.71132C2.90237 5.32079 2.90237 4.68763 3.29289 4.2971L7 0.589996Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

@ -0,0 +1 @@
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M213.333333 128h85.333334v85.333333H213.333333v213.333334a85.333333 85.333333 0 0 1-85.333333 85.333333 85.333333 85.333333 0 0 1 85.333333 85.333333v213.333334h85.333334v85.333333H213.333333c-45.653333-11.52-85.333333-38.4-85.333333-85.333333v-170.666667a85.333333 85.333333 0 0 0-85.333333-85.333333H0v-85.333334h42.666667a85.333333 85.333333 0 0 0 85.333333-85.333333V213.333333a85.333333 85.333333 0 0 1 85.333333-85.333333m597.333334 0a85.333333 85.333333 0 0 1 85.333333 85.333333v170.666667a85.333333 85.333333 0 0 0 85.333333 85.333333h42.666667v85.333334h-42.666667a85.333333 85.333333 0 0 0-85.333333 85.333333v170.666667a85.333333 85.333333 0 0 1-85.333333 85.333333h-85.333334v-85.333333h85.333334v-213.333334a85.333333 85.333333 0 0 1 85.333333-85.333333 85.333333 85.333333 0 0 1-85.333333-85.333333V213.333333h-85.333334V128h85.333334m-298.666667 512a42.666667 42.666667 0 0 1 42.666667 42.666667 42.666667 42.666667 0 0 1-42.666667 42.666666 42.666667 42.666667 0 0 1-42.666667-42.666666 42.666667 42.666667 0 0 1 42.666667-42.666667m-170.666667 0a42.666667 42.666667 0 0 1 42.666667 42.666667 42.666667 42.666667 0 0 1-42.666667 42.666666 42.666667 42.666667 0 0 1-42.666666-42.666666 42.666667 42.666667 0 0 1 42.666666-42.666667m341.333334 0a42.666667 42.666667 0 0 1 42.666666 42.666667 42.666667 42.666667 0 0 1-42.666666 42.666666 42.666667 42.666667 0 0 1-42.666667-42.666666 42.666667 42.666667 0 0 1 42.666667-42.666667z" fill="" /></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -12,7 +12,7 @@ import {
StepContent,
StepInfoNode,
TacticComponent,
} from "../model/tactic/Tactic.ts"
} from "../model/tactic/TacticInfo.ts"
import { getParent } from "../domains/StepsDomain.ts"
import {
computeRelativePositions,
@ -26,20 +26,26 @@ import { CourtAction } from "./editor/CourtAction.tsx"
import { BasketCourt, Court } from "./editor/BasketCourt.tsx"
import { TacticService } from "../service/MutableTacticService.ts"
import { useAppFetcher } from "../App.tsx"
import { mapIdentifiers } from "../domains/TacticContentDomains.ts"
export interface VisualizerProps {
tacticId: number
stepId?: number
visualizerId: string | number
}
export function Visualizer({ tacticId, stepId }: VisualizerProps) {
export function Visualizer({
visualizerId,
tacticId,
stepId,
}: VisualizerProps) {
const [panicMessage, setPanicMessage] = useState<string | null>(null)
const [courtType, setCourtType] = useState<CourtType | null>()
const [stepsTree, setStepsTree] = useState<StepInfoNode | null>()
const fetcher = useAppFetcher()
const service = useMemo(
() => new APITacticService(fetcher, tacticId),
[tacticId],
[fetcher, tacticId],
)
const isNotInit = !stepsTree || !courtType
@ -68,6 +74,7 @@ export function Visualizer({ tacticId, stepId }: VisualizerProps) {
return (
<StepVisualizer
visualizerId={visualizerId}
courtType={courtType}
stepsTree={stepsTree}
stepId={stepId}
@ -78,6 +85,7 @@ export function Visualizer({ tacticId, stepId }: VisualizerProps) {
export interface StepVisualizerProps {
stepId?: number
visualizerId: string | number
stepsTree: StepInfoNode
courtType: CourtType
service: TacticService
@ -85,15 +93,16 @@ export interface StepVisualizerProps {
export function StepVisualizer({
stepId,
visualizerId,
stepsTree,
courtType,
service,
}: StepVisualizerProps) {
const [panicMessage, setPanicMessage] = useState<string | null>(null)
const [content, setContent] = useState<StepContent | null>(null)
const [parentContent, setParentContent] = useState<StepContent | null>()
const [parentContent, setParentContent] = useState<StepContent | null>(null)
const isNotInit = !content || !parentContent
const isNotInit = !content
useEffect(() => {
async function init() {
@ -116,12 +125,21 @@ export function StepVisualizer({
parentContent = parentResult
}
setContent(contentResult)
setParentContent(parentContent)
setContent(
mapIdentifiers(contentResult, (id) => `${id}-${visualizerId}`),
)
if (parentContent) {
setParentContent(
mapIdentifiers(
parentContent,
(id) => `${id}-${visualizerId}-parent`,
),
)
}
}
if (isNotInit) init()
}, [isNotInit, service, stepId, stepsTree])
}, [isNotInit, visualizerId, service, stepId, stepsTree])
if (panicMessage) {
return <p>{panicMessage}</p>

@ -544,14 +544,22 @@ export default function BendableArrow({
<div
className={"arrow-head"}
style={{ position: "absolute", transformOrigin: "center" }}
style={{
position: "absolute",
transformOrigin: "center",
pointerEvents: "none",
}}
ref={headRef}>
{style?.head?.call(style)}
</div>
<div
className={"arrow-tail"}
style={{ position: "absolute", transformOrigin: "center" }}
style={{
position: "absolute",
transformOrigin: "center",
pointerEvents: "none",
}}
ref={tailRef}>
{style?.tail?.call(style)}
</div>

@ -6,10 +6,12 @@ import {
ComponentId,
CourtType,
TacticComponent,
} from "../../model/tactic/Tactic"
} from "../../model/tactic/TacticInfo.ts"
import PlainCourt from "../../assets/court/full_court.svg?react"
import HalfCourt from "../../assets/court/half_court.svg?react"
import "../../style/court.css"
export interface BasketCourtProps {
components: TacticComponent[]
parentComponents: TacticComponent[] | null
@ -65,9 +67,6 @@ export function BasketCourt({
origin={previewAction.origin}
color={previewAction.isInvalid ? "red" : "black"}
isEditable={true}
//do nothing on interacted, not really possible as it's a preview arrow
onActionDeleted={() => {}}
onActionChanges={() => {}}
/>
)}
</div>
@ -76,9 +75,7 @@ export function BasketCourt({
export function Court({ courtType }: { courtType: CourtType }) {
const CourtSvg = courtType === "PLAIN" ? PlainCourt : HalfCourt
return (
<div className="court-image-div">
<CourtSvg className="court-image" />
</div>
)
const courtSpecificClassName =
courtType === "PLAIN" ? "plain-court" : "half-court"
return <CourtSvg className={`court-image ${courtSpecificClassName}`} />
}

@ -2,7 +2,7 @@ import { Action, ActionKind } from "../../model/tactic/Action"
import BendableArrow from "../arrows/BendableArrow"
import { RefObject } from "react"
import { MoveToHead, ScreenHead } from "../actions/ArrowAction"
import { ComponentId } from "../../model/tactic/Tactic"
import { ComponentId } from "../../model/tactic/TacticInfo.ts"
export interface CourtActionProps {
origin: ComponentId

@ -1,5 +1,5 @@
import "../../style/steps_tree.css"
import { StepInfoNode } from "../../model/tactic/Tactic"
import { StepInfoNode } from "../../model/tactic/TacticInfo.ts"
import BendableArrow from "../arrows/BendableArrow"
import { ReactNode, useMemo, useRef } from "react"
import AddSvg from "../../assets/icon/add.svg?react"

@ -9,7 +9,7 @@ import {
ComponentId,
StepContent,
TacticComponent,
} from "../model/tactic/Tactic.ts"
} from "../model/tactic/TacticInfo.ts"
import { overlaps } from "../geo/Box.ts"
import { Action, ActionKind, moves } from "../model/tactic/Action.ts"
import { removeBall, updateComponent } from "./TacticContentDomains.ts"

@ -9,7 +9,7 @@ import {
ComponentId,
StepContent,
TacticComponent,
} from "../model/tactic/Tactic.ts"
} from "../model/tactic/TacticInfo.ts"
import { removeComponent, updateComponent } from "./TacticContentDomains.ts"
import {

@ -1,11 +1,11 @@
import { StepInfoNode } from "../model/tactic/Tactic.ts"
import { StepInfoNode } from "../model/tactic/TacticInfo.ts"
export function addStepNode(
root: StepInfoNode,
parent: StepInfoNode,
parentId: number,
child: StepInfoNode,
): StepInfoNode {
if (root.id === parent.id) {
if (root.id === parentId) {
return {
...root,
children: root.children.concat(child),
@ -14,7 +14,7 @@ export function addStepNode(
return {
...root,
children: root.children.map((c) => addStepNode(c, parent, child)),
children: root.children.map((c) => addStepNode(c, parentId, child)),
}
}
@ -110,3 +110,7 @@ export function getParent(
}
return null
}
export function countSteps(tree: StepInfoNode): number {
return 1 + tree.children.reduce((tot, node) => tot + countSteps(node), 0)
}

@ -18,7 +18,7 @@ import {
ComponentId,
StepContent,
TacticComponent,
} from "../model/tactic/Tactic.ts"
} from "../model/tactic/TacticInfo.ts"
import { overlaps } from "../geo/Box.ts"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems.ts"
@ -499,13 +499,20 @@ export function drainTerminalStateOnChildContent(
}
export function mapToParentContent(content: StepContent): StepContent {
return mapIdentifiers(content, (id) => id + "-parent")
}
export function mapIdentifiers(
Review

This seems to be quite a huge workaround when the primary issue is that the visualizer use document to get the DOM nodes by their id. Maybe update the BendableArrow component to work only in its area prop?

This seems to be quite a huge workaround when the primary issue is that the visualizer use `document` to get the DOM nodes by their id. Maybe update the `BendableArrow` component to work only in its `area` prop?
content: StepContent,
f: (id: string) => string,
): StepContent {
function mapToParentActions(actions: Action[]): Action[] {
return actions.map((a) => ({
...a,
target: a.target + "-parent",
target: typeof a.target === "string" ? f(a.target) : a.target,
segments: a.segments.map((s) => ({
...s,
next: typeof s.next === "string" ? s.next + "-parent" : s.next,
next: typeof s.next === "string" ? f(s.next) : s.next,
})),
}))
}
@ -517,10 +524,10 @@ export function mapToParentContent(content: StepContent): StepContent {
if (p.type == "player") {
return {
...p,
id: p.id + "-parent",
id: f(p.id),
actions: mapToParentActions(p.actions),
path: p.path && {
items: p.path.items.map((p) => p + "-parent"),
items: p.path.items.map(f),
},
}
}
@ -528,10 +535,10 @@ export function mapToParentContent(content: StepContent): StepContent {
...p,
pos:
p.pos.type == "follows"
? { ...p.pos, attach: p.pos.attach + "-parent" }
? { ...p.pos, attach: f(p.pos.attach) }
: p.pos,
id: p.id + "-parent",
originPlayerId: p.originPlayerId + "-parent",
id: f(p.id),
originPlayerId: f(p.originPlayerId),
actions: mapToParentActions(p.actions),
}
}),

@ -0,0 +1,109 @@
import { Fetcher } from "../app/Fetcher.ts"
import {
StepInfoNode,
Tactic,
TacticInfo,
TacticStep,
} from "../model/tactic/TacticInfo.ts"
import { APITacticService } from "../service/APITacticService.ts"
import {
TacticContext,
TacticService,
} from "../service/MutableTacticService.ts"
import { countSteps } from "./StepsDomain.ts"
export function importTacticFromFile(
fetcher: Fetcher,
blob: Blob,
onSuccess: (tactic: TacticInfo) => void,
onError: (e: Error | unknown) => void = console.error,
) {
const reader = new FileReader()
reader.onloadend = async (e) => {
const jsonString = e.target!.result as string
let tactic
try {
tactic = await importTactic(fetcher, JSON.parse(jsonString))
} catch (e) {
onError(e)
return
}
onSuccess(tactic)
}
reader.readAsText(blob)
}
export async function importTactic(
fetcher: Fetcher,
tactic: Tactic,
): Promise<TacticInfo> {
const response = await fetcher.fetchAPI(
"tactics",
{
name: tactic.name,
courtType: tactic.courtType,
},
"POST",
)
if (!response.ok) throw Error("Received unsuccessful response from API.")
const { id, rootStepId } = await response.json()
const service = new APITacticService(fetcher, id)
await service.saveContent(rootStepId, tactic.root.content)
async function importStepChildren(parent: TacticStep, parentId: number) {
return await Promise.all(
parent.children.map(async (child) => {
const result = await service.addStep(parentId, child.content)
if (typeof result === "string") throw Error(result)
await importStepChildren(child, result.id)
return result
}),
)
}
const rootStepNode: StepInfoNode = {
id: rootStepId,
children: await importStepChildren(tactic.root, rootStepId),
}
return { courtType: tactic.courtType, name: tactic.name, id, rootStepNode }
}
export async function loadPlainTactic(
context: TacticContext,
service: TacticService,
onProgress: (p: number) => void = () => {},
): Promise<Tactic> {
const tree = context.stepsTree
const treeSize = countSteps(tree)
const totalStepsCompleted = new Uint16Array(1)
async function transformToStep(
stepInfoNode: StepInfoNode,
): Promise<TacticStep> {
const contentResult = await service.getContent(stepInfoNode.id)
if (typeof contentResult === "string") throw Error(contentResult)
Atomics.add(totalStepsCompleted, 0, 1)
onProgress((Atomics.load(totalStepsCompleted, 0) / treeSize) * 100)
Review
-        Atomics.add(totalStepsCompleted, 0, 1)
-        onProgress((Atomics.load(totalStepsCompleted, 0) / treeSize) * 100)
+        totalStepsCompleted += 1
+        onProgress((totalStepsCompleted / treeSize) * 100)

No need to use atomics as everything happen on the same thread.

This work should really be done server side, as it creates a lot of requests and exchanges with the database. Also for the record, you could perform all requests at the same time, regardless of the depth of the node in the tree:

    type NodeInStack = [node: StepInfoNode, parent: number]

    let treeSize = 0
    let completedRequests = 0
    const requests: Promise<StepContent>[] = []
    const nodes: TacticStep[] = []
    const stack: NodeInStack[] = [[context.stepsTree, -1]]
    while (stack.length) {
        const [node, parent] = stack.pop()!
        treeSize += 1
        requests.push(service.getContent(node.id).then((content) => {
            completedRequests += 1
            onProgress((completedRequests / treeSize) * 100)
            if (typeof content === "string") throw Error(content)
            return content
        }));
        for (const child of node.children) {
            stack.push([child, nodes.length])
        }
        const step: TacticStep = {
            content: { components: [] },
            children: [],
        }
        nodes.push(step)
        if (parent >= 0) {
            nodes[parent].children.push(step)
        }
    }

    const contents = await Promise.all(requests)
    for (let i = 0; i < nodes.length; i++) {
        nodes[i].content.components.push(...contents[i].components)
    }
    return {
        name: context.name,
        courtType: context.courtType,
        root: nodes[0],
    }
```diff - Atomics.add(totalStepsCompleted, 0, 1) - onProgress((Atomics.load(totalStepsCompleted, 0) / treeSize) * 100) + totalStepsCompleted += 1 + onProgress((totalStepsCompleted / treeSize) * 100) ``` No need to use atomics as everything happen on the same thread. This work should really be done server side, as it creates a lot of requests and exchanges with the database. Also for the record, you could perform all requests at the same time, regardless of the depth of the node in the tree: ```ts type NodeInStack = [node: StepInfoNode, parent: number] let treeSize = 0 let completedRequests = 0 const requests: Promise<StepContent>[] = [] const nodes: TacticStep[] = [] const stack: NodeInStack[] = [[context.stepsTree, -1]] while (stack.length) { const [node, parent] = stack.pop()! treeSize += 1 requests.push(service.getContent(node.id).then((content) => { completedRequests += 1 onProgress((completedRequests / treeSize) * 100) if (typeof content === "string") throw Error(content) return content })); for (const child of node.children) { stack.push([child, nodes.length]) } const step: TacticStep = { content: { components: [] }, children: [], } nodes.push(step) if (parent >= 0) { nodes[parent].children.push(step) } } const contents = await Promise.all(requests) for (let i = 0; i < nodes.length; i++) { nodes[i].content.components.push(...contents[i].components) } return { name: context.name, courtType: context.courtType, root: nodes[0], } ```
return {
content: contentResult,
children: await Promise.all(
stepInfoNode.children.map(transformToStep),
),
}
}
return {
name: context.name,
courtType: context.courtType,
root: await transformToStep(tree),
}
}

@ -1,4 +1,4 @@
import { StepContent } from "../model/tactic/Tactic.ts"
import { StepContent } from "../model/tactic/TacticInfo.ts"
export class ContentVersions {
private index = 0

@ -1,5 +1,5 @@
import { Pos } from "../../geo/Pos"
import { ComponentId } from "./Tactic"
import { ComponentId } from "./TacticInfo.ts"
export enum ActionKind {
SCREEN = "SCREEN",

@ -1,4 +1,4 @@
import { Component, Frozable } from "./Tactic"
import { Component, Frozable } from "./TacticInfo.ts"
import { Pos } from "../../geo/Pos.ts"
export const BALL_ID = "ball"

@ -1,4 +1,4 @@
import { Component, ComponentId, Frozable } from "./Tactic"
import { Component, ComponentId, Frozable } from "./TacticInfo.ts"
import { Pos } from "../../geo/Pos.ts"
export type PlayerId = string

@ -2,6 +2,12 @@ import { Player, PlayerPhantom } from "./Player"
import { Action } from "./Action"
import { CourtObject } from "./CourtObjects"
export interface Tactic {
readonly name: string
readonly courtType: CourtType
readonly root: TacticStep
}
export interface TacticInfo {
readonly id: number
readonly name: string
@ -10,8 +16,8 @@ export interface TacticInfo {
}
export interface TacticStep {
readonly stepId: number
readonly content: StepContent
readonly children: TacticStep[]
}
export interface StepContent {

@ -22,7 +22,7 @@ import {
StepContent,
StepInfoNode,
TacticComponent,
} from "../model/tactic/Tactic"
} from "../model/tactic/TacticInfo.ts"
import SavingState, {
SaveState,
@ -97,12 +97,14 @@ import SplitLayout from "../components/SplitLayout.tsx"
import {
MutableTacticService,
ServiceError,
TacticService,
} from "../service/MutableTacticService.ts"
import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts"
import { APITacticService } from "../service/APITacticService.ts"
import { useNavigate, useParams } from "react-router-dom"
import { ContentVersions } from "../editor/ContentVersions.ts"
import { useAppFetcher } from "../App.tsx"
import ExportTacticPopup from "./popup/ExportTacticPopup.tsx"
const ERROR_STYLE: CSSProperties = {
borderColor: "red",
@ -132,6 +134,8 @@ interface EditorService {
setName(name: string): Promise<SaveState>
openVisualizer(): Promise<void>
getTacticService(): TacticService
}
export default function Editor({ guestMode }: EditorProps) {
@ -158,6 +162,7 @@ export default function Editor({ guestMode }: EditorProps) {
interface EditorPageWrapperProps {
service: MutableTacticService
openVisualizer(): void
}
@ -305,11 +310,11 @@ function EditorPageWrapper({
parent: StepInfoNode,
content: StepContent,
): Promise<StepInfoNode | ServiceError> {
const result = await service.addStep(parent, content)
const result = await service.addStep(parent.id, content)
if (typeof result !== "string") {
internalStepsTree = addStepNode(
internalStepsTree!,
parent,
parent.id,
result,
)
setStepsTree(internalStepsTree)
@ -355,6 +360,10 @@ function EditorPageWrapper({
async openVisualizer(): Promise<void> {
openVisualizer()
},
getTacticService(): TacticService {
return service
},
}
}, [stepsTree, service, stepsVersions, setStepContent, openVisualizer])
@ -422,10 +431,7 @@ function EditorPage({
const [isStepsTreeVisible, setStepsTreeVisible] = useState(true)
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
const [showExportPopup, setShowExportPopup] = useState(false)
const [editorContentCurtainWidth, setEditorContentCurtainWidth] =
useState(80)
@ -437,6 +443,11 @@ function EditorPage({
: new Map()
}, [content, courtRef])
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
const setComponents = (action: SetStateAction<TacticComponent[]>) => {
service.setContent((c) => ({
...c,
@ -759,7 +770,6 @@ function EditorPage({
/>
</div>
<div id="court-div">
<div id="court-div-bounds">
<BasketCourt
parentComponents={parentContent?.components ?? null}
components={content.components}
@ -771,7 +781,6 @@ function EditorPage({
/>
</div>
</div>
</div>
)
const stepsTreeNode = (
@ -810,6 +819,14 @@ function EditorPage({
return (
<div id="main-div">
{showExportPopup && (
<div id="exports-popup">
<ExportTacticPopup
service={service.getTacticService()}
onHide={() => setShowExportPopup(false)}
/>
</div>
)}
<div id="topbar-div">
<div id="topbar-left">
<SavingState state={contentSaveState} />
@ -835,10 +852,15 @@ function EditorPage({
VISUALISER
</button>
<button
id={"show-steps-button"}
id="show-steps-button"
onClick={() => setStepsTreeVisible((b) => !b)}>
ETAPES
</button>
<button
id="show-exports-popup"
onClick={() => setShowExportPopup(true)}>
EXPORTER
</button>
</div>
</div>
<div id="editor-div">

@ -1,8 +1,15 @@
import "../style/home/home.css"
import { useNavigate } from "react-router-dom"
import { useEffect, useState } from "react"
import { User } from "../model/User.ts"
import { createContext, Dispatch, useCallback, useContext, useEffect, useMemo, useReducer } from "react"
import { useAppFetcher } from "../App.tsx"
import { Visualizer } from "../components/Visualizer.tsx"
import BinSvg from "../assets/icon/bin.svg?react"
import ExportSvg from "../assets/icon/export.svg?react"
import DuplicateSvg from "../assets/icon/duplicate.svg?react"
import ExportTacticPopup from "./popup/ExportTacticPopup.tsx"
import { APITacticService } from "../service/APITacticService.ts"
import { FileUploader } from "react-drag-drop-files"
import { importTactic, importTacticFromFile, loadPlainTactic } from "../domains/TacticPersistenceDomain.ts"
interface Tactic {
id: number
@ -18,9 +25,87 @@ interface Team {
second_color: string
}
enum HomePageStateActionKind {
UPDATE_TACTICS = "UPDATE_TACTICS",
ADD_TACTIC = "ADD_TACTIC",
REMOVE_TACTIC = "REMOVE_TACTIC",
UPDATE_TEAMS = "UPDATE_TEAMS",
SET_EXPORTING_TACTIC = "SET_EXPORTING_TACTIC",
INIT = "INIT",
}
type HomePageStateAction =
| {
type: HomePageStateActionKind.UPDATE_TACTICS
tactics: Tactic[]
}
| {
type: HomePageStateActionKind.UPDATE_TEAMS
teams: Team[]
}
| {
type: HomePageStateActionKind.INIT
state: HomePageState
}
| {
type: HomePageStateActionKind.SET_EXPORTING_TACTIC
tacticId: number | undefined
}
| {
type: HomePageStateActionKind.REMOVE_TACTIC,
tacticId: number
} | {
type: HomePageStateActionKind.ADD_TACTIC,
tactic: Tactic
}
interface HomePageState {
tactics: Tactic[]
teams: Team[]
/**
* The home page displays a popup to export a certains tactic
*/
exportingTacticId?: number
}
function homePageStateReducer(
state: HomePageState,
action: HomePageStateAction,
): HomePageState {
switch (action.type) {
case HomePageStateActionKind.UPDATE_TACTICS:
return { ...state, tactics: action.tactics }
case HomePageStateActionKind.UPDATE_TEAMS:
return { ...state, teams: action.teams }
case HomePageStateActionKind.SET_EXPORTING_TACTIC:
return { ...state, exportingTacticId: action.tacticId }
case HomePageStateActionKind.ADD_TACTIC:
return { ...state, tactics: [action.tactic, ...state.tactics] }
case HomePageStateActionKind.REMOVE_TACTIC:
return { ...state, tactics: state.tactics.filter(t => t.id !== action.tacticId) }
case HomePageStateActionKind.INIT:
return action.state
}
}
interface HomeStateContextMutable {
state: HomePageState
dispatch: Dispatch<HomePageStateAction>
}
const HomeStateContext = createContext<HomeStateContextMutable | null>(null)
function useHomeState() {
return useContext(HomeStateContext)
}
export default function HomePage() {
type UserDataResponse = { user?: User; tactics: Tactic[]; teams: Team[] }
const [{ tactics, teams }, setInfo] = useState<UserDataResponse>({
const [state, dispatch] = useReducer(homePageStateReducer, {
tactics: [],
teams: [],
})
@ -31,76 +116,71 @@ export default function HomePage() {
useEffect(() => {
async function initUserData() {
const response = await fetcher.fetchAPIGet("user-data")
if (response.status == 401) {
navigate("/login")
return // if unauthorized
}
setInfo(await response.json())
type UserDataResponse = { teams: Team[]; tactics: Tactic[] }
const { teams, tactics }: UserDataResponse = await response.json()
tactics.sort((a, b) => b.creationDate - a.creationDate)
dispatch({
type: HomePageStateActionKind.INIT,
state: { teams, tactics },
})
}
initUserData()
}, [fetcher, navigate])
tactics!.sort((a, b) => b.creationDate - a.creationDate)
const tacticExportService = useMemo(
() =>
state.exportingTacticId
? new APITacticService(fetcher, state.exportingTacticId!)
: null,
[fetcher, state.exportingTacticId],
)
const lastTactics = tactics.slice(0, 5)
return (
<Home teams={teams!} allTactics={tactics!} lastTactics={lastTactics} />
<HomeStateContext.Provider value={{ state, dispatch }}>
{tacticExportService && (
<div id="exports-popup">
<ExportTacticPopup
service={tacticExportService}
onHide={() =>
dispatch({
type: HomePageStateActionKind.SET_EXPORTING_TACTIC,
tacticId: undefined,
})
}
/>
</div>
)}
<Home />
</HomeStateContext.Provider>
)
}
function Home({
lastTactics,
allTactics,
teams,
}: {
lastTactics: Tactic[]
allTactics: Tactic[]
teams: Team[]
}) {
function Home() {
return (
<div id="main">
<Body
lastTactics={lastTactics}
allTactics={allTactics}
teams={teams}
/>
<Body />
</div>
)
}
function Body({
lastTactics,
allTactics,
teams,
}: {
lastTactics: Tactic[]
allTactics: Tactic[]
teams: Team[]
}) {
function Body() {
const widthPersonalSpace = 78
const widthSideMenu = 100 - widthPersonalSpace
return (
<div id="body">
<PersonalSpace width={widthPersonalSpace} allTactics={allTactics} />
<SideMenu
width={widthSideMenu}
lastTactics={lastTactics}
teams={teams}
/>
<PersonalSpace width={widthPersonalSpace} />
<SideMenu width={widthSideMenu} />
</div>
)
}
function SideMenu({
width,
lastTactics,
teams,
}: {
width: number
lastTactics: Tactic[]
teams: Team[]
}) {
function SideMenu({ width }: { width: number }) {
return (
<div
id="side-menu"
@ -108,20 +188,46 @@ function SideMenu({
width: width + "%",
}}>
<div id="side-menu-content">
<Team teams={teams} />
<Tactic lastTactics={lastTactics} />
<LastTeamsSideMenu />
<LastTacticsSideMenu />
<TacticImportArea />
</div>
</div>
)
}
function TacticImportArea() {
const fetcher = useAppFetcher()
const { dispatch } = useHomeState()!
const handleDrop = useCallback(
async (file: File) => {
importTacticFromFile(fetcher, file, (tactic) => {
dispatch({
type: HomePageStateActionKind.ADD_TACTIC,
tactic: {
name: tactic.name,
id: tactic.id,
creationDate: new Date().getDate(),
},
})
})
},
[dispatch, fetcher],
)
return (
<div id="tactic-import-area">
<FileUploader
handleChange={handleDrop}
types={["json"]}
hoverTitle="Déposez ici"
label="Séléctionnez ou déposez un fichier ici"/>
</div>
maxime.batista marked this conversation as resolved
Review
-                label="Séléctionnez ou déposez un fichier ici"></FileUploader>
+                label="Sélectionnez ou déposez un fichier ici" />
```diff - label="Séléctionnez ou déposez un fichier ici"></FileUploader> + label="Sélectionnez ou déposez un fichier ici" /> ```
)
}
function PersonalSpace({
width,
allTactics,
}: {
width: number
allTactics: Tactic[]
}) {
function PersonalSpace({ width }: { width: number }) {
return (
<div
id="personal-space"
@ -129,7 +235,7 @@ function PersonalSpace({
width: width + "%",
}}>
<TitlePersonalSpace />
<BodyPersonalSpace allTactics={allTactics} />
<BodyPersonalSpace />
</div>
)
}
@ -142,69 +248,112 @@ function TitlePersonalSpace() {
)
}
function TableData({ allTactics }: { allTactics: Tactic[] }) {
const nbRow = Math.floor(allTactics.length / 3) + 1
const listTactic = Array(nbRow)
for (let i = 0; i < nbRow; i++) {
listTactic[i] = Array(0)
}
let i = 0
let j = 0
allTactics.forEach((tactic) => {
listTactic[i].push(tactic)
j++
if (j === 3) {
i++
j = 0
}
})
function TacticGrid({ tactics }: { tactics: Tactic[] }) {
return (
<div id={"tactics-grid"}>
{tactics.map((team) => (
<TacticCard key={team.id} tactic={team} />
))}
</div>
)
}
function TacticCard({ tactic }: { tactic: Tactic }) {
const navigate = useNavigate()
i = 0
while (i < nbRow) {
listTactic[i] = listTactic[i].map((tactic: Tactic) => (
<td
key={tactic.id}
className="data"
onClick={() => {
navigate("/tactic/" + tactic.id + "/edit")
}}>
{truncateString(tactic.name, 25)}
</td>
))
i++
}
if (nbRow == 1) {
if (listTactic[0].length < 3) {
for (let i = 0; i <= 3 - listTactic[0].length; i++) {
listTactic[0].push(<td key={"tdNone" + i}></td>)
}
}
const fetcher = useAppFetcher()
const {
dispatch,
} = useHomeState()!
return (
<div
className={"tactic-card"}
onClick={() => navigate(`/tactic/${tactic.id}/edit`)}>
<div className={"tactic-card-preview"}>
<Visualizer
visualizerId={tactic.id.toString()}
tacticId={tactic.id}
/>
</div>
<div className="tactic-card-content">
<p className="tactic-card-title">{tactic.name}</p>
<div className="tactic-card-actions">
<ExportSvg
className="tactic-card-export-btn"
onClick={(e) => {
e.stopPropagation()
dispatch({
type: HomePageStateActionKind.SET_EXPORTING_TACTIC,
tacticId: tactic.id,
})
}}
/>
<BinSvg
className="tactic-card-remove-btn"
onClick={async (e) => {
e.stopPropagation()
const response = await fetcher.fetchAPI(
`tactics/${tactic.id}`,
{},
"DELETE",
)
if (!response.ok) {
throw Error(
`Cannot delete tactic ${tactic.id}!`,
)
}
dispatch({
type: HomePageStateActionKind.REMOVE_TACTIC,
tacticId: tactic.id,
})
}}
/>
<DuplicateSvg
className="tactic-card-duplicate-btn"
onClick={async (e) => {
e.stopPropagation()
const service = new APITacticService(
fetcher,
tactic.id,
)
const context = await service.getContext()
if (typeof context === "string")
throw Error(context)
return listTactic.map((tactic, rowIndex) => (
<tr key={rowIndex + "row"}>{tactic}</tr>
))
const plainTactic = await loadPlainTactic(
context,
service,
)
const { name, id } = await importTactic(
fetcher,
plainTactic,
)
dispatch({
type: HomePageStateActionKind.ADD_TACTIC,
tactic: { name, id, creationDate: 0 },
})
}}
/>
</div>
</div>
</div>
)
}
function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) {
function BodyPersonalSpace() {
const tactics = useHomeState()!.state.tactics
return (
<div id="body-personal-space">
{allTactics.length == 0 ? (
{tactics.length == 0 ? (
<p>Aucune tactique créée !</p>
) : (
<table>
<tbody key="tbody">
<TableData allTactics={allTactics} />
</tbody>
</table>
<TacticGrid tactics={tactics} />
)}
</div>
)
}
function Team({ teams }: { teams: Team[] }) {
function LastTeamsSideMenu() {
const navigate = useNavigate()
return (
<div id="teams">
@ -214,12 +363,12 @@ function Team({ teams }: { teams: Team[] }) {
+
</button>
</div>
<SetButtonTeam teams={teams} />
<SetButtonTeam />
</div>
)
}
function Tactic({ lastTactics }: { lastTactics: Tactic[] }) {
function LastTacticsSideMenu() {
const navigate = useNavigate()
return (
@ -233,26 +382,27 @@ function Tactic({ lastTactics }: { lastTactics: Tactic[] }) {
+
</button>
</div>
<SetButtonTactic tactics={lastTactics} />
<SetButtonTactic />
</div>
)
}
function SetButtonTactic({ tactics }: { tactics: Tactic[] }) {
function SetButtonTactic() {
const tactics = useHomeState()!.state.tactics.slice(0, 5)
const lastTactics = tactics.map((tactic) => (
<ButtonLastTactic key={tactic.id} tactic={tactic} />
<LastTacticCard key={tactic.id} tactic={tactic} />
))
return <div className="set-button">{lastTactics}</div>
}
function SetButtonTeam({ teams }: { teams: Team[] }) {
const listTeam = teams.map((team) => (
<ButtonTeam key={team.id} team={team} />
))
function SetButtonTeam() {
const teams = useHomeState()!.state.teams
const listTeam = teams.map((team) => <TeamCard key={team.id} team={team} />)
return <div className="set-button">{listTeam}</div>
}
function ButtonTeam({ team }: { team: Team }) {
function TeamCard({ team }: { team: Team }) {
const name = truncateString(team.name, 20)
const navigate = useNavigate()
@ -270,7 +420,7 @@ function ButtonTeam({ team }: { team: Team }) {
)
}
function ButtonLastTactic({ tactic }: { tactic: Tactic }) {
function LastTacticCard({ tactic }: { tactic: Tactic }) {
const name = truncateString(tactic.name, 20)
const navigate = useNavigate()

@ -3,7 +3,7 @@ import "../style/new_tactic_panel.css"
import plainCourt from "../assets/court/full_court.svg"
import halfCourt from "../assets/court/half_court.svg"
import { CourtType } from "../model/tactic/Tactic.ts"
import { CourtType } from "../model/tactic/TacticInfo.ts"
import { useCallback } from "react"
import { useAppFetcher, useUser } from "../App.tsx"
import { useNavigate } from "react-router-dom"

@ -1,15 +1,15 @@
import { ServiceError, TacticService } from "../service/MutableTacticService.ts"
import { useNavigate, useParams } from "react-router-dom"
import { useCallback, useEffect, useMemo, useState } from "react"
import {
useVisualizer,
VisualizerState,
VisualizerStateActionKind,
} from "../visualizer/VisualizerState.ts"
import { useCallback, useEffect, useMemo, useState } from "react"
import { getParent } from "../domains/StepsDomain.ts"
import { mapToParentContent } from "../domains/TacticContentDomains.ts"
import StepsTree from "../components/editor/StepsTree.tsx"
import { StepInfoNode } from "../model/tactic/Tactic.ts"
import { StepInfoNode } from "../model/tactic/TacticInfo.ts"
import SplitLayout from "../components/SplitLayout.tsx"
import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts"
import { APITacticService } from "../service/APITacticService.ts"
@ -17,12 +17,13 @@ import { APITacticService } from "../service/APITacticService.ts"
import "../style/visualizer.css"
import { VisualizerFrame } from "../components/Visualizer.tsx"
import { useAppFetcher } from "../App.tsx"
import ExportTacticPopup from "./popup/ExportTacticPopup.tsx"
export interface VisualizerPageProps {
guestMode: boolean
}
export function VisualizerPage({ guestMode }: VisualizerPageProps) {
export default function VisualizerPage({ guestMode }: VisualizerPageProps) {
const { tacticId: idStr } = useParams()
const navigate = useNavigate()
@ -48,6 +49,8 @@ interface VisualizerService {
selectStep(step: number): Promise<void | ServiceError>
openEditor(): Promise<void>
getTacticService(): TacticService
}
interface ServedVisualizerPageProps {
@ -101,7 +104,7 @@ function ServedVisualizerPage({
}
if (state === null) init()
}, [service, state])
}, [dispatch, service, state])
const visualizerService: VisualizerService = useMemo(
() => ({
@ -125,8 +128,12 @@ function ServedVisualizerPage({
async openEditor() {
openEditor()
},
getTacticService(): TacticService {
return service
},
}),
[openEditor, service, state],
[dispatch, openEditor, service, state],
)
if (panicMessage) {
@ -162,6 +169,8 @@ function VisualizerPageContent({
const [editorContentCurtainWidth, setEditorContentCurtainWidth] =
useState(80)
const [showExportPopup, setShowExportPopup] = useState(false)
const stepsTreeNode = (
<div id={"steps-div"}>
<StepsTree
@ -177,19 +186,34 @@ function VisualizerPageContent({
const contentNode = (
<div id="content-div">
<div id="court-div">
<VisualizerFrame
content={content}
parentContent={parentContent}
courtType={courtType}
/>
</div>
</div>
)
return (
<div id="visualizer">
{showExportPopup && (
<div id="exports-popup">
<ExportTacticPopup
service={service.getTacticService()}
onHide={() => setShowExportPopup(false)}
/>
</div>
)}
<div id="header-page">
<p id="title">{tacticName}</p>
<div id="header-page-right">
<button
id="show-exports-popup"
onClick={() => setShowExportPopup(true)}>
EXPORTER
</button>
<button
id={"show-steps-button"}
onClick={() => setStepsTreeVisible((b) => !b)}>
@ -203,7 +227,7 @@ function VisualizerPageContent({
</div>
</div>
<div id="editor-div">
<div id="visualizer-div">
{isStepsTreeVisible ? (
<SplitLayout
rightWidth={editorContentCurtainWidth}

@ -0,0 +1,118 @@
import {
TacticContext,
TacticService,
} from "../../service/MutableTacticService.ts"
import { useEffect, useState } from "react"
import "../../style/export_tactic_popup.css"
import JsonIcon from "../../assets/icon/json.svg?react"
import { loadPlainTactic } from "../../domains/TacticPersistenceDomain.ts"
export interface ExportTacticPopupProps {
service: TacticService
onHide: () => void
maxime.batista marked this conversation as resolved
Review
-    show: boolean

If the popup has been rendered, it is because it should be shown.

```diff - show: boolean ``` If the popup has been rendered, it is because it should be shown.
}
export default function ExportTacticPopup({
service,
onHide,
}: ExportTacticPopupProps) {
const [context, setContext] = useState<TacticContext>()
const [panicMessage, setPanicMessage] = useState<string>()
const [exportPercentage, setExportPercentage] = useState(0)
useEffect(() => {
async function init() {
const result = await service.getContext()
if (typeof result === "string") {
setPanicMessage("Could not retrieve tactic context")
return
}
setContext(result)
}
if (!context) init()
}, [context, service])
useEffect(() => {
function onKeyUp(e: KeyboardEvent) {
if (e.key === "Escape") onHide()
}
window.addEventListener("keyup", onKeyUp)
return () => window.removeEventListener("keyup", onKeyUp)
}, [onHide])
if (panicMessage) return <p>{panicMessage}</p>
maxime.batista marked this conversation as resolved
Review

Place this code in a separate hook as it is used for all popups.

Place this code in a separate hook as it is used for all popups.
return (
<div className="popup" onClick={onHide}>
<div className="popup-card" onClick={(e) => e.stopPropagation()}>
<div className="popup-header">
<p>Exporting {context?.name ?? "Tactic"}</p>
</div>
<ProgressBar percentage={exportPercentage} />
<div className="popup-exports">
<div
className="export-card"
onClick={async () => {
await exportInJson(
context!,
service,
setExportPercentage,
)
setExportPercentage(0)
}}>
<p className="export-card-title">Exporter en JSON</p>
<JsonIcon className="json-logo" />
</div>
</div>
</div>
maxime.batista marked this conversation as resolved
Review
-                        <p className="export-card-title">Export in JSON</p>
+                        <p className="export-card-title">Exporter en JSON</p>
```diff - <p className="export-card-title">Export in JSON</p> + <p className="export-card-title">Exporter en JSON</p> ```
</div>
)
}
async function exportInJson(
context: TacticContext,
service: TacticService,
onProgress: (p: number) => void,
) {
const tactic = await loadPlainTactic(context, service, onProgress)
const e = document.createElement("a")
e.setAttribute(
"href",
"data:application/json;charset=utf-8," +
encodeURIComponent(JSON.stringify(tactic, null, 2)),
)
e.setAttribute("download", `${context.name}.json`)
e.style.display = "none"
maxime.batista marked this conversation as resolved
Review
-        "data:application/octet-stream," +
+        "data:application/json;charset=utf-8," +
```diff - "data:application/octet-stream," + + "data:application/json;charset=utf-8," + ```
document.body.appendChild(e)
e.click()
document.body.removeChild(e)
}
interface ProgressBarProps {
percentage: number
}
function ProgressBar({ percentage }: ProgressBarProps) {
return (
<div className={"progressbar"} style={{ display: "flex", height: 3 }}>
<div
style={{
backgroundColor: "#f5992b",
width: `${percentage}%`,
}}></div>
<div
style={{
backgroundColor: "white",
width: `${100 - percentage}%`,
}}></div>
</div>
)
}

@ -3,8 +3,8 @@ import {
ServiceError,
TacticContext,
} from "./MutableTacticService.ts"
import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts"
import { Fetcher } from "../app/Fetcher.ts"
import { StepContent, StepInfoNode } from "../model/tactic/TacticInfo.ts"
export class APITacticService implements MutableTacticService {
private readonly tacticId: number
@ -44,13 +44,13 @@ export class APITacticService implements MutableTacticService {
}
async addStep(
parent: StepInfoNode,
parentId: number,
content: StepContent,
): Promise<StepInfoNode | ServiceError> {
const response = await this.fetcher.fetchAPI(
`tactics/${this.tacticId}/steps`,
{
parentId: parent.id,
parentId: parentId,
content,
},
)

@ -3,7 +3,7 @@ import {
ServiceError,
TacticContext,
} from "./MutableTacticService.ts"
import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts"
import { StepContent, StepInfoNode } from "../model/tactic/TacticInfo.ts"
import {
addStepNode,
getAvailableId,
@ -55,7 +55,7 @@ export class LocalStorageTacticService implements MutableTacticService {
}
async addStep(
parent: StepInfoNode,
parentId: number,
content: StepContent,
): Promise<StepInfoNode | ServiceError> {
const root: StepInfoNode = JSON.parse(
@ -65,7 +65,7 @@ export class LocalStorageTacticService implements MutableTacticService {
const nodeId = getAvailableId(root)
const node = { id: nodeId, children: [] }
const resultTree = addStepNode(root, parent, node)
const resultTree = addStepNode(root, parentId, node)
localStorage.setItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,

@ -1,4 +1,8 @@
import { CourtType, StepContent, StepInfoNode } from "../model/tactic/Tactic.ts"
import {
CourtType,
StepContent,
StepInfoNode,
} from "../model/tactic/TacticInfo.ts"
export interface TacticContext {
stepsTree: StepInfoNode
@ -19,7 +23,7 @@ export interface TacticService {
export interface MutableTacticService extends TacticService {
addStep(
parent: StepInfoNode,
parentId: number,
content: StepContent,
): Promise<StepInfoNode | ServiceError>

@ -5,7 +5,7 @@
.arrow-action-icon {
user-select: none;
-moz-user-select: none;
-webkit-user-drag: none;
pointer-events: none;
max-width: 17px;
max-height: 17px;
}

@ -1,24 +1,33 @@
.court-image-div {
position: relative;
background-color: white;
height: 80vh;
}
.court-container {
display: flex;
align-content: center;
align-items: center;
justify-content: center;
background-color: black;
background-color: white;
padding: 5%;
}
.court-container:has(.plain-court) {
width: 80%;
}
.court-container:has(.half-court) {
height: 80%;
}
.court-image {
height: 100%;
width: 100%;
background-color: white;
user-select: none;
}
.court-image * {
stroke: var(--selected-team-secondarycolor);
}
.half-court {
max-height: 70vh;
width: fit-content;
}

@ -1,8 +1,8 @@
@import "theme/default.css";
@import "court.css";
@import "tactic.css";
#main-div {
position: relative;
display: flex;
height: 100%;
width: 100%;
@ -102,10 +102,9 @@
}
#court-div {
background-color: var(--background-color);
height: 100%;
width: 100%;
padding-left: 10%;
padding-right: 10%;
display: flex;
align-items: center;
@ -137,3 +136,10 @@
.save-state-guest {
color: gray;
}
#exports-popup {
position: absolute;
width: 100%;
height: 100%;
z-index: 1000;
}

@ -0,0 +1,65 @@
.popup {
width: 100%;
height: 100%;
display: flex;
align-items: center;
align-content: center;
justify-content: center;
background: rgba(49, 36, 36, 0.53);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(1.7px);
-webkit-backdrop-filter: blur(1.7px);
}
.popup-card {
border: 1px solid rgba(71, 71, 86, 0.72);
width: 50%;
height: 50%;
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 15px;
}
.popup-exports {
display: flex;
align-content: center;
align-items: center;
justify-content: center;
height: 100%;
background: rgba(49, 36, 36, 0.53);
backdrop-filter: blur(1.7px);
-webkit-backdrop-filter: blur(1.7px);
}
.popup-header {
display: flex;
justify-content: center;
background-color: white;
}
.export-card {
border-radius: 20px;
display: flex;
flex-direction: column;
background-color: white;
align-items: center;
cursor: pointer;
}
.export-card .json-logo {
width: 80% !important;
height: 80% !important;
}
.export-card .json-logo * {
fill: #f5992b;
}

@ -2,15 +2,8 @@
@import url(personnal_space.css);
@import url(side_menu.css);
@import url(../template/header.css);
body {
/* background-color: #303030; */
}
@import url(../tactic.css);
#main {
/* margin-left : 10%;
margin-right: 10%; */
/* border : solid 1px #303030; */
display: flex;
flex-direction: column;
font-family: var(--font-content);
@ -23,6 +16,7 @@ body {
margin: 0;
height: 100%;
background-color: var(--home-second-color);
user-select: none;
}
.data {
@ -42,3 +36,100 @@ body {
margin-left: 5%;
margin-top: 5%;
}
#tactics-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 6px;
}
.tactic-card {
cursor: pointer;
position: relative;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
width: 32%;
}
.tactic-card-content {
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
position: absolute;
}
.tactic-card-title {
user-select: none;
padding: 0 10px 0 10px;
border-radius: 200px;
background: rgba(236, 235, 235, 0.87);
}
.tactic-card-actions * {
width: 30px;
}
.tactic-card-actions {
display: flex;
width: 100%;
height: 30px;
gap: 5px;
align-content: center;
align-items: center;
justify-content: center;
* {
maxime.batista marked this conversation as resolved
Review

While it's great to try new things, note that native CSS selector nesting has only been supported for less than a year (it's not available in the current Firefox ESR, for example).

While it's great to try new things, note that native [CSS selector nesting](https://caniuse.com/css-nesting) has only been supported for less than a year (it's not available in the current Firefox ESR, for example).
pointer-events: all;
}
}
.tactic-card-export-btn {
height: 100%;
* {
fill: var(--accent-color);
}
}
.tactic-card-remove-btn {
height: 100%;
}
.tactic-card-preview * {
pointer-events: none !important;
}
.tactic-card-duplicate-btn {
fill: #494949;
}
.tactic-card-preview {
pointer-events: none;
display: flex;
justify-content: center;
overflow: hidden;
}
.tactic-card-preview .court-container {
overflow: hidden;
}
.tactic-card-preview * {
pointer-events: none;
}
#exports-popup {
position: absolute;
width: 100%;
height: 100%;
z-index: 1000;
}

@ -16,6 +16,13 @@
border: 3px var(--home-main-color) solid;
border-radius: 0.5cap;
align-self: center;
overflow-y: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
}
#body-personal-space::-webkit-scrollbar {
display: none;
maxime.batista marked this conversation as resolved
Review
-#body-personal-space::-webkit-scrollbar {
-    display: none;
+#body-personal-space {
+    overflow-y: hidden;
```diff -#body-personal-space::-webkit-scrollbar { - display: none; +#body-personal-space { + overflow-y: hidden; ```
}
#body-personal-space > p {

@ -15,11 +15,19 @@
#side-menu-content {
width: 90%;
height: 100%;
display: flex;
flex-direction: column;
gap: 25px;
}
#tactic-import-area {
height: 100%;
}
.titre-side-menu {
border-bottom: var(--home-main-color) solid 3px;
width: 100%;
margin-bottom: 3%;
}
#side-menu .title {
@ -30,7 +38,6 @@
text-transform: uppercase;
background-color: var(--home-main-color);
padding: 3%;
margin-bottom: 0px;
margin-right: 3%;
}

@ -1,6 +1,7 @@
@import url(../theme/default.css);
#header {
user-select: none;
text-align: center;
background-color: var(--home-main-color);
margin: 0;
@ -19,6 +20,7 @@
width: 50px;
height: 50px;
border-radius: 20%;
-webkit-user-drag: none;
maxime.batista marked this conversation as resolved
Review
-    -webkit-user-drag: none;
```diff - -webkit-user-drag: none; ```
}
#header-left,

@ -1,9 +1,9 @@
@import "court.css";
@import "theme/default.css";
@import "player.css";
@import "tactic.css";
#visualizer {
position: relative;
display: flex;
height: 100%;
width: 100%;
@ -14,8 +14,12 @@
overflow: hidden;
}
#editor-div {
#visualizer-div {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
align-content: center;
}
.curtain {
@ -27,7 +31,6 @@
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
@ -70,3 +73,20 @@
overflow: scroll;
height: 100%;
}
#court-div {
height: 100%;
width: 80%;
display: flex;
align-items: center;
justify-content: center;
align-content: center;
}
#exports-popup {
position: absolute;
width: 100%;
height: 100%;
z-index: 1000;
}

@ -1,4 +1,8 @@
import { CourtType, StepContent, StepInfoNode } from "../model/tactic/Tactic.ts"
import {
CourtType,
StepContent,
StepInfoNode,
} from "../model/tactic/TacticInfo.ts"
import { useReducer } from "react"
export interface VisualizerState {

@ -2,11 +2,13 @@ import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"
import svgr from "vite-plugin-svgr"
import { visualizer } from "rollup-plugin-visualizer"
// https://vitejs.dev/config/
export default defineConfig({
build: {
target: "es2021",
cssTarget: ["chrome112"]
},
test: {
environment: "jsdom",
@ -17,5 +19,6 @@ export default defineConfig({
relativeCSSInjection: true,
}),
svgr(),
visualizer()
],
})

Loading…
Cancel
Save