diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91c47b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.aider* diff --git a/README.md b/README.md index c300135..db579d2 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ If you are developing a production application, we recommend updating the config ```js export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ // Other configs... @@ -34,42 +34,40 @@ export default defineConfig([ ], languageOptions: { parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], + project: ["./tsconfig.node.json", "./tsconfig.app.json"], tsconfigRootDir: import.meta.dirname, }, // other options... }, }, -]) - +]); ``` You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: ```js // eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' +import reactX from "eslint-plugin-react-x"; +import reactDom from "eslint-plugin-react-dom"; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ // Other configs... // Enable lint rules for React - reactX.configs['recommended-typescript'], + reactX.configs["recommended-typescript"], // Enable lint rules for React DOM reactDom.configs.recommended, ], languageOptions: { parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], + project: ["./tsconfig.node.json", "./tsconfig.app.json"], tsconfigRootDir: import.meta.dirname, }, // other options... }, }, -]) - +]); ``` diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 0000000..9cb3489 --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + e2e: { + baseUrl: "http://localhost:5173", + specPattern: "cypress/e2e/**/*.cy.{js,jsx,ts,tsx}", + supportFile: false, + }, +}); diff --git a/cypress/e2e/clock.cy.ts b/cypress/e2e/clock.cy.ts new file mode 100644 index 0000000..301fa55 --- /dev/null +++ b/cypress/e2e/clock.cy.ts @@ -0,0 +1,212 @@ +/// + +describe("Berlin Clock", () => { + const range = Cypress._.range; + + beforeEach(() => { + cy.visit("/"); + }); + + const enterTime = (time: string) => { + cy.get('[data-testid="time-input"]').clear().type(time).type("{enter}"); + }; + + const getLamp = (rowTestId: string, index: number) => { + return cy.get(`[data-testid="${rowTestId}"] [data-testid="lamp-${index}"]`); + }; + + const assertLampColors = (rowTestId: string, expectedColors: string) => { + range(expectedColors.length).forEach((i) => { + getLamp(rowTestId, i).should( + "have.attr", + "aria-label", + expectedColors[i], + ); + }); + }; + + describe("Seconds Lamp", () => { + it("should show Y at 00:00:00", () => { + enterTime("00:00:00"); + cy.get('[data-testid="seconds-lamp"]').should( + "have.attr", + "aria-label", + "Y", + ); + }); + + it("should show O at 23:59:59", () => { + enterTime("23:59:59"); + cy.get('[data-testid="seconds-lamp"]').should( + "have.attr", + "aria-label", + "O", + ); + }); + }); + + describe("Five Hours Row", () => { + it("should show OOOO at 00:00:00", () => { + enterTime("00:00:00"); + assertLampColors("five-hours-row", "OOOO"); + }); + + it("should show RRRR at 23:59:59", () => { + enterTime("23:59:59"); + assertLampColors("five-hours-row", "RRRR"); + }); + + it("should show OOOO at 02:04:00", () => { + enterTime("02:04:00"); + assertLampColors("five-hours-row", "OOOO"); + }); + + it("should show ROOO at 08:23:00", () => { + enterTime("08:23:00"); + assertLampColors("five-hours-row", "ROOO"); + }); + + it("should show RRRO at 16:35:00", () => { + enterTime("16:35:00"); + assertLampColors("five-hours-row", "RRRO"); + }); + }); + + describe("Single Hours Row", () => { + it("should show OOOO at 00:00:00", () => { + enterTime("00:00:00"); + assertLampColors("single-hours-row", "OOOO"); + }); + + it("should show RRRO at 23:59:59", () => { + enterTime("23:59:59"); + assertLampColors("single-hours-row", "RRRO"); + }); + + it("should show RROO at 02:04:00", () => { + enterTime("02:04:00"); + assertLampColors("single-hours-row", "RROO"); + }); + + it("should show RRRO at 08:23:00", () => { + enterTime("08:23:00"); + assertLampColors("single-hours-row", "RRRO"); + }); + + it("should show RRRR at 14:35:00", () => { + enterTime("14:35:00"); + assertLampColors("single-hours-row", "RRRR"); + }); + }); + + describe("Five Minutes Row", () => { + it("should show OOOOOOOOOOO at 00:00:00", () => { + enterTime("00:00:00"); + assertLampColors("five-minutes-row", "OOOOOOOOOOO"); + }); + + it("should show YYRYYRYYRYY at 23:59:59", () => { + enterTime("23:59:59"); + assertLampColors("five-minutes-row", "YYRYYRYYRYY"); + }); + + it("should show OOOOOOOOOOO at 12:04:00", () => { + enterTime("12:04:00"); + assertLampColors("five-minutes-row", "OOOOOOOOOOO"); + }); + + it("should show YYRYOOOOOOO at 12:23:00", () => { + enterTime("12:23:00"); + assertLampColors("five-minutes-row", "YYRYOOOOOOO"); + }); + + it("should show YYRYYRYOOOO at 12:35:00", () => { + enterTime("12:35:00"); + assertLampColors("five-minutes-row", "YYRYYRYOOOO"); + }); + }); + + describe("Single Minutes Row", () => { + it("should show OOOO at 00:00:00", () => { + enterTime("00:00:00"); + assertLampColors("single-minutes-row", "OOOO"); + }); + + it("should show YYYY at 23:59:59", () => { + enterTime("23:59:59"); + assertLampColors("single-minutes-row", "YYYY"); + }); + + it("should show YYOO at 12:32:00", () => { + enterTime("12:32:00"); + assertLampColors("single-minutes-row", "YYOO"); + }); + + it("should show YYYY at 12:34:00", () => { + enterTime("12:34:00"); + assertLampColors("single-minutes-row", "YYYY"); + }); + + it("should show OOOO at 12:35:00", () => { + enterTime("12:35:00"); + assertLampColors("single-minutes-row", "OOOO"); + }); + }); + + describe("Full Clock Integration", () => { + const assertFullClock = (time: string, expected: string) => { + enterTime(time); + + // Seconds + cy.get('[data-testid="seconds-lamp"]').should( + "have.attr", + "aria-label", + expected[0], + ); + + // Five hours (indices 1-4) + range(4).forEach((i) => { + cy.get( + `[data-testid="five-hours-row"] [data-testid="lamp-${i}"]`, + ).should("have.attr", "aria-label", expected[1 + i]); + }); + + // Single hours (indices 5-8) + range(4).forEach((i) => { + cy.get( + `[data-testid="single-hours-row"] [data-testid="lamp-${i}"]`, + ).should("have.attr", "aria-label", expected[5 + i]); + }); + + // Five minutes (indices 9-19) + range(11).forEach((i) => { + cy.get( + `[data-testid="five-minutes-row"] [data-testid="lamp-${i}"]`, + ).should("have.attr", "aria-label", expected[9 + i]); + }); + + // Single minutes (indices 20-23) + range(4).forEach((i) => { + cy.get( + `[data-testid="single-minutes-row"] [data-testid="lamp-${i}"]`, + ).should("have.attr", "aria-label", expected[20 + i]); + }); + }; + + it("displays correct clock for 00:00:00", () => { + assertFullClock("00:00:00", "YOOOOOOOOOOOOOOOOOOOOOOO"); + }); + + it("displays correct clock for 23:59:59", () => { + assertFullClock("23:59:59", "ORRRRRRROYYRYYRYYRYYYYYY"); + }); + + it("displays correct clock for 16:50:06", () => { + assertFullClock("16:50:06", "YRRROROOOYYRYYRYYRYOOOOO"); + }); + + it("displays correct clock for 11:37:01", () => { + assertFullClock("11:37:01", "ORROOROOOYYRYYRYOOOOYYOO"); + }); + }); +}); diff --git a/eslint.config.js b/eslint.config.js index ef614d2..da6f026 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,14 +1,14 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import { defineConfig, globalIgnores } from "eslint/config"; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(["dist"]), { - files: ['**/*.{ts,tsx}'], + files: ["**/*.{ts,tsx}"], extends: [ js.configs.recommended, tseslint.configs.recommended, @@ -19,4 +19,4 @@ export default defineConfig([ globals: globals.browser, }, }, -]) +]); diff --git a/package-lock.json b/package-lock.json index d0e9369..9b86faf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "globals": "^17.6.0", + "prettier": "^3.8.4", "typescript": "~6.0.2", "typescript-eslint": "^8.61.0", "vite": "^8.1.0", @@ -5025,6 +5026,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", diff --git a/package.json b/package.json index 30bdff2..73d9570 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.3", "globals": "^17.6.0", + "prettier": "^3.8.4", "typescript": "~6.0.2", "typescript-eslint": "^8.61.0", "vite": "^8.1.0", diff --git a/src/App.css b/src/App.css index f90339d..f460279 100644 --- a/src/App.css +++ b/src/App.css @@ -167,7 +167,7 @@ &::before, &::after { - content: ''; + content: ""; position: absolute; top: -4.5px; border: 5px solid transparent; diff --git a/src/App.tsx b/src/App.tsx index a66b5ef..1fd432d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,11 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from './assets/vite.svg' -import heroImg from './assets/hero.png' -import './App.css' +import { useState } from "react"; +import reactLogo from "./assets/react.svg"; +import viteLogo from "./assets/vite.svg"; +import heroImg from "./assets/hero.png"; +import "./App.css"; function App() { - const [count, setCount] = useState(0) + const [count, setCount] = useState(0); return ( <> @@ -116,7 +116,7 @@ function App() {
- ) + ); } -export default App +export default App; diff --git a/src/index.css b/src/index.css index 5fb3313..aad5cb1 100644 --- a/src/index.css +++ b/src/index.css @@ -11,8 +11,8 @@ --shadow: rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; + --sans: system-ui, "Segoe UI", Roboto, sans-serif; + --heading: system-ui, "Segoe UI", Roboto, sans-serif; --mono: ui-monospace, Consolas, monospace; font: 18px/145% var(--sans); diff --git a/src/main.tsx b/src/main.tsx index bef5202..eff7ccc 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( , -) +); diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..0e43ae8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; // https://vite.dev/config/ export default defineConfig({ plugins: [react()], -}) +});