diff --git a/README.md b/README.md index 825eac0..b114dd3 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@ This repo contains the source code for [sharechess.github.io](https://sharechess ## 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 diff --git a/src/boot/loadFromUrl.ts b/src/boot/loadFromUrl.ts new file mode 100644 index 0000000..cc03ca7 --- /dev/null +++ b/src/boot/loadFromUrl.ts @@ -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; diff --git a/src/boot/registerEvents.ts b/src/boot/registerEvents.ts new file mode 100644 index 0000000..2e752b1 --- /dev/null +++ b/src/boot/registerEvents.ts @@ -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; diff --git a/src/boot/registerHandlers.ts b/src/boot/registerHandlers.ts new file mode 100644 index 0000000..afe3fbb --- /dev/null +++ b/src/boot/registerHandlers.ts @@ -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; diff --git a/src/imports/importFromLink.ts b/src/imports/importFromLink.ts index 8823a2d..b882cab 100644 --- a/src/imports/importFromLink.ts +++ b/src/imports/importFromLink.ts @@ -50,4 +50,6 @@ const importFromLink = async (link: string): Promise => { return { error: true, errorType: "INCORRECT_LINK" }; }; +export { importFromLichess }; + export default importFromLink; diff --git a/src/main.tsx b/src/main.tsx index 009f164..03dce12 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,262 +1,32 @@ import WebFont from "webfontloader"; import { render } from "solid-js/web"; -import { BoardStyle } from "./types"; - import Board from "./board/Board"; -import Game from "./game/Game"; import Player from "./player/Player"; import App from "./ui/App"; 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 importToLichess from "./imports/importToLichess"; +import registerHandlers from "./boot/registerHandlers"; +import loadFromUrl from "./boot/loadFromUrl"; +import registerEvents from "./boot/registerEvents"; const main = async () => { const board = new Board(state.boardConfig); const player = new Player(board, state.gameConfig); + /* Connect player to the state */ + player.watch((playing) => setState("playing", playing)); + /* Load game from url hash */ + link.read(); /* Register handlers */ - const handlers = { - 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; + const handlers = registerHandlers(player, board); /* Render the page */ @@ -265,105 +35,21 @@ const main = async () => { document.getElementById("root") as HTMLElement ); + /* Connect canvas */ const canvas = document.getElementById("canvas") as HTMLCanvasElement; - await board.setCanvas(canvas); + + /* Initialize the game */ + await player.init(); - - /* 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); + await loadFromUrl(false, handlers); /* Register events */ - 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(); - }); - - 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) => { - 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); - } - }); - } + registerEvents(handlers); }; -/* Boot */ +/* Initialize */ Promise.all([ new Promise((resolve) => diff --git a/src/persistance/clearConfig.ts b/src/persistance/clearConfig.ts new file mode 100644 index 0000000..aede5c0 --- /dev/null +++ b/src/persistance/clearConfig.ts @@ -0,0 +1,7 @@ +const clearConfig = () => { + localStorage.removeItem("boardConfig"); + localStorage.removeItem("gameConfig"); + localStorage.removeItem("siteConfig"); +}; + +export default clearConfig; diff --git a/src/persistance/extractUrlData.ts b/src/persistance/extractUrlData.ts deleted file mode 100644 index 6f3b5cb..0000000 --- a/src/persistance/extractUrlData.ts +++ /dev/null @@ -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; diff --git a/src/persistance/link.ts b/src/persistance/link.ts index 6894a22..f9163eb 100644 --- a/src/persistance/link.ts +++ b/src/persistance/link.ts @@ -1,4 +1,5 @@ -import { compressPGN, decompressPGN } from "../game/PGNHelpers"; +import { cleanPGN, compressPGN, decompressPGN } from "../game/PGNHelpers"; +import { importFromLichess } from "../imports/importFromLink"; type LinkData = { pgn: string; @@ -29,7 +30,7 @@ const link = { linkData = { ...defaultLinkData } as LinkData; linkData.fen = data.fen; - location.hash = `fen/${linkData.fen}`; + location.hash = `fen/${linkData.fen.replace(/ /g, "_")}`; return; } @@ -53,18 +54,38 @@ const link = { return location.href; }, - read() { + getFENLink(fen: string) { + return `${location.origin}/#fen/${fen.replace(/ /g, "_")}`; + }, + + async read() { const [type, ...rest] = location.hash.split("/"); if (/fen/.test(type)) { linkData = { ...defaultLinkData } as LinkData; - linkData.fen = decodeURI(rest.join("/")); + linkData.fen = decodeURI(rest.join("/")).replace(/_/g, " "); } else if (/pgn/.test(type)) { const [side, ply, ...pgn] = rest; linkData.side = side as "w" | "b"; linkData.ply = Number(ply); linkData.pgn = pgn.join("/"); 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 { diff --git a/src/player/speach.ts b/src/player/speach.ts index 8401d02..0957ddc 100644 --- a/src/player/speach.ts +++ b/src/player/speach.ts @@ -15,7 +15,7 @@ const words: { [key: string]: string } = { const config = { volume: 50, - rate: 2, + rate: 1, lang: "en-US", }; diff --git a/src/state.ts b/src/state.ts index d04ed05..948e5fa 100644 --- a/src/state.ts +++ b/src/state.ts @@ -36,6 +36,8 @@ const initialSiteConfig: SiteConfig = { sounds: true, speech: false, wrongBrowserPopup: true, + androidAppPopup: true, + iOSAppPopup: true, }; export type TabName = "game" | "load" | "share" | "boards" | "pieces"; @@ -57,6 +59,7 @@ export type State = { refreshHash: boolean; browser?: string; os?: string; + about: boolean; }; const initialState: State = { @@ -84,10 +87,9 @@ const initialState: State = { refreshHash: true, browser: userAgent.browser.name, os: userAgent.os.name, + about: false, }; const [state, setState] = createStore(initialState); -console.log(state); - export { state, setState }; diff --git a/src/types.ts b/src/types.ts index bbb43d2..ac32668 100644 --- a/src/types.ts +++ b/src/types.ts @@ -106,6 +106,8 @@ export type SiteConfig = { sounds: boolean; speech: boolean; wrongBrowserPopup: boolean; + androidAppPopup: boolean; + iOSAppPopup: boolean; }; export type MaterialCount = { @@ -173,9 +175,10 @@ export type Handlers = { changeBoardStyle: (style: BoardStyle) => void; changePiecesStyle: (style: PiecesStyle) => void; loadPGN: (pgn: string, side?: "w" | "b", ply?: number) => Promise; - loadFEN: (fen: string) => Promise; + loadFEN: (fen: string, hash?: boolean) => Promise; importPGN: (link: string) => Promise; load: (data: string) => Promise; + loadFromClipboard(): Promise; downloadImage: () => Promise; downloadAnimation: () => Promise; toggleSound(): void; diff --git a/src/ui/App.tsx b/src/ui/App.tsx index 047a7fd..d8a7f90 100644 --- a/src/ui/App.tsx +++ b/src/ui/App.tsx @@ -2,16 +2,18 @@ import { Component, Show } from "solid-js"; import type { DeepReadonly } from "solid-js/store"; import { Handlers } from "../types"; -import { setState, State, state } from "../state"; +import { State, state } from "../state"; import Header from "./components/Header"; import GameTabs from "./components/GameTabs"; import SetupTabs from "./components/SetupTabs"; import Controls from "./components/Controls"; -import Popup from "./components/Popup"; 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 }> = ( props @@ -44,17 +46,10 @@ const App: Component<{ handlers: Handlers; state: DeepReadonly }> = ( > - - { - setState("siteConfig", "wrongBrowserPopup", false); - saveConfig("site"); - }} - > - {state.browser} | {state.os} - - + + + + ); }; diff --git a/src/ui/components/About.css b/src/ui/components/About.css new file mode 100644 index 0000000..5428a15 --- /dev/null +++ b/src/ui/components/About.css @@ -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; +} diff --git a/src/ui/components/About.tsx b/src/ui/components/About.tsx new file mode 100644 index 0000000..eeb12d3 --- /dev/null +++ b/src/ui/components/About.tsx @@ -0,0 +1,90 @@ +import { Component, Show } from "solid-js"; +import { setState, state } from "../../state"; + +import "./About.css"; + +const About: Component = () => { + return ( + +
+
+ +
+

About

+

+ ShareChess 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. +

+

+ You can find the complete source code on our{" "} + GitHub page + . +

+
+

Keyboard Shortcuts

+
+
    +
  • + Next move +
  • +
  • + Previous move +
  • +
  • + Start position +
  • +
  • + Final position +
  • +
  • + f Flip the board +
  • +
  • + Space Play / Pause +
  • +
  • + Enter Analyze on Lichess +
  • +
+
    +
  • + l Load from clipboard +
  • +
  • + a Toggle anonymous +
  • +
  • + b Toggle border +
  • +
  • + i Toggle extra info +
  • +
  • + h Toggle header (title screen) +
  • +
  • + s Toggle shadows +
  • +
+
+
+
+
+
+ ); +}; + +export default About; diff --git a/src/ui/components/Header.tsx b/src/ui/components/Header.tsx index 86f830d..8dbd1cd 100644 --- a/src/ui/components/Header.tsx +++ b/src/ui/components/Header.tsx @@ -1,11 +1,9 @@ -import { Component, createSignal } from "solid-js"; +import { Component } from "solid-js"; import { Handlers } from "../../types"; -import { state } from "../../state"; +import { setState, state } from "../../state"; import "./Header.css"; const Header: Component<{ handlers: Handlers }> = (props) => { - const [darkMode, setDarkMode] = createSignal(true); - return (
- {/*
{}}> +
setState("about", !state.about)} + title="ABOUT" + > -
*/} +
= (props) => { - -

Popup title

+

{props.title}

{props.children}
diff --git a/src/utils/isFEN.ts b/src/utils/isFEN.ts index f0892c0..6e6a92a 100644 --- a/src/utils/isFEN.ts +++ b/src/utils/isFEN.ts @@ -1,5 +1,5 @@ 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) => { return REGEX.test(data.trim());