WIP
This commit is contained in:
@@ -6,9 +6,9 @@ This repo contains the source code for [sharechess.github.io](https://sharechess
|
|||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
[ShareChess](https://sharechess.github.io/) is an open source website that allows you to share chess games as self-contained replay links (the whole game is stored in the url without the need for a database), PNG images, or GIF / MP4 / WebM animations.
|
[ShareChess](https://sharechess.github.io/) is a free, open source website that allows you to share chess games as self-contained replay links (the whole game is stored in the url without the need for a database), PNG images, or GIF / MP4 / WebM animations.
|
||||||
|
|
||||||
The website provides a high variety of chessboard and piece designs to serve as an open alternative for commercial chess GIF makers. The projects is free and open source.
|
The website provides a high variety of chessboard and piece designs to serve as an open alternative for commercial chess GIF makers.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
25
src/boot/loadFromUrl.ts
Normal file
25
src/boot/loadFromUrl.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Handlers } from "./../types";
|
||||||
|
import { setState } from "../state";
|
||||||
|
import link from "../persistance/link";
|
||||||
|
|
||||||
|
const loadFromUrl = async (refreshHash: boolean, handlers: Handlers) => {
|
||||||
|
setState("refreshHash", refreshHash);
|
||||||
|
const { pgn, fen, side, ply } = await link.read();
|
||||||
|
|
||||||
|
await (pgn
|
||||||
|
? handlers.loadPGN(pgn, side, ply)
|
||||||
|
: fen
|
||||||
|
? handlers.loadFEN(fen)
|
||||||
|
: handlers.loadFEN(
|
||||||
|
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
||||||
|
false
|
||||||
|
));
|
||||||
|
|
||||||
|
if (ply !== 0) {
|
||||||
|
handlers.goto(ply);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState("refreshHash", true);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default loadFromUrl;
|
||||||
80
src/boot/registerEvents.ts
Normal file
80
src/boot/registerEvents.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Handlers } from "./../types";
|
||||||
|
import { state, setState } from "../state";
|
||||||
|
import loadFromUrl from "./loadFromUrl";
|
||||||
|
import readFile from "../utils/readFile";
|
||||||
|
|
||||||
|
const registerEvents = (handlers: Handlers) => {
|
||||||
|
document.addEventListener("dblclick", function (el) {
|
||||||
|
el.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
setState(
|
||||||
|
"layout",
|
||||||
|
window.innerWidth < window.innerHeight
|
||||||
|
? "single"
|
||||||
|
: window.innerWidth < 1366
|
||||||
|
? "double"
|
||||||
|
: "triple"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("hashchange", () => {
|
||||||
|
if (!state.refreshHash) {
|
||||||
|
setState("refreshHash", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFromUrl(true, handlers);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!state.mobile) {
|
||||||
|
const keyMapping: { [key: string]: () => void } = {
|
||||||
|
ArrowLeft: handlers.prev,
|
||||||
|
ArrowRight: handlers.next,
|
||||||
|
ArrowUp: handlers.first,
|
||||||
|
ArrowDown: handlers.last,
|
||||||
|
f: handlers.flip,
|
||||||
|
" ": handlers.togglePlay,
|
||||||
|
Enter: handlers.openOnLichess,
|
||||||
|
l: handlers.loadFromClipboard.bind(handlers),
|
||||||
|
a: handlers.toggleAnonymous,
|
||||||
|
b: handlers.toggleBorder,
|
||||||
|
i: handlers.toggleExtraInfo,
|
||||||
|
h: handlers.toggleTitleScreen,
|
||||||
|
s: handlers.toggleShadows,
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", (e) => {
|
||||||
|
const target = e.target as HTMLElement | null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
keyMapping[e.key] &&
|
||||||
|
target?.nodeName !== "INPUT" &&
|
||||||
|
target?.nodeName !== "TEXTAREA"
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
keyMapping[e.key]();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const preventDefaults = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||||
|
document.addEventListener(eventName, preventDefaults, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("drop", async (e) => {
|
||||||
|
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
||||||
|
const content = await readFile(e.dataTransfer.files[0]);
|
||||||
|
setState("refreshHash", false);
|
||||||
|
handlers.loadPGN(content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default registerEvents;
|
||||||
247
src/boot/registerHandlers.ts
Normal file
247
src/boot/registerHandlers.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
import { BoardStyle, Handlers } from "../types";
|
||||||
|
|
||||||
|
import Board from "../board/Board";
|
||||||
|
import Game from "../game/Game";
|
||||||
|
import Player from "../player/Player";
|
||||||
|
|
||||||
|
import { state, setState } from "../state";
|
||||||
|
import saveConfig from "../persistance/saveConfig";
|
||||||
|
|
||||||
|
import createImage from "../encoders/createImage";
|
||||||
|
import createAnimation from "../encoders/createAnimation";
|
||||||
|
import download from "../utils/download";
|
||||||
|
import importFromLink from "../imports/importFromLink";
|
||||||
|
import isFEN from "../utils/isFEN";
|
||||||
|
import isPGN from "../utils/isPGN";
|
||||||
|
import isSafeLink from "../utils/isSafeLink";
|
||||||
|
import { PiecesStyle } from "../board/styles-pieces/piecesStyles";
|
||||||
|
import link from "../persistance/link";
|
||||||
|
import importToLichess from "../imports/importToLichess";
|
||||||
|
|
||||||
|
const registerHandlers = (player: Player, board: Board): Handlers => {
|
||||||
|
return {
|
||||||
|
prev() {
|
||||||
|
player.pause();
|
||||||
|
player.prev();
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
player.pause();
|
||||||
|
player.next();
|
||||||
|
},
|
||||||
|
first() {
|
||||||
|
player.pause();
|
||||||
|
player.first();
|
||||||
|
},
|
||||||
|
last() {
|
||||||
|
player.pause();
|
||||||
|
player.last();
|
||||||
|
},
|
||||||
|
togglePlay() {
|
||||||
|
player.playing ? player.pause() : player.play();
|
||||||
|
},
|
||||||
|
goto(ply: number) {
|
||||||
|
player.pause();
|
||||||
|
player.goto(ply);
|
||||||
|
},
|
||||||
|
toggleBorder() {
|
||||||
|
board.toggleBorder();
|
||||||
|
setState("boardConfig", "showBorder", !state.boardConfig.showBorder);
|
||||||
|
saveConfig("board");
|
||||||
|
},
|
||||||
|
showBorder() {
|
||||||
|
board.showBorder();
|
||||||
|
setState("boardConfig", "showBorder", true);
|
||||||
|
saveConfig("board");
|
||||||
|
},
|
||||||
|
hideBorder() {
|
||||||
|
board.hideBorder();
|
||||||
|
setState("boardConfig", "showBorder", false);
|
||||||
|
saveConfig("board");
|
||||||
|
},
|
||||||
|
toggleExtraInfo() {
|
||||||
|
board.toggleExtraInfo();
|
||||||
|
setState(
|
||||||
|
"boardConfig",
|
||||||
|
"showExtraInfo",
|
||||||
|
!state.boardConfig.showExtraInfo
|
||||||
|
);
|
||||||
|
saveConfig("board");
|
||||||
|
},
|
||||||
|
toggleAnonymous() {
|
||||||
|
setState("anonymous", !state.anonymous);
|
||||||
|
board.anonymous = state.anonymous;
|
||||||
|
|
||||||
|
if (state.pgn !== "") {
|
||||||
|
const pgn = state.anonymous ? state.game.anonymousPGN : state.game.pgn;
|
||||||
|
link.set({ pgn });
|
||||||
|
setState("refreshHash", false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleTitleScreen() {
|
||||||
|
setState("gameConfig", "titleScreen", !state.gameConfig.titleScreen);
|
||||||
|
saveConfig("game");
|
||||||
|
},
|
||||||
|
toggleShadows() {
|
||||||
|
board.toggleShadows();
|
||||||
|
setState("boardConfig", "showShadows", !state.boardConfig.showShadows);
|
||||||
|
saveConfig("board");
|
||||||
|
},
|
||||||
|
flip() {
|
||||||
|
board.flip();
|
||||||
|
setState("boardConfig", "flipped", !state.boardConfig.flipped);
|
||||||
|
setState("refreshHash", false);
|
||||||
|
link.set({ side: state.boardConfig.flipped ? "b" : "w" });
|
||||||
|
},
|
||||||
|
changeBoardStyle(style: BoardStyle) {
|
||||||
|
board.setStyle(style);
|
||||||
|
setState("boardConfig", "boardStyle", style);
|
||||||
|
saveConfig("board");
|
||||||
|
},
|
||||||
|
changePiecesStyle(style: PiecesStyle) {
|
||||||
|
board.setPiecesStyle(style);
|
||||||
|
setState("boardConfig", "piecesStyle", style);
|
||||||
|
saveConfig("board");
|
||||||
|
},
|
||||||
|
async loadPGN(pgn: string, side: "w" | "b" = "w", ply: number = 0) {
|
||||||
|
const game = new Game().loadPGN(pgn);
|
||||||
|
setState({
|
||||||
|
pgn: game.pgn,
|
||||||
|
fen: "",
|
||||||
|
moves: game.getMoves(),
|
||||||
|
ply: 0,
|
||||||
|
game,
|
||||||
|
});
|
||||||
|
link.set({ pgn: game.pgn, side, ply });
|
||||||
|
|
||||||
|
await player.load(game);
|
||||||
|
setState("activeTab", "game");
|
||||||
|
|
||||||
|
if (side === "w") {
|
||||||
|
board.flipWhite();
|
||||||
|
} else {
|
||||||
|
board.flipBlack();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState("boardConfig", "flipped", side === "b");
|
||||||
|
|
||||||
|
document.title = `ShareChess - ${game.getTitle({ anonymous: false })}`;
|
||||||
|
},
|
||||||
|
async loadFEN(fen: string, hash = true) {
|
||||||
|
const game = new Game().loadFEN(fen);
|
||||||
|
setState({
|
||||||
|
pgn: "",
|
||||||
|
fen,
|
||||||
|
moves: game.getMoves(),
|
||||||
|
ply: 0,
|
||||||
|
game,
|
||||||
|
});
|
||||||
|
|
||||||
|
await player.load(game);
|
||||||
|
|
||||||
|
if (hash) {
|
||||||
|
link.set({ fen: state.fen });
|
||||||
|
setState("activeTab", "game");
|
||||||
|
}
|
||||||
|
|
||||||
|
const side = game.getPosition(0).turn;
|
||||||
|
|
||||||
|
if (side === "w") {
|
||||||
|
board.flipWhite();
|
||||||
|
} else {
|
||||||
|
board.flipBlack();
|
||||||
|
}
|
||||||
|
|
||||||
|
setState("boardConfig", "flipped", side === "b");
|
||||||
|
|
||||||
|
document.title = `ShareChess - FEN ${fen}`;
|
||||||
|
},
|
||||||
|
async load(data: string) {
|
||||||
|
setState("refreshHash", false);
|
||||||
|
|
||||||
|
if (isFEN(data)) {
|
||||||
|
await this.loadFEN(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPGN(data)) {
|
||||||
|
await this.loadPGN(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSafeLink(data)) {
|
||||||
|
await this.importPGN(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
async loadFromClipboard() {
|
||||||
|
const clip = await navigator.clipboard.readText();
|
||||||
|
return this.load(clip);
|
||||||
|
},
|
||||||
|
async importPGN(link: string) {
|
||||||
|
const result = await importFromLink(link);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadPGN(result.pgn, result.side);
|
||||||
|
},
|
||||||
|
async downloadImage() {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
const data = await createImage(
|
||||||
|
state.fen,
|
||||||
|
state.pgn,
|
||||||
|
state.ply,
|
||||||
|
state.boardConfig,
|
||||||
|
state.gameConfig.picSize
|
||||||
|
);
|
||||||
|
download(data, `fen_${Date.now()}`, "png");
|
||||||
|
},
|
||||||
|
async downloadAnimation() {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
const data = await createAnimation(
|
||||||
|
state.pgn,
|
||||||
|
state.boardConfig,
|
||||||
|
state.gameConfig.format,
|
||||||
|
state.gameConfig.animationSize,
|
||||||
|
state.gameConfig.titleScreen
|
||||||
|
);
|
||||||
|
|
||||||
|
const name = state.game.getFileName(state.anonymous);
|
||||||
|
|
||||||
|
download(data, name, state.gameConfig.format.toLowerCase());
|
||||||
|
},
|
||||||
|
toggleSound() {
|
||||||
|
setState("siteConfig", "sounds", !state.siteConfig.sounds);
|
||||||
|
saveConfig("site");
|
||||||
|
},
|
||||||
|
toggleSpeech() {
|
||||||
|
setState("siteConfig", "speech", !state.siteConfig.speech);
|
||||||
|
saveConfig("site");
|
||||||
|
},
|
||||||
|
toggleDarkMode() {
|
||||||
|
setState("siteConfig", "darkMode", !state.siteConfig.darkMode);
|
||||||
|
saveConfig("site");
|
||||||
|
},
|
||||||
|
async openOnLichess() {
|
||||||
|
if (state.pgn === "") {
|
||||||
|
window.open(
|
||||||
|
`https://lichess.org/analysis/${state.fen.replace(/\s+/g, "_")}`
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = await importToLichess(state.pgn, state.game.header.Site);
|
||||||
|
window.open(`${url}/${state.boardConfig.flipped ? "black" : ""}`);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default registerHandlers;
|
||||||
@@ -50,4 +50,6 @@ const importFromLink = async (link: string): Promise<Result> => {
|
|||||||
return { error: true, errorType: "INCORRECT_LINK" };
|
return { error: true, errorType: "INCORRECT_LINK" };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export { importFromLichess };
|
||||||
|
|
||||||
export default importFromLink;
|
export default importFromLink;
|
||||||
|
|||||||
344
src/main.tsx
344
src/main.tsx
@@ -1,262 +1,32 @@
|
|||||||
import WebFont from "webfontloader";
|
import WebFont from "webfontloader";
|
||||||
import { render } from "solid-js/web";
|
import { render } from "solid-js/web";
|
||||||
|
|
||||||
import { BoardStyle } from "./types";
|
|
||||||
|
|
||||||
import Board from "./board/Board";
|
import Board from "./board/Board";
|
||||||
import Game from "./game/Game";
|
|
||||||
import Player from "./player/Player";
|
import Player from "./player/Player";
|
||||||
import App from "./ui/App";
|
import App from "./ui/App";
|
||||||
|
|
||||||
import { state, setState } from "./state";
|
import { state, setState } from "./state";
|
||||||
import saveConfig from "./persistance/saveConfig";
|
|
||||||
|
|
||||||
import createImage from "./encoders/createImage";
|
|
||||||
import createAnimation from "./encoders/createAnimation";
|
|
||||||
import readFile from "./utils/readFile";
|
|
||||||
import download from "./utils/download";
|
|
||||||
import { compressPGN } from "./game/PGNHelpers";
|
|
||||||
import importFromLink from "./imports/importFromLink";
|
|
||||||
import isFEN from "./utils/isFEN";
|
|
||||||
import isPGN from "./utils/isPGN";
|
|
||||||
import isSafeLink from "./utils/isSafeLink";
|
|
||||||
import { PiecesStyle } from "./board/styles-pieces/piecesStyles";
|
|
||||||
import link from "./persistance/link";
|
import link from "./persistance/link";
|
||||||
import importToLichess from "./imports/importToLichess";
|
import registerHandlers from "./boot/registerHandlers";
|
||||||
|
import loadFromUrl from "./boot/loadFromUrl";
|
||||||
|
import registerEvents from "./boot/registerEvents";
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const board = new Board(state.boardConfig);
|
const board = new Board(state.boardConfig);
|
||||||
const player = new Player(board, state.gameConfig);
|
const player = new Player(board, state.gameConfig);
|
||||||
|
|
||||||
|
/* Connect player to the state */
|
||||||
|
|
||||||
player.watch((playing) => setState("playing", playing));
|
player.watch((playing) => setState("playing", playing));
|
||||||
|
|
||||||
|
/* Load game from url hash */
|
||||||
|
|
||||||
link.read();
|
link.read();
|
||||||
|
|
||||||
/* Register handlers */
|
/* Register handlers */
|
||||||
|
|
||||||
const handlers = {
|
const handlers = registerHandlers(player, board);
|
||||||
prev() {
|
|
||||||
player.pause();
|
|
||||||
player.prev();
|
|
||||||
},
|
|
||||||
next() {
|
|
||||||
player.pause();
|
|
||||||
player.next();
|
|
||||||
},
|
|
||||||
first() {
|
|
||||||
player.pause();
|
|
||||||
player.first();
|
|
||||||
},
|
|
||||||
last() {
|
|
||||||
player.pause();
|
|
||||||
player.last();
|
|
||||||
},
|
|
||||||
togglePlay() {
|
|
||||||
player.playing ? player.pause() : player.play();
|
|
||||||
},
|
|
||||||
goto(ply: number) {
|
|
||||||
player.pause();
|
|
||||||
player.goto(ply);
|
|
||||||
},
|
|
||||||
toggleBorder() {
|
|
||||||
board.toggleBorder();
|
|
||||||
setState("boardConfig", "showBorder", !state.boardConfig.showBorder);
|
|
||||||
saveConfig("board");
|
|
||||||
},
|
|
||||||
showBorder() {
|
|
||||||
board.showBorder();
|
|
||||||
setState("boardConfig", "showBorder", true);
|
|
||||||
saveConfig("board");
|
|
||||||
},
|
|
||||||
hideBorder() {
|
|
||||||
board.hideBorder();
|
|
||||||
setState("boardConfig", "showBorder", false);
|
|
||||||
saveConfig("board");
|
|
||||||
},
|
|
||||||
toggleExtraInfo() {
|
|
||||||
board.toggleExtraInfo();
|
|
||||||
setState(
|
|
||||||
"boardConfig",
|
|
||||||
"showExtraInfo",
|
|
||||||
!state.boardConfig.showExtraInfo
|
|
||||||
);
|
|
||||||
saveConfig("board");
|
|
||||||
},
|
|
||||||
toggleAnonymous() {
|
|
||||||
setState("anonymous", !state.anonymous);
|
|
||||||
board.anonymous = state.anonymous;
|
|
||||||
|
|
||||||
if (state.pgn !== "") {
|
|
||||||
const pgn = state.anonymous ? state.game.anonymousPGN : state.game.pgn;
|
|
||||||
window.location.hash = `pgn/${compressPGN(pgn)}`;
|
|
||||||
setState("refreshHash", false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleTitleScreen() {
|
|
||||||
setState("gameConfig", "titleScreen", !state.gameConfig.titleScreen);
|
|
||||||
saveConfig("game");
|
|
||||||
},
|
|
||||||
toggleShadows() {
|
|
||||||
board.toggleShadows();
|
|
||||||
setState("boardConfig", "showShadows", !state.boardConfig.showShadows);
|
|
||||||
saveConfig("board");
|
|
||||||
},
|
|
||||||
flip() {
|
|
||||||
console.log("FLIP");
|
|
||||||
board.flip();
|
|
||||||
setState("boardConfig", "flipped", !state.boardConfig.flipped);
|
|
||||||
setState("refreshHash", false);
|
|
||||||
link.set({ side: state.boardConfig.flipped ? "b" : "w" });
|
|
||||||
},
|
|
||||||
changeBoardStyle(style: BoardStyle) {
|
|
||||||
board.setStyle(style);
|
|
||||||
setState("boardConfig", "boardStyle", style);
|
|
||||||
saveConfig("board");
|
|
||||||
},
|
|
||||||
changePiecesStyle(style: PiecesStyle) {
|
|
||||||
board.setPiecesStyle(style);
|
|
||||||
setState("boardConfig", "piecesStyle", style);
|
|
||||||
saveConfig("board");
|
|
||||||
},
|
|
||||||
async loadPGN(pgn: string, side: "w" | "b" = "w", ply: number = 0) {
|
|
||||||
const game = new Game().loadPGN(pgn);
|
|
||||||
setState({
|
|
||||||
pgn: game.pgn,
|
|
||||||
fen: "",
|
|
||||||
moves: game.getMoves(),
|
|
||||||
ply: 0,
|
|
||||||
game,
|
|
||||||
});
|
|
||||||
link.set({ pgn: game.pgn, side, ply });
|
|
||||||
|
|
||||||
await player.load(game);
|
|
||||||
setState("activeTab", "game");
|
|
||||||
|
|
||||||
if (side === "w") {
|
|
||||||
board.flipWhite();
|
|
||||||
} else {
|
|
||||||
board.flipBlack();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState("boardConfig", "flipped", side === "b");
|
|
||||||
|
|
||||||
document.title = `ShareChess - ${game.getTitle({ anonymous: false })}`;
|
|
||||||
},
|
|
||||||
async loadFEN(fen: string, hash = true) {
|
|
||||||
const game = new Game().loadFEN(fen);
|
|
||||||
setState({
|
|
||||||
pgn: "",
|
|
||||||
fen,
|
|
||||||
moves: game.getMoves(),
|
|
||||||
ply: 0,
|
|
||||||
game,
|
|
||||||
});
|
|
||||||
|
|
||||||
await player.load(game);
|
|
||||||
|
|
||||||
if (hash) {
|
|
||||||
link.set({ fen: state.fen });
|
|
||||||
setState("activeTab", "game");
|
|
||||||
}
|
|
||||||
|
|
||||||
const side = game.getPosition(0).turn;
|
|
||||||
|
|
||||||
if (side === "w") {
|
|
||||||
board.flipWhite();
|
|
||||||
} else {
|
|
||||||
board.flipBlack();
|
|
||||||
}
|
|
||||||
|
|
||||||
setState("boardConfig", "flipped", side === "b");
|
|
||||||
|
|
||||||
document.title = `ShareChess - FEN ${fen}`;
|
|
||||||
},
|
|
||||||
async load(data: string) {
|
|
||||||
setState("refreshHash", false);
|
|
||||||
|
|
||||||
if (isFEN(data)) {
|
|
||||||
await this.loadFEN(data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPGN(data)) {
|
|
||||||
await this.loadPGN(data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSafeLink(data)) {
|
|
||||||
await this.importPGN(data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
async importPGN(link: string) {
|
|
||||||
const result = await importFromLink(link);
|
|
||||||
|
|
||||||
if (result.error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.loadPGN(result.pgn, result.side);
|
|
||||||
},
|
|
||||||
async downloadImage() {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
const data = await createImage(
|
|
||||||
state.fen,
|
|
||||||
state.pgn,
|
|
||||||
state.ply,
|
|
||||||
state.boardConfig,
|
|
||||||
state.gameConfig.picSize
|
|
||||||
);
|
|
||||||
download(data, `fen_${Date.now()}`, "png");
|
|
||||||
},
|
|
||||||
async downloadAnimation() {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
||||||
const data = await createAnimation(
|
|
||||||
state.pgn,
|
|
||||||
state.boardConfig,
|
|
||||||
state.gameConfig.format,
|
|
||||||
state.gameConfig.animationSize,
|
|
||||||
state.gameConfig.titleScreen
|
|
||||||
);
|
|
||||||
|
|
||||||
const name = state.game.getFileName(state.anonymous);
|
|
||||||
|
|
||||||
download(data, name, state.gameConfig.format.toLowerCase());
|
|
||||||
},
|
|
||||||
toggleSound() {
|
|
||||||
setState("siteConfig", "sounds", !state.siteConfig.sounds);
|
|
||||||
saveConfig("site");
|
|
||||||
},
|
|
||||||
toggleSpeech() {
|
|
||||||
setState("siteConfig", "speech", !state.siteConfig.speech);
|
|
||||||
saveConfig("site");
|
|
||||||
},
|
|
||||||
toggleDarkMode() {
|
|
||||||
setState("siteConfig", "darkMode", !state.siteConfig.darkMode);
|
|
||||||
saveConfig("site");
|
|
||||||
},
|
|
||||||
async openOnLichess() {
|
|
||||||
if (state.pgn === "") {
|
|
||||||
window.open(
|
|
||||||
`https://lichess.org/analysis/${state.fen.replace(/\s+/g, "_")}`
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = await importToLichess(state.pgn, state.game.header.Site);
|
|
||||||
window.open(`${url}/${state.boardConfig.flipped ? "black" : ""}`);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
window.handlers = handlers;
|
|
||||||
|
|
||||||
/* Render the page */
|
/* Render the page */
|
||||||
|
|
||||||
@@ -265,105 +35,21 @@ const main = async () => {
|
|||||||
document.getElementById("root") as HTMLElement
|
document.getElementById("root") as HTMLElement
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* Connect canvas */
|
||||||
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
|
||||||
|
|
||||||
await board.setCanvas(canvas);
|
await board.setCanvas(canvas);
|
||||||
|
|
||||||
|
/* Initialize the game */
|
||||||
|
|
||||||
await player.init();
|
await player.init();
|
||||||
|
await loadFromUrl(false, handlers);
|
||||||
/* Load game from the url */
|
|
||||||
|
|
||||||
const loadFromUrl = async (refreshHash: boolean = true) => {
|
|
||||||
setState("refreshHash", refreshHash);
|
|
||||||
const { pgn, fen, side, ply } = link.read();
|
|
||||||
|
|
||||||
await (pgn
|
|
||||||
? handlers.loadPGN(pgn, side, ply)
|
|
||||||
: fen
|
|
||||||
? handlers.loadFEN(fen)
|
|
||||||
: handlers.loadFEN(
|
|
||||||
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
|
|
||||||
false
|
|
||||||
));
|
|
||||||
|
|
||||||
if (ply !== 0) {
|
|
||||||
handlers.goto(ply);
|
|
||||||
}
|
|
||||||
|
|
||||||
setState("refreshHash", true);
|
|
||||||
};
|
|
||||||
|
|
||||||
await loadFromUrl(false);
|
|
||||||
|
|
||||||
/* Register events */
|
/* Register events */
|
||||||
document.addEventListener("dblclick", function (el) {
|
|
||||||
el.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
registerEvents(handlers);
|
||||||
setState(
|
|
||||||
"layout",
|
|
||||||
window.innerWidth < window.innerHeight
|
|
||||||
? "single"
|
|
||||||
: window.innerWidth < 1366
|
|
||||||
? "double"
|
|
||||||
: "triple"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("hashchange", () => {
|
|
||||||
if (!state.refreshHash) {
|
|
||||||
setState("refreshHash", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFromUrl();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!state.mobile) {
|
|
||||||
const keyMapping: { [key: string]: () => void } = {
|
|
||||||
ArrowLeft: handlers.prev,
|
|
||||||
ArrowRight: handlers.next,
|
|
||||||
ArrowUp: handlers.first,
|
|
||||||
ArrowDown: handlers.last,
|
|
||||||
" ": handlers.togglePlay,
|
|
||||||
b: handlers.toggleBorder,
|
|
||||||
f: handlers.flip,
|
|
||||||
e: handlers.toggleExtraInfo,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", (e) => {
|
/* Initialize */
|
||||||
const target = e.target as HTMLElement | null;
|
|
||||||
|
|
||||||
if (
|
|
||||||
keyMapping[e.key] &&
|
|
||||||
target?.nodeName !== "INPUT" &&
|
|
||||||
target?.nodeName !== "TEXTAREA"
|
|
||||||
) {
|
|
||||||
e.preventDefault();
|
|
||||||
keyMapping[e.key]();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const preventDefaults = (e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
|
||||||
document.addEventListener(eventName, preventDefaults, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("drop", async (e) => {
|
|
||||||
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
|
||||||
const content = await readFile(e.dataTransfer.files[0]);
|
|
||||||
setState("refreshHash", false);
|
|
||||||
handlers.loadPGN(content);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Boot */
|
|
||||||
|
|
||||||
Promise.all([
|
Promise.all([
|
||||||
new Promise((resolve) =>
|
new Promise((resolve) =>
|
||||||
|
|||||||
7
src/persistance/clearConfig.ts
Normal file
7
src/persistance/clearConfig.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const clearConfig = () => {
|
||||||
|
localStorage.removeItem("boardConfig");
|
||||||
|
localStorage.removeItem("gameConfig");
|
||||||
|
localStorage.removeItem("siteConfig");
|
||||||
|
};
|
||||||
|
|
||||||
|
export default clearConfig;
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// import { decompressPGN } from "../game/PGNHelpers";
|
|
||||||
|
|
||||||
// const HEADER_REGEX = /^#(pgn|fen)\//;
|
|
||||||
|
|
||||||
// const extractUrlData = () => {
|
|
||||||
// const hash = window.location.hash;
|
|
||||||
|
|
||||||
// if (!HEADER_REGEX.test(hash)) {
|
|
||||||
// return {
|
|
||||||
// pgn: "",
|
|
||||||
// fen: "",
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const [format, ...chunks] = hash.slice(1).split("/");
|
|
||||||
|
|
||||||
// const data = chunks.join("/");
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// pgn: format === "pgn" ? decompressPGN(data) : "",
|
|
||||||
// fen: format === "fen" ? decodeURI(data) : "",
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// export default extractUrlData;
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { compressPGN, decompressPGN } from "../game/PGNHelpers";
|
import { cleanPGN, compressPGN, decompressPGN } from "../game/PGNHelpers";
|
||||||
|
import { importFromLichess } from "../imports/importFromLink";
|
||||||
|
|
||||||
type LinkData = {
|
type LinkData = {
|
||||||
pgn: string;
|
pgn: string;
|
||||||
@@ -29,7 +30,7 @@ const link = {
|
|||||||
linkData = { ...defaultLinkData } as LinkData;
|
linkData = { ...defaultLinkData } as LinkData;
|
||||||
linkData.fen = data.fen;
|
linkData.fen = data.fen;
|
||||||
|
|
||||||
location.hash = `fen/${linkData.fen}`;
|
location.hash = `fen/${linkData.fen.replace(/ /g, "_")}`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,18 +54,38 @@ const link = {
|
|||||||
return location.href;
|
return location.href;
|
||||||
},
|
},
|
||||||
|
|
||||||
read() {
|
getFENLink(fen: string) {
|
||||||
|
return `${location.origin}/#fen/${fen.replace(/ /g, "_")}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
async read() {
|
||||||
const [type, ...rest] = location.hash.split("/");
|
const [type, ...rest] = location.hash.split("/");
|
||||||
|
|
||||||
if (/fen/.test(type)) {
|
if (/fen/.test(type)) {
|
||||||
linkData = { ...defaultLinkData } as LinkData;
|
linkData = { ...defaultLinkData } as LinkData;
|
||||||
linkData.fen = decodeURI(rest.join("/"));
|
linkData.fen = decodeURI(rest.join("/")).replace(/_/g, " ");
|
||||||
} else if (/pgn/.test(type)) {
|
} else if (/pgn/.test(type)) {
|
||||||
const [side, ply, ...pgn] = rest;
|
const [side, ply, ...pgn] = rest;
|
||||||
linkData.side = side as "w" | "b";
|
linkData.side = side as "w" | "b";
|
||||||
linkData.ply = Number(ply);
|
linkData.ply = Number(ply);
|
||||||
linkData.pgn = pgn.join("/");
|
linkData.pgn = pgn.join("/");
|
||||||
linkData.fen = "";
|
linkData.fen = "";
|
||||||
|
} else if (/lid/.test(type)) {
|
||||||
|
const [side, ply, ...id] = rest;
|
||||||
|
linkData.side = side as "w" | "b";
|
||||||
|
linkData.ply = Number(ply);
|
||||||
|
|
||||||
|
const result = await importFromLichess(
|
||||||
|
new URL(`https://lichess.org/${id[0]}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.error) {
|
||||||
|
linkData.pgn = compressPGN(cleanPGN(result.pgn));
|
||||||
|
linkData.fen = "";
|
||||||
|
} else {
|
||||||
|
linkData.pgn = "";
|
||||||
|
linkData.fen = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const words: { [key: string]: string } = {
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
volume: 50,
|
volume: 50,
|
||||||
rate: 2,
|
rate: 1,
|
||||||
lang: "en-US",
|
lang: "en-US",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ const initialSiteConfig: SiteConfig = {
|
|||||||
sounds: true,
|
sounds: true,
|
||||||
speech: false,
|
speech: false,
|
||||||
wrongBrowserPopup: true,
|
wrongBrowserPopup: true,
|
||||||
|
androidAppPopup: true,
|
||||||
|
iOSAppPopup: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TabName = "game" | "load" | "share" | "boards" | "pieces";
|
export type TabName = "game" | "load" | "share" | "boards" | "pieces";
|
||||||
@@ -57,6 +59,7 @@ export type State = {
|
|||||||
refreshHash: boolean;
|
refreshHash: boolean;
|
||||||
browser?: string;
|
browser?: string;
|
||||||
os?: string;
|
os?: string;
|
||||||
|
about: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
@@ -84,10 +87,9 @@ const initialState: State = {
|
|||||||
refreshHash: true,
|
refreshHash: true,
|
||||||
browser: userAgent.browser.name,
|
browser: userAgent.browser.name,
|
||||||
os: userAgent.os.name,
|
os: userAgent.os.name,
|
||||||
|
about: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [state, setState] = createStore(initialState);
|
const [state, setState] = createStore(initialState);
|
||||||
|
|
||||||
console.log(state);
|
|
||||||
|
|
||||||
export { state, setState };
|
export { state, setState };
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export type SiteConfig = {
|
|||||||
sounds: boolean;
|
sounds: boolean;
|
||||||
speech: boolean;
|
speech: boolean;
|
||||||
wrongBrowserPopup: boolean;
|
wrongBrowserPopup: boolean;
|
||||||
|
androidAppPopup: boolean;
|
||||||
|
iOSAppPopup: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MaterialCount = {
|
export type MaterialCount = {
|
||||||
@@ -173,9 +175,10 @@ export type Handlers = {
|
|||||||
changeBoardStyle: (style: BoardStyle) => void;
|
changeBoardStyle: (style: BoardStyle) => void;
|
||||||
changePiecesStyle: (style: PiecesStyle) => void;
|
changePiecesStyle: (style: PiecesStyle) => void;
|
||||||
loadPGN: (pgn: string, side?: "w" | "b", ply?: number) => Promise<void>;
|
loadPGN: (pgn: string, side?: "w" | "b", ply?: number) => Promise<void>;
|
||||||
loadFEN: (fen: string) => Promise<void>;
|
loadFEN: (fen: string, hash?: boolean) => Promise<void>;
|
||||||
importPGN: (link: string) => Promise<void>;
|
importPGN: (link: string) => Promise<void>;
|
||||||
load: (data: string) => Promise<boolean>;
|
load: (data: string) => Promise<boolean>;
|
||||||
|
loadFromClipboard(): Promise<boolean>;
|
||||||
downloadImage: () => Promise<void>;
|
downloadImage: () => Promise<void>;
|
||||||
downloadAnimation: () => Promise<void>;
|
downloadAnimation: () => Promise<void>;
|
||||||
toggleSound(): void;
|
toggleSound(): void;
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import { Component, Show } from "solid-js";
|
|||||||
import type { DeepReadonly } from "solid-js/store";
|
import type { DeepReadonly } from "solid-js/store";
|
||||||
|
|
||||||
import { Handlers } from "../types";
|
import { Handlers } from "../types";
|
||||||
import { setState, State, state } from "../state";
|
import { State, state } from "../state";
|
||||||
|
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
import GameTabs from "./components/GameTabs";
|
import GameTabs from "./components/GameTabs";
|
||||||
import SetupTabs from "./components/SetupTabs";
|
import SetupTabs from "./components/SetupTabs";
|
||||||
import Controls from "./components/Controls";
|
import Controls from "./components/Controls";
|
||||||
import Popup from "./components/Popup";
|
|
||||||
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import saveConfig from "../persistance/saveConfig";
|
import WrongBrowserPopup from "./components/popups/WrongBrowserPopup";
|
||||||
|
import AndroidAppPopup from "./components/popups/AndroidAppPopup";
|
||||||
|
import IOSAppPopup from "./components/popups/IOSAppPopup";
|
||||||
|
import About from "./components/About";
|
||||||
|
|
||||||
const App: Component<{ handlers: Handlers; state: DeepReadonly<State> }> = (
|
const App: Component<{ handlers: Handlers; state: DeepReadonly<State> }> = (
|
||||||
props
|
props
|
||||||
@@ -44,17 +46,10 @@ const App: Component<{ handlers: Handlers; state: DeepReadonly<State> }> = (
|
|||||||
></GameTabs>
|
></GameTabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={state.siteConfig.wrongBrowserPopup}>
|
<About />
|
||||||
<Popup
|
<WrongBrowserPopup />
|
||||||
handlers={props.handlers}
|
<AndroidAppPopup />
|
||||||
onClose={() => {
|
<IOSAppPopup />
|
||||||
setState("siteConfig", "wrongBrowserPopup", false);
|
|
||||||
saveConfig("site");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{state.browser} | {state.os}
|
|
||||||
</Popup>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
77
src/ui/components/About.css
Normal file
77
src/ui/components/About.css
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
.about {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
display: grid;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding-top: var(--header-height);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__box {
|
||||||
|
background-color: var(--color-bg-input);
|
||||||
|
min-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
padding: 2rem;
|
||||||
|
padding-top: 6rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__close {
|
||||||
|
width: 3.2rem;
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__title {
|
||||||
|
text-align: left;
|
||||||
|
margin: 0 4.2rem 3rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__content {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__content p {
|
||||||
|
margin-top: 2rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__content ul {
|
||||||
|
margin-top: 2rem;
|
||||||
|
list-style: none;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__content li {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__content kbd {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
background-color: var(--color-tab);
|
||||||
|
color: var(--color-text-contrast);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0.5rem 0.5rem 1rem #00000033;
|
||||||
|
font-family: "Fira Mono", monospace;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about__shortcuts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
90
src/ui/components/About.tsx
Normal file
90
src/ui/components/About.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { Component, Show } from "solid-js";
|
||||||
|
import { setState, state } from "../../state";
|
||||||
|
|
||||||
|
import "./About.css";
|
||||||
|
|
||||||
|
const About: Component = () => {
|
||||||
|
return (
|
||||||
|
<Show when={state.about}>
|
||||||
|
<div className="about">
|
||||||
|
<div className="about__box">
|
||||||
|
<button
|
||||||
|
className="about__close"
|
||||||
|
onClick={() => setState("about", !state.about)}
|
||||||
|
>
|
||||||
|
<i class="las la-times"></i>
|
||||||
|
</button>
|
||||||
|
<div className="about__content">
|
||||||
|
<h2>About</h2>
|
||||||
|
<p>
|
||||||
|
<b>ShareChess</b> is a free, open source website that allows you
|
||||||
|
to share chess games as self-contained replay links (the whole
|
||||||
|
game is stored in the url without the need for a database), PNG
|
||||||
|
images, or GIF / MP4 / WebM animations.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The website provides a high variety of chessboard and piece
|
||||||
|
designs to serve as an open alternative for commercial chess GIF
|
||||||
|
makers.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
You can find the complete source code on our{" "}
|
||||||
|
<a href="https://github.com/sharechess/sharechess">GitHub page</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<h2>Keyboard Shortcuts</h2>
|
||||||
|
<div className="about__shortcuts">
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<kbd>→</kbd> Next move
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>←</kbd> Previous move
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>↑</kbd> Start position
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>↓</kbd> Final position
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>f</kbd> Flip the board
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>Space</kbd> Play / Pause
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>Enter</kbd> Analyze on Lichess
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<kbd>l</kbd> Load from clipboard
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>a</kbd> Toggle anonymous
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>b</kbd> Toggle border
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>i</kbd> Toggle extra info
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>h</kbd> Toggle header (title screen)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<kbd>s</kbd> Toggle shadows
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default About;
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Component, createSignal } from "solid-js";
|
import { Component } from "solid-js";
|
||||||
import { Handlers } from "../../types";
|
import { Handlers } from "../../types";
|
||||||
import { state } from "../../state";
|
import { setState, state } from "../../state";
|
||||||
import "./Header.css";
|
import "./Header.css";
|
||||||
|
|
||||||
const Header: Component<{ handlers: Handlers }> = (props) => {
|
const Header: Component<{ handlers: Handlers }> = (props) => {
|
||||||
const [darkMode, setDarkMode] = createSignal(true);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header class="header-box">
|
<header class="header-box">
|
||||||
<div class="header__logo">
|
<div class="header__logo">
|
||||||
@@ -14,9 +12,13 @@ const Header: Component<{ handlers: Handlers }> = (props) => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="header__options">
|
<div class="header__options">
|
||||||
{/* <div class="header__options-ico" onClick={() => {}}>
|
<div
|
||||||
|
class="header__options-ico"
|
||||||
|
onClick={() => setState("about", !state.about)}
|
||||||
|
title="ABOUT"
|
||||||
|
>
|
||||||
<i class="las la-question-circle"></i>
|
<i class="las la-question-circle"></i>
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="header__options-ico"
|
class="header__options-ico"
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ const Load: Component<{ handlers: Handlers; class?: string }> = (props) => {
|
|||||||
<button
|
<button
|
||||||
classList={{ "load__game-btn": true, "btn--error": clipError() }}
|
classList={{ "load__game-btn": true, "btn--error": clipError() }}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const clip = await navigator.clipboard.readText();
|
const success = await props.handlers.loadFromClipboard();
|
||||||
const success = await props.handlers.load(clip);
|
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
setClipError(true);
|
setClipError(true);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Scrollable from "./reusable/Scrollable";
|
|||||||
import { state, setState } from "../../state";
|
import { state, setState } from "../../state";
|
||||||
import "./Share.css";
|
import "./Share.css";
|
||||||
import download from "../../utils/download";
|
import download from "../../utils/download";
|
||||||
|
import link from "../../persistance/link";
|
||||||
|
|
||||||
const Share: Component<{ handlers: Handlers; class?: string }> = (props) => {
|
const Share: Component<{ handlers: Handlers; class?: string }> = (props) => {
|
||||||
const [copyId, setCopyId] = createSignal("");
|
const [copyId, setCopyId] = createSignal("");
|
||||||
@@ -19,6 +20,17 @@ const Share: Component<{ handlers: Handlers; class?: string }> = (props) => {
|
|||||||
<Scrollable class={"share" + (props.class ? ` ${props.class}` : "")}>
|
<Scrollable class={"share" + (props.class ? ` ${props.class}` : "")}>
|
||||||
<div className="share__view">
|
<div className="share__view">
|
||||||
<h2 class="header--first">Board options</h2>
|
<h2 class="header--first">Board options</h2>
|
||||||
|
<button
|
||||||
|
classList={{
|
||||||
|
options__button: true,
|
||||||
|
"options__button--last": false,
|
||||||
|
"options__button--active": state.anonymous,
|
||||||
|
}}
|
||||||
|
onClick={props.handlers.toggleAnonymous}
|
||||||
|
title="TOGGLE ANONYMOUS"
|
||||||
|
>
|
||||||
|
<i class="las la-user-secret"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
classList={{
|
classList={{
|
||||||
options__button: true,
|
options__button: true,
|
||||||
@@ -57,17 +69,6 @@ const Share: Component<{ handlers: Handlers; class?: string }> = (props) => {
|
|||||||
>
|
>
|
||||||
<i class="las la-heading"></i>
|
<i class="las la-heading"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
classList={{
|
|
||||||
options__button: true,
|
|
||||||
"options__button--last": false,
|
|
||||||
"options__button--active": state.anonymous,
|
|
||||||
}}
|
|
||||||
onClick={props.handlers.toggleAnonymous}
|
|
||||||
title="TOGGLE ANONYMOUS"
|
|
||||||
>
|
|
||||||
<i class="las la-user-secret"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
classList={{
|
classList={{
|
||||||
options__button: true,
|
options__button: true,
|
||||||
@@ -110,8 +111,7 @@ const Share: Component<{ handlers: Handlers; class?: string }> = (props) => {
|
|||||||
<button
|
<button
|
||||||
class="share__btn share__btn--right"
|
class="share__btn share__btn--right"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const link = `${location.origin}/#fen/${encodeURI(state.fen)}`;
|
navigator.clipboard.writeText(link.getFENLink(state.fen));
|
||||||
navigator.clipboard.writeText(link);
|
|
||||||
blinkCopy("fen-link");
|
blinkCopy("fen-link");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
38
src/ui/components/popups/AndroidAppPopup.tsx
Normal file
38
src/ui/components/popups/AndroidAppPopup.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Component, Show } from "solid-js";
|
||||||
|
import { setState, state } from "../../../state";
|
||||||
|
import Popup from "../reusable/Popup";
|
||||||
|
import saveConfig from "../../../persistance/saveConfig";
|
||||||
|
|
||||||
|
const AndroidAppPopup: Component = () => {
|
||||||
|
return (
|
||||||
|
<Show
|
||||||
|
when={state.siteConfig.androidAppPopup && state.os?.includes("Android")}
|
||||||
|
>
|
||||||
|
<Popup
|
||||||
|
onClose={() => {
|
||||||
|
setState("siteConfig", "androidAppPopup", false);
|
||||||
|
saveConfig("site");
|
||||||
|
}}
|
||||||
|
title="Tip"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
For easy access, you can install this website as a standalone app.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
To do that:
|
||||||
|
<li>open the website in Chrome,</li>
|
||||||
|
<li>tap the menu icon (3 dots in the corner),</li>
|
||||||
|
<li>
|
||||||
|
tap{" "}
|
||||||
|
<u>
|
||||||
|
<b>Add to Home screen</b>
|
||||||
|
</u>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popup>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AndroidAppPopup;
|
||||||
36
src/ui/components/popups/IOSAppPopup.tsx
Normal file
36
src/ui/components/popups/IOSAppPopup.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, Show } from "solid-js";
|
||||||
|
import { setState, state } from "../../../state";
|
||||||
|
import Popup from "../reusable/Popup";
|
||||||
|
import saveConfig from "../../../persistance/saveConfig";
|
||||||
|
|
||||||
|
const IOSAppPopup: Component = () => {
|
||||||
|
return (
|
||||||
|
<Show when={state.siteConfig.iOSAppPopup && state.os?.includes("iOS")}>
|
||||||
|
<Popup
|
||||||
|
onClose={() => {
|
||||||
|
setState("siteConfig", "iOSAppPopup", false);
|
||||||
|
saveConfig("site");
|
||||||
|
}}
|
||||||
|
title="Tip"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
For easy access, you can install this website as a standalone app.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
To do that:
|
||||||
|
<li>open the website in Safari,</li>
|
||||||
|
<li>tap the Share icon,</li>
|
||||||
|
<li>
|
||||||
|
tap{" "}
|
||||||
|
<u>
|
||||||
|
<b>Add to Home Screen</b>
|
||||||
|
</u>
|
||||||
|
.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Popup>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IOSAppPopup;
|
||||||
36
src/ui/components/popups/WrongBrowserPopup.tsx
Normal file
36
src/ui/components/popups/WrongBrowserPopup.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { Component, Show } from "solid-js";
|
||||||
|
import { setState, state } from "../../../state";
|
||||||
|
import Popup from "../reusable/Popup";
|
||||||
|
import saveConfig from "../../../persistance/saveConfig";
|
||||||
|
|
||||||
|
const WrongBrowserPopup: Component = () => {
|
||||||
|
return (
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
!state.siteConfig.iOSAppPopup &&
|
||||||
|
state.siteConfig.wrongBrowserPopup &&
|
||||||
|
state.os === "iOS" &&
|
||||||
|
!state.browser?.includes("Safari")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Popup
|
||||||
|
onClose={() => {
|
||||||
|
setState("siteConfig", "wrongBrowserPopup", false);
|
||||||
|
saveConfig("site");
|
||||||
|
}}
|
||||||
|
title="Note"
|
||||||
|
>
|
||||||
|
<p>Saving files may not work correctly in this browser.</p>
|
||||||
|
<p>
|
||||||
|
To enjoy the full functionality of the website, please open it in{" "}
|
||||||
|
<u>
|
||||||
|
<b>Safari</b>
|
||||||
|
</u>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</Popup>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WrongBrowserPopup;
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.7);
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
box-shadow: 0 0 2rem #00000099;
|
box-shadow: 0 0 2rem #00000099;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
border: solid 1px var(--color-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popup__close {
|
.popup__close {
|
||||||
@@ -29,6 +30,23 @@
|
|||||||
|
|
||||||
.popup__title {
|
.popup__title {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
margin: 0 4.2rem 2rem 0;
|
margin: 0 4.2rem 3rem 0;
|
||||||
/* background-color: aqua; */
|
}
|
||||||
|
|
||||||
|
.popup__content {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__content p {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__content ul {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup__content li {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-left: 2rem;
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,18 @@
|
|||||||
import { Component } from "solid-js";
|
import { Component } from "solid-js";
|
||||||
import { Handlers } from "../../types";
|
|
||||||
import { state, setState } from "../../state";
|
|
||||||
|
|
||||||
import "./Popup.css";
|
import "./Popup.css";
|
||||||
|
|
||||||
const Popup: Component<{ handlers: Handlers; onClose: () => void }> = (
|
const Popup: Component<{
|
||||||
props
|
onClose: () => void;
|
||||||
) => {
|
title: string;
|
||||||
|
}> = (props) => {
|
||||||
return (
|
return (
|
||||||
<div className="popup">
|
<div className="popup">
|
||||||
<div className="popup__box">
|
<div className="popup__box">
|
||||||
<button className="popup__close" onClick={props.onClose}>
|
<button className="popup__close" onClick={props.onClose}>
|
||||||
<i class="las la-times"></i>
|
<i class="las la-times"></i>
|
||||||
</button>
|
</button>
|
||||||
<h2 className="popup__title">Popup title</h2>
|
<h2 className="popup__title">{props.title}</h2>
|
||||||
<div className="popup__content">{props.children}</div>
|
<div className="popup__content">{props.children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const REGEX =
|
const REGEX =
|
||||||
/^([1-8kqrbnp]+\/)+[1-8kqrbnp]+ [wb] ([kq]+|-) ([a-h1-8]{2}|-) [01] \d+$/i;
|
/^([1-8kqrbnp]+\/)+[1-8kqrbnp]+ [wb] ([kq]+|-) ([a-h1-8]{2}|-) \d+ \d+$/i;
|
||||||
|
|
||||||
const isFEN = (data: string) => {
|
const isFEN = (data: string) => {
|
||||||
return REGEX.test(data.trim());
|
return REGEX.test(data.trim());
|
||||||
|
|||||||
Reference in New Issue
Block a user