From fee5309c37585a37112fdebe1081de8c2f46aae1 Mon Sep 17 00:00:00 2001 From: saritahimthani Date: Tue, 14 Apr 2026 15:55:09 -0700 Subject: [PATCH 01/11] fixed sponsor page img alignment and fixed active button click color for selected btns --- .../about-us/sponsors-partners/SponsorsPartners.scss | 12 ++++++++++++ .../src/features/engine/PlayComputer.module.scss | 1 - 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/react-ystemandchess/src/features/about-us/sponsors-partners/SponsorsPartners.scss b/react-ystemandchess/src/features/about-us/sponsors-partners/SponsorsPartners.scss index 7575c692..43885705 100644 --- a/react-ystemandchess/src/features/about-us/sponsors-partners/SponsorsPartners.scss +++ b/react-ystemandchess/src/features/about-us/sponsors-partners/SponsorsPartners.scss @@ -18,6 +18,18 @@ line-height: 1.5; } + .logo-break { + display: flex; + justify-content: center; + margin: 30px 0; + + img { + display: block; + max-width: 100%; + height: auto; + } + } + .sponsors-partners-section { margin-top: 0; diff --git a/react-ystemandchess/src/features/engine/PlayComputer.module.scss b/react-ystemandchess/src/features/engine/PlayComputer.module.scss index 1ea84776..dc5fdb32 100644 --- a/react-ystemandchess/src/features/engine/PlayComputer.module.scss +++ b/react-ystemandchess/src/features/engine/PlayComputer.module.scss @@ -151,7 +151,6 @@ } &.active { - color: white; border-color: #2563eb; box-shadow: 0 4px 16px rgba(58, 124, 202, 0.4); transform: scale(1.05); From ca5c2e5b140818515cbd81de3d46bf5077b92bab Mon Sep 17 00:00:00 2001 From: F-Hejazi <60328249+F-Hejazi@users.noreply.github.com> Date: Wed, 15 Apr 2026 04:58:00 +0100 Subject: [PATCH 02/11] ui: update footer positioning --- react-ystemandchess/src/App.css | 2 -- react-ystemandchess/src/App.tsx | 6 ++++-- react-ystemandchess/src/AppRoutes.tsx | 2 +- react-ystemandchess/src/index.css | 19 ++++++++++++++++++- react-ystemandchess/tailwind.config.js | 22 +++++++++++++++++++++- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/react-ystemandchess/src/App.css b/react-ystemandchess/src/App.css index c083e43b..7c54c403 100644 --- a/react-ystemandchess/src/App.css +++ b/react-ystemandchess/src/App.css @@ -1,6 +1,4 @@ .App { - overflow-y: hidden; margin-top: 0; text-align: center; - height: 100%; } diff --git a/react-ystemandchess/src/App.tsx b/react-ystemandchess/src/App.tsx index 351e94a3..53281882 100644 --- a/react-ystemandchess/src/App.tsx +++ b/react-ystemandchess/src/App.tsx @@ -142,9 +142,11 @@ function App() { */ return ( -
+
- +
+ +
diff --git a/react-ystemandchess/src/AppRoutes.tsx b/react-ystemandchess/src/AppRoutes.tsx index a962f78d..adcacab0 100644 --- a/react-ystemandchess/src/AppRoutes.tsx +++ b/react-ystemandchess/src/AppRoutes.tsx @@ -34,7 +34,7 @@ import AboutUs from "./features/about-us/aboutus/AboutUs"; // Educational content pages import PlayComputer from "./features/engine/PlayComputer"; import Lessons from "./features/lessons/lessons-main/Lessons"; -import Puzzles from './features/puzzles/puzzles-page/Puzzles'; +import Puzzles from './features/puzzles/Puzzles'; import LessonSelection from "./features/lessons/lessons-selection/LessonsSelection"; import LessonOverlay from "./features/lessons/piece-lessons/lesson-overlay/Lesson-overlay"; diff --git a/react-ystemandchess/src/index.css b/react-ystemandchess/src/index.css index 1f4db540..c01f6b9d 100644 --- a/react-ystemandchess/src/index.css +++ b/react-ystemandchess/src/index.css @@ -31,7 +31,7 @@ button { @layer components { .btn-primary { @apply - bg-dark text-light font-bold leading-relaxed + bg-dark text-light font-semibold leading-relaxed py-3 px-8 rounded-full border-2 border-dark @@ -45,6 +45,23 @@ button { focus-visible:ring-primary; } + .btn-green { + @apply + bg-primary text-light text-xl font-semibold + py-3 px-8 + rounded-xl + shadow-md + transition-all duration-300 + hover:opacity-90 + hover:scale-105 + active:scale-95 + disabled:opacity-50 + disabled:cursor-not-allowed + focus-visible:outline-none + focus-visible:ring-2 + focus-visible:ring-primary/50; + } + .btn-toolbar { @apply relative flex-1 max-w-xs p-0 bg-transparent rounded-lg cursor-pointer transition-all duration-200 hover:scale-105 active:scale-95 diff --git a/react-ystemandchess/tailwind.config.js b/react-ystemandchess/tailwind.config.js index 0d6b454d..128bb355 100644 --- a/react-ystemandchess/tailwind.config.js +++ b/react-ystemandchess/tailwind.config.js @@ -22,7 +22,7 @@ module.exports = { // Error colors red: "#D64545", - redLight: "#F4CACAFF", + redLight: "#F5E9E9", }, fontFamily: { @@ -39,6 +39,26 @@ module.exports = { 'card-yellow': '1.25rem 1.25rem 0.063rem rgb(209, 230, 28)', 'card-green': '1.25rem 1.25rem 0.063rem rgb(115, 179, 19)', }, + + keyframes: { + 'modal-in': { + '0%': { opacity: '0', transform: 'scale(0.95) translateY(8px)' }, + '100%': { opacity: '1', transform: 'scale(1) translateY(0)' }, + }, + 'fade-out': { + 'to': { opacity: '0', transform: 'translateY(-5px)' }, + }, + 'shake': { + '0%, 100%': { transform: 'translateX(0)' }, + '25%': { transform: 'translateX(-5px)' }, + '75%': { transform: 'translateX(5px)' }, + }, + }, + animation: { + 'modal-in': 'modal-in 0.15s ease-out', + 'fade-out': 'fade-out 0.4s ease 2.1s forwards', + 'shake': 'shake 0.5s ease', + }, }, }, plugins: [], From e65d5657e219f175d0c417a457c3402b5fc077b1 Mon Sep 17 00:00:00 2001 From: F-Hejazi <60328249+F-Hejazi@users.noreply.github.com> Date: Wed, 15 Apr 2026 04:58:32 +0100 Subject: [PATCH 03/11] feat: add modal component --- .../src/components/modal/Modal.tsx | 97 ++++++++++++++++++ .../lessons/lessons-main/Rectangle 313.png | Bin 3052 -> 0 bytes .../lessons/lessons-main/register_button.png | Bin 25591 -> 0 bytes 3 files changed, 97 insertions(+) create mode 100644 react-ystemandchess/src/components/modal/Modal.tsx delete mode 100644 react-ystemandchess/src/features/lessons/lessons-main/Rectangle 313.png delete mode 100644 react-ystemandchess/src/features/lessons/lessons-main/register_button.png diff --git a/react-ystemandchess/src/components/modal/Modal.tsx b/react-ystemandchess/src/components/modal/Modal.tsx new file mode 100644 index 00000000..6c0c1f3f --- /dev/null +++ b/react-ystemandchess/src/components/modal/Modal.tsx @@ -0,0 +1,97 @@ +import React, { useEffect } from "react"; + +export type ModalType = "success" | "error" | "loading"; + +export interface ModalProps { + type: ModalType; + title: string; + message?: string; + confirmText?: string; + /** Called after the modal closes itself (button click). Not used for loading. */ + onConfirm?: () => void; +} + +interface Props extends ModalProps { + /** Called by the modal to close itself — pass your state setter here. */ + onClose: () => void; +} + +const icons: Record = { + success: ( +
+ + + +
+ ), + error: ( +
+ + + +
+ ), + loading: ( +
+ ), +}; + +const Modal: React.FC = ({ + type, + title, + message, + confirmText = "OK", + onConfirm, + onClose, +}) => { + // Allow ESC to dismiss non-loading modals + useEffect(() => { + if (type === "loading") return; + const handleKey = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + onConfirm?.(); + } + }; + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [type, onClose, onConfirm]); + + const handleConfirm = () => { + onClose(); + onConfirm?.(); + }; + + return ( +
+
+ {icons[type]} + + + + {message && ( +

{message}

+ )} + + {type !== "loading" && ( + + )} +
+
+ ); +}; + +export default Modal; diff --git a/react-ystemandchess/src/features/lessons/lessons-main/Rectangle 313.png b/react-ystemandchess/src/features/lessons/lessons-main/Rectangle 313.png deleted file mode 100644 index 766137581332a0c7097ddd44d3b71af134798dac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3052 zcmeAS@N?(olHy`uVBq!ia0y~yU|qn#z*xb-1{7J;6gLw{aTa()7Bet#3xhBt!>l*8o|0J>kRW*9LIEGX(zP;hdctC-HL2>&($umj3 zmoqruvm5@nHZL8hg@NHf@_#Vh(9i&~j6p$h0V9a^;K%?`3_?s&AgZBdR2mcxqrozo z9DvDSG?$E)4x^>wXf_zFBtezpXyahC%>+sZqisdhCUlH^B%9iVbw>(8f#d1w=d#Wz Gp$P!;NM3pX diff --git a/react-ystemandchess/src/features/lessons/lessons-main/register_button.png b/react-ystemandchess/src/features/lessons/lessons-main/register_button.png deleted file mode 100644 index 85a4902025d633a658e70831b4a7ad3c56d292ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25591 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 From dd167f80fd27b1dd58197c83aa95b22e2fa2b230 Mon Sep 17 00:00:00 2001 From: F-Hejazi <60328249+F-Hejazi@users.noreply.github.com> Date: Wed, 15 Apr 2026 05:04:59 +0100 Subject: [PATCH 04/11] ui: update puzzles styling --- .../{puzzles-page => }/Puzzles.test.tsx | 123 +++++------ .../puzzles/{puzzles-page => }/Puzzles.tsx | 197 +++++++++--------- .../puzzles-page/Puzzles-profile.module.scss | 57 ----- .../puzzles/puzzles-page/Puzzles.module.scss | 75 ------- 4 files changed, 150 insertions(+), 302 deletions(-) rename react-ystemandchess/src/features/puzzles/{puzzles-page => }/Puzzles.test.tsx (85%) rename react-ystemandchess/src/features/puzzles/{puzzles-page => }/Puzzles.tsx (79%) delete mode 100644 react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles-profile.module.scss delete mode 100644 react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.module.scss diff --git a/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.test.tsx b/react-ystemandchess/src/features/puzzles/Puzzles.test.tsx similarity index 85% rename from react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.test.tsx rename to react-ystemandchess/src/features/puzzles/Puzzles.test.tsx index acd4b408..3e542426 100644 --- a/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.test.tsx +++ b/react-ystemandchess/src/features/puzzles/Puzzles.test.tsx @@ -9,36 +9,27 @@ import { import Puzzles from "./Puzzles"; import { MemoryRouter } from "react-router"; import { useCookies } from "react-cookie"; -import { useChessSocket } from "../../../features/lessons/piece-lessons/lesson-overlay/hooks/useChessSocket"; -import Swal from "sweetalert2"; -import { useNavigate } from "react-router"; +import { useChessSocket } from "../lessons/piece-lessons/lesson-overlay/hooks/useChessSocket"; // Mock dependencies jest.mock("react-cookie", () => ({ useCookies: jest.fn(), })); -jest.mock("sweetalert2", () => ({ - fire: jest.fn(() => Promise.resolve({ isConfirmed: true })), - close: jest.fn(), - showLoading: jest.fn(), -})); - jest.mock( - "../../../features/lessons/piece-lessons/lesson-overlay/hooks/useChessSocket", + "../lessons/piece-lessons/lesson-overlay/hooks/useChessSocket", () => ({ useChessSocket: jest.fn(), }) ); -// mock navigating to Puzzles jest.mock("react-router", () => ({ ...jest.requireActual("react-router"), useNavigate: jest.fn(), })); // Mock ChessBoard component -jest.mock("../../../components/ChessBoard/ChessBoard", () => { +jest.mock("../../components/ChessBoard/ChessBoard", () => { const React = require("react"); return React.forwardRef((props: any, ref: any) => (
{ )); }); +// Mock Modal — exposes data-type so tests can assert which variant is shown, +// and a confirm button that fires both onClose and onConfirm (matching real behaviour). +jest.mock("../../components/modal/Modal", () => { + const React = require("react"); + return function MockModal({ type, title, message, onConfirm, onClose }: any) { + return ( +
+ {title} + {message && {message}} + {type !== "loading" && ( + + )} +
+ ); + }; +}); + // Mock themes service -jest.mock("../../../core/services/themesService", () => ({ +jest.mock("../../core/services/themesService", () => ({ themesName: { theme1: "Theme 1" }, themesDescription: { theme1: "Description 1" }, })); @@ -113,11 +126,8 @@ const mockSocket = { describe("Puzzles Component", () => { beforeEach(() => { - // Reset mocks jest.clearAllMocks(); - (Swal.fire as jest.Mock).mockResolvedValue({ isConfirmed: true }); - // Default cookie mock (useCookies as jest.Mock).mockReturnValue([ { login: @@ -127,7 +137,6 @@ describe("Puzzles Component", () => { jest.fn(), ]); - // Default socket mock (useChessSocket as jest.Mock).mockImplementation((props) => { React.useEffect(() => { if (props.onRoleAssigned) { @@ -137,7 +146,6 @@ describe("Puzzles Component", () => { return mockSocket; }); - // Mock global fetch global.fetch = jest.fn((url) => { if (typeof url === "string" && url.includes("puzzles/random")) { return Promise.resolve({ @@ -225,13 +233,11 @@ describe("Puzzles Component", () => { ); + // Board is always rendered; invalid FEN means the puzzle won't start + // but the board container is still present showing the start position. await waitFor(() => { - expect(screen.getByText(/Loading puzzle.../i)).toBeInTheDocument(); + expect(screen.getByTestId("chess-board-container")).toBeInTheDocument(); }); - expect( - screen.queryByTestId("chess-board-container") - ).not.toBeInTheDocument(); - expect(screen.queryByTestId("chess-board-mock")).not.toBeInTheDocument(); expect(warnSpy).toHaveBeenCalledWith( "Invalid or missing FEN:", expect.any(String) @@ -275,25 +281,21 @@ describe("Puzzles Component", () => { ); - await waitFor(() => { - expect(screen.getByText(/Loading puzzle.../i)).toBeInTheDocument(); - }); - expect( - screen.queryByTestId("chess-board-container") - ).not.toBeInTheDocument(); - expect(screen.queryByTestId("chess-board-mock")).not.toBeInTheDocument(); + // Board is always rendered, showing the start position while no puzzle is loaded. + expect(screen.getByTestId("chess-board-container")).toBeInTheDocument(); + expect(screen.queryByText(/Loading puzzle.../i)).not.toBeInTheDocument(); }); - test("renders loading state initially", async () => { - // Simulate socket not yet providing a puzzle or FEN not set + test("renders board immediately with no loading text", () => { render( ); - // Initially it might show "Loading puzzle..." because currentFEN is empty - expect(screen.getByText(/Loading puzzle.../i)).toBeInTheDocument(); + // Board is always present; pieces are hidden via CSS until the first FEN arrives. + expect(screen.getByTestId("chess-board-container")).toBeInTheDocument(); + expect(screen.queryByText(/Loading puzzle.../i)).not.toBeInTheDocument(); }); test("renders Puzzles page", async () => { @@ -303,20 +305,12 @@ describe("Puzzles Component", () => { ); - // Wait for the puzzle to be fetched and loaded await waitFor(() => { - expect(screen.queryByText(/Loading puzzle.../i)).not.toBeInTheDocument(); + expect(screen.getByTestId("chess-board-container")).toBeInTheDocument(); }); - // Check if chessboard is present - const lessonTitle = screen.getByTestId("chess-board-container"); - expect(lessonTitle).toBeInTheDocument(); - - // // Check if Get New Puzzle and Show Hint selectors are present - const scenarioSelector = screen.getByTestId("next-puzzle-button"); - const lessonSelector = screen.getByTestId("hint-button"); - expect(scenarioSelector).toBeInTheDocument(); - expect(lessonSelector).toBeInTheDocument(); + expect(screen.getByTestId("next-puzzle-button")).toBeInTheDocument(); + expect(screen.getByTestId("hint-button")).toBeInTheDocument(); }); test("renders chess board when puzzle is loaded", async () => { @@ -326,17 +320,12 @@ describe("Puzzles Component", () => { ); - // Wait for the puzzle to be fetched and loaded await waitFor(() => { - expect(screen.queryByText(/Loading puzzle.../i)).not.toBeInTheDocument(); + expect(screen.getByTestId("chess-board-mock")).toBeInTheDocument(); }); - - expect(screen.getByTestId("chess-board-mock")).toBeInTheDocument(); - // how did Trae get here? This ID was never in puzzles.tsx }); test("calls startNewPuzzle if connected and status is empty", async () => { - // Override mock for this test to NOT assign role immediately (useChessSocket as jest.Mock).mockReturnValue(mockSocket); render( @@ -357,7 +346,6 @@ describe("Puzzles Component", () => { ); - // Wait for board to load await waitFor(() => { expect(screen.getByTestId("chess-board-mock")).toBeInTheDocument(); }); @@ -382,17 +370,6 @@ describe("Puzzles Component", () => { const showHintBtn = screen.getByText("Show Hint"); fireEvent.click(showHintBtn); - // The hint text div should become visible (display: block) - // Note: checking style visibility in jsdom can be tricky if it's done via inline styles. - // The component sets `style={{ display: 'none' }}` initially and toggles it. - // Let's check if the element exists and we can try to check style. - - // However, the component modifies the DOM element directly using document.getElementById('hint-text') - // which might not work perfectly with React testing library's render container if not careful, - // but since we are rendering into the document, it should be findable. - - // We can also check if socket.sendMessage was called with hints during initialization - // because updateInfoBox is called when puzzle loads. expect(mockSocket.sendMessage).toHaveBeenCalledWith( expect.stringContaining("Puzzle Rating:") ); @@ -409,13 +386,8 @@ describe("Puzzles Component", () => { expect(screen.getByTestId("chess-board-mock")).toBeInTheDocument(); }); - // Simulate a move from the mock board const moveBtn = screen.getByTestId("mock-move-btn"); - // The mock puzzle moves are "e2e4 e7e5" - // The button simulates sending { from: 'e2', to: 'e4' } - // This matches the first move. - await act(async () => { fireEvent.click(moveBtn); }); @@ -746,7 +718,7 @@ describe("Puzzles Component", () => { jest.useRealTimers(); }); - test("puzzle completion flow triggers success and loads next puzzle", async () => { + test("puzzle completion flow shows success modal and loads next puzzle", async () => { jest.useFakeTimers(); render( @@ -766,16 +738,19 @@ describe("Puzzles Component", () => { expect.objectContaining({ from: "e7", to: "e5" }) ); expect(mockSocket.sendMessage).toHaveBeenCalledWith("puzzle completed"); + act(() => { jest.advanceTimersByTime(250); }); + await waitFor(() => { - expect(Swal.fire).toHaveBeenCalledWith( - "Puzzle completed", - "Good Job", - "success" - ); + expect(screen.getByTestId("mock-modal")).toBeInTheDocument(); + expect(screen.getByTestId("mock-modal")).toHaveAttribute("data-type", "success"); }); + + // Clicking OK closes the modal and sends "next puzzle" + fireEvent.click(screen.getByTestId("modal-confirm-btn")); + await waitFor(() => { expect(mockSocket.sendMessage).toHaveBeenCalledWith("next puzzle"); }); diff --git a/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.tsx b/react-ystemandchess/src/features/puzzles/Puzzles.tsx similarity index 79% rename from react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.tsx rename to react-ystemandchess/src/features/puzzles/Puzzles.tsx index dbf61faa..6f3c0163 100644 --- a/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.tsx +++ b/react-ystemandchess/src/features/puzzles/Puzzles.tsx @@ -1,20 +1,18 @@ import React, { useRef, useState, useEffect, useCallback } from "react"; -import pageStyles from "./Puzzles.module.scss"; -import profileStyles from "./Puzzles-profile.module.scss"; import { themesName, themesDescription, -} from "../../../core/services/themesService"; -import Swal from "sweetalert2"; -import { environment } from "../../../environments/environment"; +} from "../../core/services/themesService"; +import Modal, { ModalProps } from "../../components/modal/Modal"; +import { environment } from "../../environments/environment"; import { v4 as uuidv4 } from "uuid"; -import { SetPermissionLevel } from "../../../globals"; +import { SetPermissionLevel } from "../../globals"; import { useCookies } from "react-cookie"; import ChessBoard, { ChessBoardRef, -} from "../../../components/ChessBoard/ChessBoard"; -import { useChessSocket } from "../../../features/lessons/piece-lessons/lesson-overlay/hooks/useChessSocket"; -import { Move } from "../../../core/types/chess"; +} from "../../components/ChessBoard/ChessBoard"; +import { useChessSocket } from "../lessons/piece-lessons/lesson-overlay/hooks/useChessSocket"; +import { Move } from "../../core/types/chess"; type PuzzlesProps = { student?: any; @@ -53,11 +51,10 @@ const Puzzles: React.FC = ({ role = "student", styleType = "page", }) => { - const styles = styleType === "profile" ? profileStyles : pageStyles; + const isProfile = styleType === "profile"; // Refs const chessBoardRef = useRef(null); - const swalRef = useRef(""); const moveListRef = useRef([]); const isPuzzleEndRef = useRef(false); const currentPuzzleRef = useRef(null); @@ -71,12 +68,15 @@ const Puzzles: React.FC = ({ // State const [puzzleArray, setPuzzleArray] = useState([]); const [currentFEN, setCurrentFEN] = useState(""); + const [hidePieces, setHidePieces] = useState(true); const [playerColor, setPlayerColor] = useState<"white" | "black">("white"); const [themeList, setThemeList] = useState([]); const [status, setStatus] = useState(""); const [highlightSquares, setHighlightSquares] = useState([]); const [isInitialized, setIsInitialized] = useState(false); const [cookies] = useCookies(["login"]); + const [modal, setModal] = useState | null>(null); + const closeModal = () => setModal(null); // Time tracking const [eventID, setEventID] = useState(null); @@ -130,6 +130,13 @@ const Puzzles: React.FC = ({ } }; + // Reveal pieces once the first puzzle FEN arrives + useEffect(() => { + if (currentFEN && hidePieces) { + setHidePieces(false); + } + }, [currentFEN, hidePieces]); + // Prefetch when running low useEffect(() => { if ( @@ -185,7 +192,7 @@ const Puzzles: React.FC = ({ const startLesson = (puzzle: any, color: "white" | "black") => { console.log("StartLesson called... "); - + const fen = puzzle.FEN; if (!fen || fen.split("/").length !== 8) { @@ -307,13 +314,12 @@ const Puzzles: React.FC = ({ isPuzzleEndRef.current = true; socket.sendMessage("puzzle completed"); setTimeout(() => { - Swal.fire("Puzzle completed", "Good Job", "success").then( - (result) => { - if (result.isConfirmed) { - socket.sendMessage("next puzzle"); - } - } - ); + setModal({ + type: "success", + title: "Puzzle completed", + message: "Good job!", + onConfirm: () => socket.sendMessage("next puzzle"), + }); }, 200); } else { setTimeout(() => { @@ -322,10 +328,15 @@ const Puzzles: React.FC = ({ } } else { // Wrong move - reset to current position - Swal.fire("Incorrect move", "Try again!", "error").then(() => { - if (currentPuzzleRef.current) { - startLesson(currentPuzzleRef.current, playerColor); - } + setModal({ + type: "error", + title: "Incorrect move", + message: "Try again!", + onConfirm: () => { + if (currentPuzzleRef.current) { + startLesson(currentPuzzleRef.current, playerColor); + } + }, }); } }; @@ -340,37 +351,23 @@ const Puzzles: React.FC = ({ (msg: string) => { if (msg === "puzzle completed") { if (status === "guest") { - Swal.fire("Puzzle completed", "Good Job", "success").then( - (result) => { - if (result.isConfirmed) { - socket.sendMessage("next puzzle"); - } - } - ); + setModal({ + type: "success", + title: "Puzzle completed", + message: "Good job!", + onConfirm: () => socket.sendMessage("next puzzle"), + }); } } else if (msg === "next puzzle") { - Swal.close(); + closeModal(); if (status === "guest") { - Swal.fire({ - title: "Loading next puzzle", - text: "Please wait...", - allowOutsideClick: false, - allowEscapeKey: false, - showConfirmButton: false, - didOpen: () => { - Swal.showLoading(); - swalRef.current = "loading"; - }, - willClose: () => { - swalRef.current = ""; - }, - }); + setModal({ type: "loading", title: "Loading next puzzle", message: "Please wait…" }); } getNextPuzzleRef.current?.(); } else if (msg === "new game received") { - if (swalRef.current === "loading") Swal.close(); + closeModal(); } else if (msg.startsWith(" = ({ initializeComponentRef.current?.(); if (styleType === "profile" && status !== "") { - const message = - role === "student" - ? "Your mentor has left! Creating a new puzzle for you" - : "Your student has left! Creating a new puzzle for you"; - Swal.fire( - message.split("!")[0] + "!", - message.split("!")[1], - "success" - ); + setModal({ + type: "success", + title: role === "student" ? "Your mentor has left!" : "Your student has left!", + message: "Creating a new puzzle for you.", + }); } } else if (assignedRole === "guest") { const wasHost = status === "host"; setStatus("guest"); if (wasHost) { - const message = - role === "student" - ? "Your mentor has joined you! You can now also see their moves" - : "Your student has joined you! You can now also see their moves"; - Swal.fire( - message.split("!")[0] + "!", - message.split("!")[1], - "success" - ); + setModal({ + type: "success", + title: role === "student" ? "Your mentor has joined you!" : "Your student has joined you!", + message: "You can now also see their moves.", + }); } else { - const message = - role === "student" - ? "You joined your mentor's puzzle! Have fun collaborating." - : "You joined your student's puzzle! Have fun collaborating."; - Swal.fire( - message.split("!")[0] + "!", - message.split("!")[1], - "success" - ); + setModal({ + type: "success", + title: role === "student" ? "You joined your mentor's puzzle!" : "You joined your student's puzzle!", + message: "Have fun collaborating.", + }); } } }, @@ -560,7 +545,6 @@ const Puzzles: React.FC = ({ return () => { window.removeEventListener("beforeunload", handleUnloadRef.current); handleUnloadRef.current(); - Swal.close(); }; }, []); @@ -579,29 +563,46 @@ const Puzzles: React.FC = ({ // RENDER // ============================================================================ + const puzzleButtonClass = "btn-green w-full md:w-auto"; + return ( -
- {!currentFEN ? ( -
Loading puzzle...
- ) : ( -
- -
- )} + <> +
+
+ +
-
-
+
+
+ + {modal && } + ); }; diff --git a/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles-profile.module.scss b/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles-profile.module.scss deleted file mode 100644 index 602c389d..00000000 --- a/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles-profile.module.scss +++ /dev/null @@ -1,57 +0,0 @@ -.mainElements { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - margin-top: 2rem; - gap: 2rem; -} - -.chessBoard { - width: 600px; - height: 600px; - border-radius: 10px; - box-shadow: 0 0 15px rgba(0, 0, 0, 0.2); -} - -.hintMenu { - display: flex; - flex-direction: column; - align-items: center; - gap: 1.5rem; - width: 100%; -} - -.puzzleButtonGroup { - display: flex; - gap: 1rem; - justify-content: center; -} - -.puzzleButton { - padding: 0.8rem 1.6rem; - font-size: 1.1rem; - font-weight: bold; - background-color: #7FCC26; - color: black; - border: none; - border-radius: 6px; - cursor: pointer; - transition: background-color 0.3s ease; -} - -.puzzleButton:hover { - background-color: #5d971b; -} - -.hintText { - max-width: 600px; - padding: 1.5rem; - background-color: #f7f7f7; - border-radius: 8px; - box-shadow: 0 0 8px rgba(0, 0, 0, 0.1); - font-size: 1.05rem; - line-height: 1.6; - color: #333; - text-align: left; -} \ No newline at end of file diff --git a/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.module.scss b/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.module.scss deleted file mode 100644 index 444f9563..00000000 --- a/react-ystemandchess/src/features/puzzles/puzzles-page/Puzzles.module.scss +++ /dev/null @@ -1,75 +0,0 @@ -body { - background-color: #e5f3d2; -} - -.mainElements { - display: flex; - justify-content: center; - align-items: flex-start; - flex-wrap: wrap; - padding: 2rem; - gap: 4rem; // <- INCREASE gap to move buttons closer while keeping board centered - box-sizing: border-box; - width: 100%; - max-width: 1200px; - margin: 0 auto; // <- centers the entire container -} - - -.chessBoard { - width: 100%; - max-width: 600px; - aspect-ratio: 1 / 1; - border: none; - flex-shrink: 0; -} - -.chessBoardWrapper { - width: 100%; - max-width: 600px; - aspect-ratio: 1 / 1; - overflow: hidden; -} - -.hintMenu { - flex: 1; - min-width: 250px; - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -.hintButtonRow { - display: flex; - flex-direction: column; - gap: 1rem; - width: 100%; -} - -.puzzleButton { - padding: 1rem; - font-size: 1rem; - width: 100%; - font-weight: bold; - background-color: #7FCC26; - color: black; - border: none; - border-radius: 6px; - cursor: pointer; -} - -.puzzleButton:hover { - background-color: #5d971b; -} - -@media (min-width: 800px) { - .hintButtonRow { - flex-direction: row; - justify-content: center; - } - - .puzzleButton { - width: auto; - } -} From 263625dabe6c816fe334455fa52f8367f8aabf19 Mon Sep 17 00:00:00 2001 From: F-Hejazi <60328249+F-Hejazi@users.noreply.github.com> Date: Wed, 15 Apr 2026 05:05:43 +0100 Subject: [PATCH 05/11] ui: update login page styling --- .../src/features/auth/login/Login.scss | 113 ------------------ .../src/features/auth/login/Login.tsx | 78 ++++++++---- 2 files changed, 57 insertions(+), 134 deletions(-) delete mode 100644 react-ystemandchess/src/features/auth/login/Login.scss diff --git a/react-ystemandchess/src/features/auth/login/Login.scss b/react-ystemandchess/src/features/auth/login/Login.scss deleted file mode 100644 index 8271ceae..00000000 --- a/react-ystemandchess/src/features/auth/login/Login.scss +++ /dev/null @@ -1,113 +0,0 @@ -body { - margin: 0; - font-family: 'Poppins', sans-serif; -} - -.login-page { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - min-height: 100vh; - padding: 1rem; -} - -.login-title { - font-size: 2.5rem; - color: #1e293b; - margin-bottom: 2rem; - text-align: center; -} - -.login-error { - color: #ef4444; - font-weight: 600; - margin-bottom: 1rem; - text-align: center; -} - -.login-form { - display: flex; - flex-direction: column; - gap: 1.5rem; - width: 100%; - max-width: 360px; - background: white; - padding: 2rem; - border-radius: 20px; - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1); -} - -.input-wrapper { - display: flex; - flex-direction: column; -} - -.input-wrapper label { - font-size: 1rem; - font-weight: 600; - color: #1e293b; - margin-bottom: 0.4rem; -} - -.input-wrapper input { - padding: 12px 16px; - font-size: 1rem; - width: 100%; - border-radius: 8px; - border: 2px solid #cbd5e1; - transition: border 0.3s ease; - background-color: #fff; - font-family: 'Comic Sans MS', cursive; - color: #334155; - box-sizing: border-box; -} - -.input-wrapper input:focus { - border-color: #3b82f6; - outline: none; -} - -.button-wrapper { - display: flex; - justify-content: center; - margin-top: 1rem; -} - -.login-form button { - padding: 12px 40px; - background-color: #3b82f6; - color: white; - font-weight: bold; - font-size: 1.1rem; - border: none; - border-radius: 25px; - box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); - transition: 0.3s; - cursor: pointer; -} - -.login-form button:hover { - background-color: #2563eb; - transform: scale(1.05); -} - -.login-links { - margin-top: 1.5rem; - display: flex; - justify-content: center; - gap: 2rem; - font-size: 1rem; - font-weight: 600; -} - -.login-links a { - color: #1e293b; - text-decoration: none; - border-bottom: 2px solid transparent; - transition: border-color 0.3s; -} - -.login-links a:hover { - border-color: #1e293b; -} diff --git a/react-ystemandchess/src/features/auth/login/Login.tsx b/react-ystemandchess/src/features/auth/login/Login.tsx index 1060c33b..a1cc874f 100644 --- a/react-ystemandchess/src/features/auth/login/Login.tsx +++ b/react-ystemandchess/src/features/auth/login/Login.tsx @@ -1,6 +1,4 @@ import React from "react"; -// import { Link } from 'react-router-dom'; -import './Login.scss'; import { useState } from 'react'; import { environment } from "../../../environments/environment"; import { useCookies } from 'react-cookie'; @@ -109,14 +107,32 @@ const Login = () => { }); }; + const inputClass = (invalid: boolean) => + `w-full rounded-lg border-2 px-4 py-3 text-sm text-dark bg-white caret-dark + focus:outline-none focus:shadow-none transition-colors ${ + invalid ? "border-red" : "border-borderLight focus:border-primary" + }`; + return ( -
-

Login

- {loginError &&
{loginError}
} +
+

Login

+ + {loginError && ( +

+ {loginError} +

+ )} -
-
- + +
+ { setUsername(e.target.value); //usernameVerification(); }} - aria-describedby='loginError-h3' + aria-describedby="loginError-h3" aria-invalid={usernameFlag} required + className={inputClass(usernameFlag)} />
-
- + +
+ { setPassword(e.target.value); //passwordVerification(); }} - aria-describedby='loginError-h3' + aria-describedby="loginError-h3" aria-invalid={passwordFlag} required + className={inputClass(passwordFlag)} />
-
- + +
+
-