diff --git a/ci/build_react.msh b/ci/build_react.msh index 203afa0..c893498 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -4,7 +4,11 @@ mkdir -p /outputs/public apt update && apt install jq -y npm install -npm run build -- --base=/IQBall/public --mode PROD + +val drone_branch = std::env("DRONE_BRANCH").unwrap() + +val base = "/IQBall/$drone_branch/public" +npm run build -- --base=$base --mode PROD // Read generated mappings from build val result = $(jq -r 'to_entries|map(.key + " " +.value.file)|.[]' dist/manifest.json) diff --git a/front/ViewRenderer.tsx b/front/ViewRenderer.tsx index 17148dc..ffaf886 100644 --- a/front/ViewRenderer.tsx +++ b/front/ViewRenderer.tsx @@ -11,8 +11,6 @@ export function renderView(Component: FunctionComponent, args: {}) { document.getElementById('root') as HTMLElement ); - console.log(args) - root.render( diff --git a/front/assets/basketball_court.svg b/front/assets/basketball_court.svg new file mode 100644 index 0000000..e0df003 --- /dev/null +++ b/front/assets/basketball_court.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/front/assets/icon/remove.svg b/front/assets/icon/remove.svg new file mode 100644 index 0000000..6886097 --- /dev/null +++ b/front/assets/icon/remove.svg @@ -0,0 +1,5 @@ + + + + diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx new file mode 100644 index 0000000..09681e7 --- /dev/null +++ b/front/components/Rack.tsx @@ -0,0 +1,58 @@ +import {ReactElement, useRef} from "react"; +import Draggable from "react-draggable"; + +export interface RackProps { + id: string, + objects: E[], + onChange: (objects: E[]) => void, + canDetach: (ref: HTMLDivElement) => boolean, + onElementDetached: (ref: HTMLDivElement, el: E) => void, + render: (e: E) => ReactElement, +} + +interface RackItemProps { + item: E, + onTryDetach: (ref: HTMLDivElement, el: E) => void, + render: (e: E) => ReactElement, +} + +/** + * A container of draggable objects + * */ +export function Rack({id, objects, onChange, canDetach, onElementDetached, render}: RackProps) { + return ( +
+ {objects.map(element => ( + { + if (!canDetach(ref)) + return + + const index = objects.findIndex(o => o.key === element.key) + onChange(objects.toSpliced(index, 1)) + + onElementDetached(ref, element) + }}/> + ))} +
+ ) +} + +function RackItem({item, onTryDetach, render}: RackItemProps) { + const divRef = useRef(null); + + return ( + onTryDetach(divRef.current!, item)}> +
+ {render(item)} +
+
+ ) +} \ No newline at end of file diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx new file mode 100644 index 0000000..7e839a8 --- /dev/null +++ b/front/components/editor/BasketCourt.tsx @@ -0,0 +1,25 @@ +import CourtSvg from '../../assets/basketball_court.svg?react'; +import '../../style/basket_court.css'; +import {useRef} from "react"; +import CourtPlayer from "./CourtPlayer"; +import {Player} from "../../data/Player"; + +export interface BasketCourtProps { + players: Player[], + onPlayerRemove: (p: Player) => void, +} + +export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) { + return ( +
+ + {players.map(player => { + return onPlayerRemove(player)} + /> + })} +
+ ) +} + diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx new file mode 100644 index 0000000..b6b64de --- /dev/null +++ b/front/components/editor/CourtPlayer.tsx @@ -0,0 +1,55 @@ +import {useRef} from "react"; +import "../../style/player.css"; +import RemoveIcon from "../../assets/icon/remove.svg?react"; +import Draggable from "react-draggable"; +import {PlayerPiece} from "./PlayerPiece"; +import {Player} from "../../data/Player"; + +export interface PlayerProps { + player: Player, + onRemove: () => void +} + +/** + * A player that is placed on the court, which can be selected, and moved in the associated bounds + * */ +export default function CourtPlayer({player, onRemove}: PlayerProps) { + + const ref = useRef(null); + + const x = player.rightRatio; + const y = player.bottomRatio; + + return ( + +
+ +
{ + if (e.key == "Delete") + onRemove() + }}> +
+ +
+ +
+
+ +
+ + ) +} \ No newline at end of file diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx new file mode 100644 index 0000000..83e7dfc --- /dev/null +++ b/front/components/editor/PlayerPiece.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import '../../style/player.css' +import {Team} from "../../data/Team"; + + +export function PlayerPiece({team, text}: { team: Team, text: string }) { + return ( +
+

{text}

+
+ ) +} \ No newline at end of file diff --git a/front/data/Player.ts b/front/data/Player.ts new file mode 100644 index 0000000..af88c1c --- /dev/null +++ b/front/data/Player.ts @@ -0,0 +1,29 @@ +import {Team} from "./Team"; + +export interface Player { + /** + * unique identifier of the player. + * This identifier must be unique to the associated court. + */ + id: number, + + /** + * the player's team + * */ + team: Team, + + /** + * player's position + * */ + role: string, + + /** + * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) + */ + bottomRatio: number + + /** + * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) + */ + rightRatio: number, +} \ No newline at end of file diff --git a/front/data/Team.tsx b/front/data/Team.tsx new file mode 100644 index 0000000..ea4c384 --- /dev/null +++ b/front/data/Team.tsx @@ -0,0 +1,4 @@ +export enum Team { + Allies = "allies", + Opponents = "opponents" +} \ No newline at end of file diff --git a/front/style/basket_court.css b/front/style/basket_court.css new file mode 100644 index 0000000..a5bc688 --- /dev/null +++ b/front/style/basket_court.css @@ -0,0 +1,20 @@ + + +#court-container { + display: flex; + + + background-color: var(--main-color); +} + +#court-svg { + margin: 5%; + user-select: none; + -webkit-user-drag: none; +} + + + +#court-svg * { + stroke: var(--selected-team-secondarycolor); +} \ No newline at end of file diff --git a/front/style/colors.css b/front/style/colors.css index 34bdbb5..f3287cb 100644 --- a/front/style/colors.css +++ b/front/style/colors.css @@ -5,4 +5,9 @@ --second-color: #ccde54; --background-color: #d2cdd3; + + --selected-team-primarycolor: #ffffff; + --selected-team-secondarycolor: #000000; + + --selection-color: #3f7fc4 } \ No newline at end of file diff --git a/front/style/editor.css b/front/style/editor.css index 2ed88d6..3aad26c 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -1,13 +1,16 @@ @import "colors.css"; -#main { +#main-div { + display: flex; height: 100%; width: 100%; background-color: var(--background-color); + + flex-direction: column; } -#topbar { +#topbar-div { display: flex; background-color: var(--main-color); @@ -15,6 +18,42 @@ align-items: stretch; } +#racks { + display: flex; + justify-content: space-between; +} + .title_input { width: 25ch; +} + +#edit-div { + height: 100%; +} + +#allies-rack .player-piece , #opponent-rack .player-piece { + margin-left: 5px; +} + +.player-piece.opponents { + background-color: #f59264; +} + +#court-div { + background-color: var(--background-color); + height: 100%; + + display: flex; + align-items: center; + justify-content: center; + align-content: center; +} + +#court-div-bounds { + width: 60%; +} + + +.react-draggable { + z-index: 2; } \ No newline at end of file diff --git a/front/style/player.css b/front/style/player.css new file mode 100644 index 0000000..264b479 --- /dev/null +++ b/front/style/player.css @@ -0,0 +1,79 @@ +/** +as the .player div content is translated, +the real .player div position is not were the user can expect. +Disable pointer events to this div as it may overlap on other components +on the court. +*/ +.player { + pointer-events: none; +} + +.player-content { + display: flex; + flex-direction: column; + align-content: center; + align-items: center; + outline: none; +} + +.player-piece { + font-family: monospace; + pointer-events: all; + + background-color: var(--selected-team-primarycolor); + color: var(--selected-team-secondarycolor); + + border-width: 2px; + border-radius: 100px; + border-style: solid; + + width: 20px; + height: 20px; + + display: flex; + + align-items: center; + justify-content: center; + + user-select: none; +} + +.player-selection-tab { + display: flex; + + position: absolute; + margin-bottom: 10%; + justify-content: center; + visibility: hidden; + + width: 100%; + transform: translateY(-20px); +} + +.player-selection-tab-remove { + pointer-events: all; + height: 25%; +} + +.player-selection-tab-remove * { + stroke: red; + fill: white; +} + +.player-selection-tab-remove:hover * { + fill: #f1dbdb; + stroke: #ff331a; + cursor: pointer; +} + +.player:focus-within .player-selection-tab { + visibility: visible; +} + +.player:focus-within .player-piece { + color: var(--selection-color); +} + +.player:focus-within { + z-index: 1000; +} \ No newline at end of file diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 84d24e6..423385f 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,19 +1,77 @@ -import React, {CSSProperties, useState} from "react"; +import {CSSProperties, useRef, useState} from "react"; import "../style/editor.css"; import TitleInput from "../components/TitleInput"; import {API} from "../Constants"; +import {BasketCourt} from "../components/editor/BasketCourt"; + +import {Rack} from "../components/Rack"; +import {PlayerPiece} from "../components/editor/PlayerPiece"; +import {Player} from "../data/Player"; +import {Team} from "../data/Team"; const ERROR_STYLE: CSSProperties = { borderColor: "red" } -export default function Editor({id, name}: { id: number, name: string }) { +/** + * information about a player that is into a rack + */ +interface RackedPlayer { + team: Team, + key: string, +} +export default function Editor({id, name}: { id: number, name: string }) { const [style, setStyle] = useState({}); + const positions = ["1", "2", "3", "4", "5"] + const [allies, setAllies] = useState( + positions.map(key => ({team: Team.Allies, key})) + ) + const [opponents, setOpponents] = useState( + positions.map(key => ({team: Team.Opponents, key})) + ) + + const [players, setPlayers] = useState([]); + const courtDivContentRef = useRef(null); + + const canDetach = (ref: HTMLDivElement) => { + const refBounds = ref.getBoundingClientRect(); + const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + + // check if refBounds overlaps courtBounds + return !( + refBounds.top > courtBounds.bottom || + refBounds.right < courtBounds.left || + refBounds.bottom < courtBounds.top || + refBounds.left > courtBounds.right + ); + } + + const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { + const refBounds = ref.getBoundingClientRect(); + const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + + const relativeXPixels = refBounds.x - courtBounds.x; + const relativeYPixels = refBounds.y - courtBounds.y; + + const xRatio = relativeXPixels / courtBounds.width; + const yRatio = relativeYPixels / courtBounds.height; + + setPlayers(players => { + return [...players, { + id: players.length, + team: element.team, + role: element.key, + rightRatio: xRatio, + bottomRatio: yRatio + }] + }) + } + return ( -
-
+
+
LEFT
{ fetch(`${API}/tactic/${id}/edit/name`, { @@ -35,6 +93,53 @@ export default function Editor({id, name}: { id: number, name: string }) { }}/>
RIGHT
+
+
+ }/> + }/> +
+
+
+ { + setPlayers(players => { + const idx = players.indexOf(player) + return players.toSpliced(idx, 1) + }) + switch (player.team) { + case Team.Opponents: + setOpponents(opponents => ( + [...opponents, { + team: player.team, + pos: player.role, + key: player.role + }] + )) + break + case Team.Allies: + setAllies(allies => ( + [...allies, { + team: player.team, + pos: player.role, + key: player.role + }] + )) + } + }}/> +
+
+
) } diff --git a/package.json b/package.json index 0eb1e79..97f0039 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "@types/react-dom": "^18.2.14", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "typescript": "^5.2.2", "vite": "^4.5.0", - "vite-plugin-css-injected-by-js": "^3.3.0", - "web-vitals": "^2.1.4" + "vite-plugin-css-injected-by-js": "^3.3.0" }, "scripts": { "start": "vite --host", @@ -29,6 +29,7 @@ ] }, "devDependencies": { - "@vitejs/plugin-react": "^4.1.0" + "@vitejs/plugin-react": "^4.1.0", + "vite-plugin-svgr": "^4.1.0" } } diff --git a/public/front b/public/front new file mode 120000 index 0000000..c1394c9 --- /dev/null +++ b/public/front @@ -0,0 +1 @@ +../front \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 9da1fb5..d01f3cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "dom.iterable", "esnext" ], - "types": ["vite/client"], + "types": ["vite/client", "vite-plugin-svgr/client"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/vite.config.ts b/vite.config.ts index 03ab8f4..4ff1dc5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import {defineConfig} from "vite"; import react from '@vitejs/plugin-react'; import fs from "fs"; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import svgr from "vite-plugin-svgr"; function resolve_entries(dirname: string): [string, string][] { @@ -38,6 +39,9 @@ export default defineConfig({ react(), cssInjectedByJsPlugin({ relativeCSSInjection: true, + }), + svgr({ + include: "**/*.svg?react" }) ] })