WordGrid/ui/src/UI.tsx

251 lines
7.6 KiB
TypeScript
Raw Normal View History

2023-08-24 03:46:43 +00:00
import * as React from "react";
import {ChangeEvent, JSX} from "react";
2023-09-23 01:24:05 +00:00
import {PlayerAndScore} from "word_grid";
2023-08-24 03:46:43 +00:00
import {
CellType,
2023-09-23 01:24:05 +00:00
cellTypeToDetails,
CoordinateData,
GridArrowData,
GridArrowDispatch,
GridArrowDispatchActionType,
HighlightableLetterData,
2023-08-24 03:46:43 +00:00
LocationType,
PlayableLetterData,
2023-09-23 01:24:05 +00:00
TileDispatch,
TileDispatchActionType,
2023-08-24 03:46:43 +00:00
} from "./utils";
2023-09-23 01:24:05 +00:00
export function TileSlot(props: {
tile?: React.JSX.Element | undefined,
location: CoordinateData,
tileDispatch: TileDispatch,
arrowDispatch?: GridArrowDispatch,
}): React.JSX.Element {
2023-08-24 03:46:43 +00:00
let isDraggable = props.tile !== undefined;
function onDragStart(e: React.DragEvent<HTMLDivElement>) {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("wordGrid/coords", JSON.stringify(props.location));
}
function onDrop(e: React.DragEvent<HTMLDivElement>) {
const startLocation: CoordinateData = JSON.parse(e.dataTransfer.getData("wordGrid/coords"));
const thisLocation = props.location;
2023-09-23 01:24:05 +00:00
props.tileDispatch({action: TileDispatchActionType.MOVE, start: startLocation, end: thisLocation});
2023-08-24 03:46:43 +00:00
}
let className = "tileSpot";
if (props.location.location === LocationType.GRID) {
className += " ephemeral";
}
2023-09-23 01:24:05 +00:00
let onClick: () => void;
if(props.arrowDispatch != null && props.location.location == LocationType.GRID) {
onClick = () => {
props.arrowDispatch({action: GridArrowDispatchActionType.CYCLE, position: props.location.index});
}
} else if(props.location.location == LocationType.TRAY && props.tile != null && props.tile.props.data.text != ' ' && props.tile.props.data.text != '') {
onClick = () => {
props.tileDispatch({
action: TileDispatchActionType.MOVE_TO_ARROW, end: undefined, start: props.location,
});
}
}
2023-08-24 03:46:43 +00:00
return <div className={className}
draggable={isDraggable}
onDragStart={onDragStart}
onDrop={onDrop}
2023-09-23 01:24:05 +00:00
onClick={onClick}
2023-08-24 03:46:43 +00:00
onDragOver={(e) => {e.preventDefault()}}
>
{props.tile}
</div>
}
2023-09-15 02:38:34 +00:00
export function Letter(props: { data: HighlightableLetterData, letterUpdater?: (value: string) => void }): React.JSX.Element {
2023-08-24 03:46:43 +00:00
function modifyThisLetter(value:string){
props.letterUpdater(value);
}
function onBlankInput(e: ChangeEvent<HTMLInputElement>){
let value = e.target.value.toUpperCase();
if(value.length > 1){
value = value[value.length - 1];
} else if(value.length == 0){
modifyThisLetter(value);
return;
}
// Now check that it's a letter
let is_letter = value.match("[A-Z]") != null;
if(is_letter){
modifyThisLetter(value);
} else {
// Cancel and do nothing
e.preventDefault();
}
}
if(props.data.is_blank && props.data.ephemeral) {
return <div className="letter">
<input className="blank-input" type="text" onChange={onBlankInput} value={props.data.text} />
<div className="letter-points">{props.data.points}</div>
</div>
} else {
let className = "text";
if (props.data.is_blank) { // not ephemeral
className += " prev-blank";
}
2023-09-15 02:38:34 +00:00
let letterClassName = "letter";
if (props.data.highlight) {
letterClassName += " highlight";
}
return <div className={letterClassName}>
2023-08-24 03:46:43 +00:00
<div className={className}>{props.data.text}</div>
<div className="letter-points">{props.data.points}</div>
</div>
}
}
2023-09-23 01:24:05 +00:00
export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength: number, trayDispatch: TileDispatch }): React.JSX.Element {
2023-08-24 03:46:43 +00:00
let elements: JSX.Element[] = [];
for (let i=0; i<props.trayLength; i++) {
elements.push(
<TileSlot
key={"empty" + i}
location={{location: LocationType.TRAY, index: i}}
2023-09-23 01:24:05 +00:00
tileDispatch={props.trayDispatch} />
2023-08-24 03:46:43 +00:00
);
}
props.letters
.filter((x) => {return x !== undefined;})
.forEach((letter, i) => {
if (letter.location === LocationType.TRAY) {
elements[letter.index] =
<TileSlot
tile={<Letter
2023-09-23 01:24:05 +00:00
data={{...letter, highlight: false}}
2023-08-24 03:46:43 +00:00
letterUpdater={(value) => {
2023-09-23 01:24:05 +00:00
props.trayDispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value})
2023-08-24 03:46:43 +00:00
}}
/>}
key={"letter" + letter.index}
location={{location: LocationType.TRAY, index: letter.index}}
2023-09-23 01:24:05 +00:00
tileDispatch={props.trayDispatch} />
2023-08-24 03:46:43 +00:00
}
});
return (
<div className="tray">
{elements}
</div>
)
}
2023-09-23 01:24:05 +00:00
export function Arrow(props: {
data: GridArrowData,
dispatch: GridArrowDispatch,
}) {
return <div
className={`arrow ${props.data.direction}`}
>
</div>;
}
2023-08-24 03:46:43 +00:00
export function Grid(props: {
cellTypes: CellType[],
playerLetters: Array<PlayableLetterData>,
2023-09-15 02:38:34 +00:00
boardLetters: HighlightableLetterData[],
2023-09-23 01:24:05 +00:00
tileDispatch: TileDispatch,
arrow: GridArrowData,
arrowDispatch: GridArrowDispatch,
}) {
2023-08-24 03:46:43 +00:00
const elements = props.cellTypes.map((ct, i) => {
const {className, text} = cellTypeToDetails(ct);
let tileElement: JSX.Element;
if (props.boardLetters[i] !== undefined) {
tileElement = <Letter data={props.boardLetters[i]} />;
} else {
tileElement = <>
<span>{text}</span>
<TileSlot
2023-09-23 01:24:05 +00:00
location={{location: LocationType.GRID, index: i}}
tileDispatch={props.tileDispatch}
arrowDispatch={props.arrowDispatch}
/>
</>;
2023-08-24 03:46:43 +00:00
}
2023-09-23 01:24:05 +00:00
let arrowElement: JSX.Element;
if(props.arrow != null && props.arrow.position == i) {
arrowElement = <Arrow data={props.arrow} dispatch={props.arrowDispatch} />;
}
2023-08-24 03:46:43 +00:00
2023-09-23 01:24:05 +00:00
return <div key={i} className={"grid-spot " + className}>
2023-08-24 03:46:43 +00:00
{tileElement}
2023-09-23 01:24:05 +00:00
{arrowElement}
2023-08-24 03:46:43 +00:00
</div>;
});
props.playerLetters
.filter((letter) => {return letter !== undefined})
.forEach((letter, i) => {
if (letter.location === LocationType.GRID) {
const ct = props.cellTypes[letter.index];
2023-08-24 04:58:34 +00:00
const {className} = cellTypeToDetails(ct);
2023-08-24 03:46:43 +00:00
elements[letter.index] =
<div key={"letter" + letter.index} className={"grid-spot " + className}>
<TileSlot
tile={<Letter
2023-09-23 01:24:05 +00:00
data={{...letter, highlight: false}}
2023-08-24 03:46:43 +00:00
letterUpdater={(value) => {
2023-09-23 01:24:05 +00:00
props.tileDispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value});
2023-08-24 03:46:43 +00:00
}}
/>}
location={{location: LocationType.GRID, index: letter.index}}
2023-09-23 01:24:05 +00:00
tileDispatch={props.tileDispatch} />
2023-08-24 03:46:43 +00:00
</div>;
}
});
return <div className="board-grid">
{elements}
</div>
}
export function Scores(props: {playerScores: Array<PlayerAndScore>}){
2023-08-24 04:58:34 +00:00
let elements = props.playerScores.map((ps) => {
2023-08-24 03:46:43 +00:00
return <div key={ps.name}>
<h3>{ps.name}</h3>
<span>{ps.score}</span>
</div>;
});
return <div className="scoring">
{elements}
</div>
}