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()],
-})
+});