From 762ead2a9e1f0e2b8129e7119219b58b7ec4763d Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 8 Jul 2024 21:54:48 -0400 Subject: [PATCH 001/113] Update README.md --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 482b67a..92479ff 100644 --- a/README.md +++ b/README.md @@ -1 +1,54 @@ -# Digital_Services.HUB \ No newline at end of file +Here's a sample README for your repository with links to the two branches: + +--- + +# Digital Services Hub + +Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available and links to the respective branches. + +## Themes + +### Cutting-Edge Theme +A futuristic and vibrant theme with glowing elements and modern design aesthetics. + +- **Branch:** [Cutting-Edge-Theme](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Cutting-Edge-Theme) + +### Minimalistic Theme +A clean and simple theme focused on usability and minimalist design principles. + +- **Branch:** [Minimalistic-Theme](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Minimalistic-Theme) + +## Features + +- **Image Resizer:** Easily resize images by specifying width and height. +- **Color Palette Generator:** Create and explore various color palettes for design projects. + +## Usage + +1. Clone the repository: + ```sh + git clone https://github.com/TMHSDigital/Digital_Services.HUB.git + ``` + +2. Checkout to a specific theme branch: + ```sh + git checkout Cutting-Edge-Theme + ``` + or + ```sh + git checkout Minimalistic-Theme + ``` + +3. Open `index.html` in your browser to explore the tools. + +## Contributing + +Feel free to contribute by opening issues or submitting pull requests. + +## License + +This project is licensed under the MIT License. + +--- + +This README provides a clear overview of your project with direct links to the theme branches. Adjust the content as needed to fit any additional details specific to your project. From 30af8c624daf6d8376bce09d0a3f108c6dca054e Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:02:03 -0400 Subject: [PATCH 002/113] Update README.md --- README.md | 50 +++++++++++++++++++------------------------------- 1 file changed, 19 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 92479ff..8b091b4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,26 @@ -Here's a sample README for your repository with links to the two branches: +# Digital Services Hub + +Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available and links to the respective branches. --- -# Digital Services Hub + +
+ + Cutting-Edge-Theme + +
-Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available and links to the respective branches. +--- + + +
+ + Minimalistic-Theme + +
+ +--- ## Themes @@ -23,32 +39,4 @@ A clean and simple theme focused on usability and minimalist design principles. - **Image Resizer:** Easily resize images by specifying width and height. - **Color Palette Generator:** Create and explore various color palettes for design projects. -## Usage - -1. Clone the repository: - ```sh - git clone https://github.com/TMHSDigital/Digital_Services.HUB.git - ``` - -2. Checkout to a specific theme branch: - ```sh - git checkout Cutting-Edge-Theme - ``` - or - ```sh - git checkout Minimalistic-Theme - ``` - -3. Open `index.html` in your browser to explore the tools. - -## Contributing - -Feel free to contribute by opening issues or submitting pull requests. - -## License - -This project is licensed under the MIT License. - --- - -This README provides a clear overview of your project with direct links to the theme branches. Adjust the content as needed to fit any additional details specific to your project. From dd7c067c6db29c79ebd81f86da202fcda21b0558 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:26:03 -0400 Subject: [PATCH 003/113] Update README.md --- README.md | 58 +++++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 8b091b4..e64ca42 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,42 @@ # Digital Services Hub -Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available and links to the respective branches. - ---- - - -
- - Cutting-Edge-Theme - -
- ---- - - -
- - Minimalistic-Theme - -
- ---- +Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available, features, and usage instructions. ## Themes -### Cutting-Edge Theme -A futuristic and vibrant theme with glowing elements and modern design aesthetics. +> ### [![Cutting-Edge-Theme](https://img.shields.io/badge/Cutting--Edge-Theme-blue?style=for-the-badge)](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Cutting-Edge-Theme) +> ___A futuristic and vibrant theme with glowing elements and modern design aesthetics.___ -- **Branch:** [Cutting-Edge-Theme](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Cutting-Edge-Theme) +___ -### Minimalistic Theme -A clean and simple theme focused on usability and minimalist design principles. - -- **Branch:** [Minimalistic-Theme](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Minimalistic-Theme) +> ### [![Minimalistic-Theme](https://img.shields.io/badge/Minimalistic-Theme-green?style=for-the-badge)](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Minimalistic-Theme) +> ___A clean and simple theme focused on usability and minimalist design principles.___ ## Features - **Image Resizer:** Easily resize images by specifying width and height. - **Color Palette Generator:** Create and explore various color palettes for design projects. ---- +## Usage + +1. Click on the button below to visit the GitHub Pages site: + +

+ + Visit Site + +

+ +
+Connect +

+ + GitHub Profile + +

+
+ +
+License +

This project is licensed under the MIT License.

+
From 442a5db7bff0f977f0bf604dfdecaca3acbaf39c Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:29:39 -0400 Subject: [PATCH 004/113] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e64ca42..85d4ea9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,10 @@ ___ - **Image Resizer:** Easily resize images by specifying width and height. - **Color Palette Generator:** Create and explore various color palettes for design projects. -## Usage +------ + +
+Usage 1. Click on the button below to visit the GitHub Pages site: @@ -27,6 +30,8 @@ ___

+
+
Connect

From 714cc6cb0be6fa1c61f5f1bbffe7fc51d13e0b9e Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 8 Jul 2024 22:31:44 -0400 Subject: [PATCH 005/113] Update README.md --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 482b67a..85d4ea9 100644 --- a/README.md +++ b/README.md @@ -1 +1,47 @@ -# Digital_Services.HUB \ No newline at end of file +# Digital Services Hub + +Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available, features, and usage instructions. + +## Themes + +> ### [![Cutting-Edge-Theme](https://img.shields.io/badge/Cutting--Edge-Theme-blue?style=for-the-badge)](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Cutting-Edge-Theme) +> ___A futuristic and vibrant theme with glowing elements and modern design aesthetics.___ + +___ + +> ### [![Minimalistic-Theme](https://img.shields.io/badge/Minimalistic-Theme-green?style=for-the-badge)](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Minimalistic-Theme) +> ___A clean and simple theme focused on usability and minimalist design principles.___ + +## Features + +- **Image Resizer:** Easily resize images by specifying width and height. +- **Color Palette Generator:** Create and explore various color palettes for design projects. + +------ + +

+Usage + +1. Click on the button below to visit the GitHub Pages site: + +

+ + Visit Site + +

+ +
+ +
+Connect +

+ + GitHub Profile + +

+
+ +
+License +

This project is licensed under the MIT License.

+
From f9f38c049943b8fe44d232a229d5f7f157137713 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:02:14 -0400 Subject: [PATCH 006/113] Update README.md --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 85d4ea9..939539f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,19 @@ Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available, features, and usage instructions. +## Repository Stats + +![Issues](https://img.shields.io/github/issues/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Last Commit](https://img.shields.io/github/last-commit/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![License](https://img.shields.io/github/license/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Commit Activity](https://img.shields.io/github/commit-activity/m/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Code Size](https://img.shields.io/github/languages/code-size/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Repository Size](https://img.shields.io/github/repo-size/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Language Count](https://img.shields.io/github/languages/count/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Top Language](https://img.shields.io/github/languages/top/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Open Issues](https://img.shields.io/github/issues-raw/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Closed Issues](https://img.shields.io/github/issues-closed-raw/TMHSDigital/Digital_Services.HUB?style=for-the-badge) + ## Themes > ### [![Cutting-Edge-Theme](https://img.shields.io/badge/Cutting--Edge-Theme-blue?style=for-the-badge)](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Cutting-Edge-Theme) From fc748b115676eaa785ae0170a46b10206a746abc Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Mon, 8 Jul 2024 23:02:40 -0400 Subject: [PATCH 007/113] Update README.md --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 85d4ea9..939539f 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,19 @@ Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available, features, and usage instructions. +## Repository Stats + +![Issues](https://img.shields.io/github/issues/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Last Commit](https://img.shields.io/github/last-commit/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![License](https://img.shields.io/github/license/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Commit Activity](https://img.shields.io/github/commit-activity/m/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Code Size](https://img.shields.io/github/languages/code-size/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Repository Size](https://img.shields.io/github/repo-size/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Language Count](https://img.shields.io/github/languages/count/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Top Language](https://img.shields.io/github/languages/top/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Open Issues](https://img.shields.io/github/issues-raw/TMHSDigital/Digital_Services.HUB?style=for-the-badge) +![Closed Issues](https://img.shields.io/github/issues-closed-raw/TMHSDigital/Digital_Services.HUB?style=for-the-badge) + ## Themes > ### [![Cutting-Edge-Theme](https://img.shields.io/badge/Cutting--Edge-Theme-blue?style=for-the-badge)](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Cutting-Edge-Theme) From 9c603f2aece8a4a4394bfeab59f824fa5f6c7e85 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 01:12:22 -0400 Subject: [PATCH 008/113] Claude update Digital Services Hub

Digital Services Hub

Welcome to our Digital Services Hub!

We offer a range of free tools to help with your digital media needs:

Choose a service above to get started, or visit our About page to learn more.

From 62aad30a200e5a060c7bf392babc27bc9ad4eb78 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 01:12:59 -0400 Subject: [PATCH 009/113] Update image-resizer.html From 8ed24a6e55439881b94a6ea80aa9f5d540a376b4 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 01:13:18 -0400 Subject: [PATCH 010/113] Update color-palette.html From 2aef5021ed8cb5d8f96f459e30d3789936c1f527 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 01:13:32 -0400 Subject: [PATCH 011/113] Update about.html From a26e58c66cc07dd6b099d953441de0df40789691 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 01:25:16 -0400 Subject: [PATCH 012/113] Add files via upload --- js/ascii-art-css.txt | 26 ++++++++++++++++++++++++ js/ascii-art-html.html | 35 +++++++++++++++++++++++++++++++++ js/ascii-art-js.txt | 19 ++++++++++++++++++ js/qr-generator-html.html | 36 ++++++++++++++++++++++++++++++++++ js/qr-generator-js.txt | 33 +++++++++++++++++++++++++++++++ js/updated-common-js (1).txt | 15 ++++++++++++++ js/updated-common-js.txt | 14 +++++++++++++ js/updated-index-html (1).html | 3 +++ js/updated-index-html (2).html | 8 ++++++++ js/updated-index-html.html | 7 +++++++ 10 files changed, 196 insertions(+) create mode 100644 js/ascii-art-css.txt create mode 100644 js/ascii-art-html.html create mode 100644 js/ascii-art-js.txt create mode 100644 js/qr-generator-html.html create mode 100644 js/qr-generator-js.txt create mode 100644 js/updated-common-js (1).txt create mode 100644 js/updated-common-js.txt create mode 100644 js/updated-index-html (1).html create mode 100644 js/updated-index-html (2).html create mode 100644 js/updated-index-html.html diff --git a/js/ascii-art-css.txt b/js/ascii-art-css.txt new file mode 100644 index 0000000..85f3d6b --- /dev/null +++ b/js/ascii-art-css.txt @@ -0,0 +1,26 @@ +.ascii-output { + background-color: #1a1a1a; + border: 1px solid #00fff2; + border-radius: 5px; + padding: 1rem; + margin-top: 1rem; + font-family: monospace; + white-space: pre; + overflow-x: auto; +} + +#ascii-output { + color: #00fff2; + font-size: 0.8rem; + line-height: 1; +} + +#text-input { + width: 100%; + background-color: #1f2937; + border: 1px solid #374151; + color: #fff; + padding: 0.5rem; + border-radius: 5px; + resize: vertical; +} diff --git a/js/ascii-art-html.html b/js/ascii-art-html.html new file mode 100644 index 0000000..d1afd69 --- /dev/null +++ b/js/ascii-art-html.html @@ -0,0 +1,35 @@ + + + + + + ASCII Art Converter - Digital Services Hub + + + + +
+

ASCII Art Converter

+ + + +
+
+ + +
+ + + +
+

+            
+ +

Enter your text and click 'Convert to ASCII Art' to see the result.

+
+
+ + + + + diff --git a/js/ascii-art-js.txt b/js/ascii-art-js.txt new file mode 100644 index 0000000..6ea39fa --- /dev/null +++ b/js/ascii-art-js.txt @@ -0,0 +1,19 @@ +const textInput = document.getElementById('text-input'); +const convertButton = document.getElementById('convert-button'); +const asciiOutput = document.getElementById('ascii-output'); + +const asciiChars = ['@', '#', 'S', '%', '?', '*', '+', ';', ':', ',', '.']; + +function textToAscii(text) { + return text.split('\n').map(line => + line.split('').map(char => + asciiChars[Math.floor(Math.random() * asciiChars.length)] + ).join('') + ).join('\n'); +} + +convertButton.addEventListener('click', () => { + const inputText = textInput.value; + const asciiArt = textToAscii(inputText); + asciiOutput.textContent = asciiArt; +}); diff --git a/js/qr-generator-html.html b/js/qr-generator-html.html new file mode 100644 index 0000000..60e44f0 --- /dev/null +++ b/js/qr-generator-html.html @@ -0,0 +1,36 @@ + + + + + + QR Code Generator - Digital Services Hub + + + + + +
+

QR Code Generator

+ + + +
+
+ + +
+ + + +
+ + + +

Enter text or a URL and click 'Generate QR Code' to create your QR code.

+
+
+ + + + + diff --git a/js/qr-generator-js.txt b/js/qr-generator-js.txt new file mode 100644 index 0000000..9d9c177 --- /dev/null +++ b/js/qr-generator-js.txt @@ -0,0 +1,33 @@ +const qrInput = document.getElementById('qr-input'); +const generateButton = document.getElementById('generate-button'); +const qrOutput = document.getElementById('qr-output'); +const downloadLink = document.getElementById('download-link'); + +let qr = null; + +generateButton.addEventListener('click', () => { + const inputText = qrInput.value; + if (inputText) { + if (qr) { + qr.clear(); + qr.makeCode(inputText); + } else { + qr = new QRCode(qrOutput, { + text: inputText, + width: 256, + height: 256, + colorDark: "#000000", + colorLight: "#ffffff", + correctLevel: QRCode.CorrectLevel.H + }); + } + + // Enable download after a short delay to ensure QR code is generated + setTimeout(() => { + const qrImage = qrOutput.querySelector('img'); + downloadLink.href = qrImage.src; + downloadLink.download = 'qrcode.png'; + downloadLink.style.display = 'inline-block'; + }, 100); + } +}); diff --git a/js/updated-common-js (1).txt b/js/updated-common-js (1).txt new file mode 100644 index 0000000..672a2d1 --- /dev/null +++ b/js/updated-common-js (1).txt @@ -0,0 +1,15 @@ +document.addEventListener('DOMContentLoaded', (event) => { + const nav = document.getElementById('main-nav'); + const currentPath = window.location.pathname; + + const navItems = [ + { href: "index.html", text: "Home" }, + { href: "pages/image-resizer.html", text: "Image Resizer" }, + { href: "pages/color-palette.html", text: "Color Palette" }, + { href: "pages/ascii-art.html", text: "ASCII Art" }, + { href: "pages/qr-generator.html", text: "QR Code Generator" }, + { href: "pages/about.html", text: "About" } + ]; + + // ... rest of the code remains the same +}); diff --git a/js/updated-common-js.txt b/js/updated-common-js.txt new file mode 100644 index 0000000..cadcace --- /dev/null +++ b/js/updated-common-js.txt @@ -0,0 +1,14 @@ +document.addEventListener('DOMContentLoaded', (event) => { + const nav = document.getElementById('main-nav'); + const currentPath = window.location.pathname; + + const navItems = [ + { href: "index.html", text: "Home" }, + { href: "pages/image-resizer.html", text: "Image Resizer" }, + { href: "pages/color-palette.html", text: "Color Palette" }, + { href: "pages/ascii-art.html", text: "ASCII Art" }, + { href: "pages/about.html", text: "About" } + ]; + + // ... rest of the code remains the same +}); diff --git a/js/updated-index-html (1).html b/js/updated-index-html (1).html new file mode 100644 index 0000000..e986e0a --- /dev/null +++ b/js/updated-index-html (1).html @@ -0,0 +1,3 @@ + +
- ## License
From f1be8e116b925dbc0538dcace8abd921e7b5f5fb Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 02:34:41 -0400 Subject: [PATCH 045/113] Rename FUTURE-FEATURES.md to docs/FUTURE-FEATURES.md --- FUTURE-FEATURES.md => docs/FUTURE-FEATURES.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename FUTURE-FEATURES.md => docs/FUTURE-FEATURES.md (100%) diff --git a/FUTURE-FEATURES.md b/docs/FUTURE-FEATURES.md similarity index 100% rename from FUTURE-FEATURES.md rename to docs/FUTURE-FEATURES.md From 7bdfee2fc17387050dcf865d8469012c429dcc03 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 02:35:08 -0400 Subject: [PATCH 046/113] Rename README.md to docs/README.md --- README.md => docs/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README.md => docs/README.md (100%) diff --git a/README.md b/docs/README.md similarity index 100% rename from README.md rename to docs/README.md From e96ae0853177c7d242231f034b24bf76e424cd05 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 02:38:08 -0400 Subject: [PATCH 047/113] Create config.yml (JEKYLL CONFIG FOR .md TABS)(ADDED.md TO DOCS FOLDER) --- config.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 config.yml diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..95b8bd5 --- /dev/null +++ b/config.yml @@ -0,0 +1,7 @@ +theme: jekyll-theme-cayman + +navigation: + - title: "Home" + url: README.md + - title: "Future Features" + url: FUTURE-FEATURES.md From ed2385cf4d7a1f5ec79f66380659f7faef117243 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Tue, 9 Jul 2024 02:42:43 -0400 Subject: [PATCH 048/113] Update README.md --- docs/README.md | 64 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/docs/README.md b/docs/README.md index e555405..ce434b9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,11 @@ Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available, features, and usage instructions. +## Documentation + +- [README](docs/README.md) +- [Future Features](docs/FUTURE-FEATURES.md) + ## Repository Stats ![GitHub Stars](https://img.shields.io/github/stars/TMHSDigital/Digital_Services.HUB?style=for-the-badge) @@ -39,23 +44,61 @@ ___ ## 🚀 Upcoming Features -We're constantly innovating! Check out our [Future Features Roadmap](FUTURE-FEATURES.md) for exciting upcoming additions. +We're constantly innovating! Check out our [Future Features Roadmap](docs/FUTURE-FEATURES.md) for exciting upcoming additions. ## Usage
How to Use -1. Click on the button below to visit the GitHub Pages site: -

- - Visit Site - -

+### General Usage Instructions: + +1. **Visit the GitHub Pages Site:** + Click on the button below to visit our GitHub Pages site where all tools are hosted: +

+ + Visit Site + +

+ +2. **Select a Theme:** + Choose your preferred theme from the available options. Each theme offers a unique look and feel to enhance your user experience. + +3. **Navigate to the Desired Tool:** + Browse through the list of available tools. Click on the tool you want to use. Each tool is designed to be intuitive and user-friendly. -2. Choose your preferred theme from the available options. -3. Navigate to the desired tool and follow the on-screen instructions. -4. Enjoy the enhanced digital experience! +4. **Follow On-Screen Instructions:** + Each tool comes with its own set of instructions. Follow these instructions to utilize the tool effectively. For instance: + + - **Image Resizer:** Upload an image, specify the desired width and height, and click "Resize" to get the resized image. + - **Color Palette Generator:** Choose or input base colors, and the tool will generate a palette of complementary colors. + - **ASCII Art Converter:** Input your text, choose formatting options, and click "Convert" to generate ASCII art. + - **QR Code Generator:** Input the data you want encoded, choose customization options, and generate the QR code. + +5. **Enjoy Enhanced Digital Experience:** + Utilize the results as needed. Download images, copy text, or use the generated content in your projects. + +### Detailed Tool Instructions: + +#### Image Resizer: +- **Upload an Image:** Click on the "Upload" button to select an image from your device. +- **Specify Dimensions:** Enter the desired width and height for the image. +- **Resize:** Click "Resize" to process the image. The resized image will be available for download. + +#### Color Palette Generator: +- **Select Base Colors:** Either select colors using a color picker or input hex values. +- **Generate Palette:** Click "Generate" to see a palette of complementary colors. +- **Explore Variations:** Adjust the base colors and regenerate as needed. + +#### ASCII Art Converter: +- **Input Text:** Type or paste the text you want to convert. +- **Choose Options:** Select font style, size, and other formatting options. +- **Convert:** Click "Convert" to see your text in ASCII art format. Copy the art for use. + +#### QR Code Generator: +- **Input Data:** Enter the URL or text you want to encode in the QR code. +- **Customize:** Choose color, size, and error correction level. +- **Generate:** Click "Generate" to create the QR code. Download or share it directly.
@@ -68,6 +111,7 @@ We're constantly innovating! Check out our [Future Features Roadmap](FUTURE-FEAT GitHub Profile

+
## License From a8ea219485ba608add85ecb1b77395a9aaa31bca Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:11:24 -0400 Subject: [PATCH 049/113] add project description .md --- docs/PROJECT-DESCRIPTION.md | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/PROJECT-DESCRIPTION.md diff --git a/docs/PROJECT-DESCRIPTION.md b/docs/PROJECT-DESCRIPTION.md new file mode 100644 index 0000000..c4eb3c0 --- /dev/null +++ b/docs/PROJECT-DESCRIPTION.md @@ -0,0 +1,81 @@ +Project Overview: Digital Services HUB + +This project appears to be a web-based platform offering various digital tools and services. Based on the file structure and names, it seems to include several standalone tools accessible through a common interface. + +Project Structure: + +1. Root Directory: + - index.html (main entry point) + - LICENSE file + +2. CSS Directory: + - ascii-art.css + - color-palette.css + - image-resizer.css + - qr-generator.css + - styles.css (likely the main stylesheet) + +3. JavaScript Directory (js): + - ascii-art.js + - color-palette.js + - common.js (shared functionality across pages) + - image-resizer.js + - qr-generator.js + +4. HTML Pages Directory (pages): + - about.html + - ascii-art.html + - color-palette.html + - image-resizer.html + - qr-generator.html + +5. Images Directory: + - Contains .png file(s) + +6. Docs Directory: + - FUTURE-FEATURES.md + - README.md + +7. Configuration: + - config.yml + +Main Features: + +1. Image Resizer: Allows users to resize images. +2. Color Palette Generator: Likely generates color schemes or palettes. +3. ASCII Art Converter: Converts images or text into ASCII art. +4. QR Code Generator: Creates QR codes from input data. +5. About Page: Provides information about the project or services. + +Recent Updates: +- We've added localStorage functionality to common.js to allow for saving user preferences and recent operations across sessions. +- The navigation is dynamically generated in common.js, ensuring consistency across all pages. + +Key Components: + +1. common.js: + - Handles site-wide functionality like navigation. + - Includes localStorage utilities for data persistence. + +2. Tool-specific JS files (e.g., image-resizer.js): + - Contain the core logic for each tool. + - Have been updated to use localStorage for saving user preferences. + +3. HTML files: + - Provide the structure for each tool's interface. + - Link to both common and tool-specific CSS and JS files. + +4. CSS files: + - Style the interface for each tool and the overall site. + +Next Steps: + +1. Review and update each tool-specific JS file to implement localStorage functionality similar to image-resizer.js. +2. Ensure all HTML files are properly structured and linked to the correct CSS and JS files. +3. Test each tool thoroughly to ensure proper functionality and data persistence. +4. Consider implementing additional features or improvements as outlined in FUTURE-FEATURES.md. +5. Update the README.md with any new information about the project's features and usage. + +This project appears to be a well-structured, modular web application providing various digital services. The use of separate HTML, CSS, and JS files for each tool allows for easy maintenance and scalability. The recent addition of localStorage functionality enhances user experience by remembering preferences and recent operations. + +Is there any specific area of the project you'd like to focus on or any particular feature you'd like to implement or improve? \ No newline at end of file From 0019f6a9b91172636735f1a9190b00a400255d0e Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:06:03 -0400 Subject: [PATCH 050/113] PROJECT-OVERVIEW.md creation --- docs/PROJECT-OVERVIEW.md | 322 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 docs/PROJECT-OVERVIEW.md diff --git a/docs/PROJECT-OVERVIEW.md b/docs/PROJECT-OVERVIEW.md new file mode 100644 index 0000000..0f25c9a --- /dev/null +++ b/docs/PROJECT-OVERVIEW.md @@ -0,0 +1,322 @@ +1. Root Directory: + +--- START OF index.html --- + + + + + + Digital Services Hub + + + + +
+

Digital Services Hub

+ + +
+

Welcome to our Digital Services Hub!

+

We offer a range of free tools to help with your digital media needs:

+ + +

Choose a service above to get started, or visit our About page to learn more.

+
+
+ + + +--- END OF index.html --- + +--- START OF LICENSE --- +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +--- END OF LICENSE --- + +Now, let's move to the CSS directory: + +2. CSS Directory: + +--- START OF styles.css --- +body { + font-family: 'Roboto', sans-serif; + background-color: #0c0c0c; + color: #ffffff; + line-height: 1.6; + margin: 0; + padding: 0; +} + +.container { + width: 80%; + margin: auto; + overflow: hidden; + padding: 20px; +} + +h1, h2, h3 { + font-family: 'Orbitron', sans-serif; + margin-bottom: 20px; +} + +.neon-text { + color: #fff; + text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #ff00de, 0 0 35px #ff00de, 0 0 40px #ff00de, 0 0 50px #ff00de, 0 0 75px #ff00de; +} + +nav { + background: #1a1a1a; + padding: 10px 0; +} + +nav a { + color: #ffffff; + text-decoration: none; + padding: 10px 15px; + margin: 0 5px; + transition: 0.3s; +} + +nav a:hover { + background: #ff00de; + border-radius: 5px; +} + +.content { + background: #1a1a1a; + padding: 20px; + margin-top: 20px; + border-radius: 5px; +} + +.welcome-text { + font-size: 1.5em; + margin-bottom: 20px; +} + +.services-list { + list-style-type: none; + padding: 0; +} + +.services-list li { + margin-bottom: 10px; +} + +.services-list a { + color: #00ffff; + text-decoration: none; + transition: 0.3s; +} + +.services-list a:hover { + color: #ff00de; +} + +.cta-text { + margin-top: 20px; + font-style: italic; +} + +/* Add more styles as needed for specific pages */ +--- END OF styles.css --- + +--- START OF ascii-art.css --- +/* Styles specific to the ASCII Art page */ +.ascii-container { + background: #0f0f0f; + border: 1px solid #2a2a2a; + border-radius: 5px; + padding: 20px; + margin-top: 20px; +} + +#input-text, #output-ascii { + width: 100%; + padding: 10px; + margin-bottom: 10px; + background: #1a1a1a; + border: 1px solid #2a2a2a; + color: #ffffff; + font-family: monospace; +} + +#output-ascii { + white-space: pre; + overflow-x: auto; +} + +#convert-button { + background: #ff00de; + color: #ffffff; + border: none; + padding: 10px 20px; + cursor: pointer; + transition: 0.3s; +} + +#convert-button:hover { + background: #00ffff; + color: #0c0c0c; +} +--- END OF ascii-art.css --- + +--- START OF color-palette.css --- +/* Styles specific to the Color Palette Generator page */ +.palette-container { + display: flex; + flex-wrap: wrap; + justify-content: space-around; + margin-top: 20px; +} + +.color-swatch { + width: 100px; + height: 100px; + margin: 10px; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + font-family: monospace; + color: #ffffff; + text-shadow: 1px 1px 1px rgba(0,0,0,0.5); +} + +#generate-button { + background: #ff00de; + color: #ffffff; + border: none; + padding: 10px 20px; + cursor: pointer; + transition: 0.3s; + margin-top: 20px; +} + +#generate-button:hover { + background: #00ffff; + color: #0c0c0c; +} +--- END OF color-palette.css --- + +--- START OF image-resizer.css --- +/* Styles specific to the Image Resizer page */ +.resizer-container { + background: #0f0f0f; + border: 1px solid #2a2a2a; + border-radius: 5px; + padding: 20px; + margin-top: 20px; +} + +#image-input, #width, #height { + width: 100%; + padding: 10px; + margin-bottom: 10px; + background: #1a1a1a; + border: 1px solid #2a2a2a; + color: #ffffff; +} + +#resize-button { + background: #ff00de; + color: #ffffff; + border: none; + padding: 10px 20px; + cursor: pointer; + transition: 0.3s; +} + +#resize-button:hover { + background: #00ffff; + color: #0c0c0c; +} + +#output-image { + max-width: 100%; + height: auto; + margin-top: 20px; + border: 1px solid #2a2a2a; +} + +#download-link { + display: inline-block; + margin-top: 10px; + color: #00ffff; + text-decoration: none; +} + +#download-link:hover { + color: #ff00de; +} +--- END OF image-resizer.css --- + +--- START OF qr-generator.css --- +/* Styles specific to the QR Code Generator page */ +.qr-container { + background: #0f0f0f; + border: 1px solid #2a2a2a; + border-radius: 5px; + padding: 20px; + margin-top: 20px; +} + +#qr-text { + width: 100%; + padding: 10px; + margin-bottom: 10px; + background: #1a1a1a; + border: 1px solid #2a2a2a; + color: #ffffff; +} + +#generate-button { + background: #ff00de; + color: #ffffff; + border: none; + padding: 10px 20px; + cursor: pointer; + transition: 0.3s; +} + +#generate-button:hover { + background: #00ffff; + color: #0c0c0c; +} + +#qr-code { + margin-top: 20px; + text-align: center; +} + +#qr-code img { + max-width: 100%; + height: auto; +} +--- END OF qr-generator.css --- + +That covers the CSS directory. Would you like me to continue with the JavaScript (js) directory next? \ No newline at end of file From 20b93269604a8145bb89077736dc673bfe99ef11 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:13:31 -0400 Subject: [PATCH 051/113] Create Text-to-speech.js --- js/Text-to-speech.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 js/Text-to-speech.js diff --git a/js/Text-to-speech.js b/js/Text-to-speech.js new file mode 100644 index 0000000..32478ca --- /dev/null +++ b/js/Text-to-speech.js @@ -0,0 +1,38 @@ +// Initialize speech synthesis +const synth = window.speechSynthesis; + +// Get DOM elements +const textInput = document.getElementById('text-input'); +const voiceSelect = document.getElementById('voice-select'); +const speakButton = document.getElementById('speak-button'); + +// Populate voice list +function populateVoiceList() { + voices = synth.getVoices(); + voiceSelect.innerHTML = ''; + voices.forEach((voice, i) => { + const option = new Option(voice.name, i); + voiceSelect.options.add(option); + }); +} + +populateVoiceList(); +if (speechSynthesis.onvoiceschanged !== undefined) { + speechSynthesis.onvoiceschanged = populateVoiceList; +} + +// Speak function +function speak() { + if (synth.speaking) { + console.error('speechSynthesis.speaking'); + return; + } + if (textInput.value !== '') { + const utterThis = new SpeechSynthesisUtterance(textInput.value); + utterThis.voice = voices[voiceSelect.selectedIndex]; + synth.speak(utterThis); + } +} + +// Event listener +speakButton.addEventListener('click', speak); From f5e89027628ab678fb8b776f3417b31126d221a0 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:13:40 -0400 Subject: [PATCH 052/113] Rename Text-to-speech.js to text-to-speech.js --- js/{Text-to-speech.js => text-to-speech.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename js/{Text-to-speech.js => text-to-speech.js} (100%) diff --git a/js/Text-to-speech.js b/js/text-to-speech.js similarity index 100% rename from js/Text-to-speech.js rename to js/text-to-speech.js From 105a6d5474c5daccadee92ca330a75e041a7b4a6 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:15:00 -0400 Subject: [PATCH 053/113] Create text-to-speach.html --- pages/text-to-speach.html | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 pages/text-to-speach.html diff --git a/pages/text-to-speach.html b/pages/text-to-speach.html new file mode 100644 index 0000000..d5fd8d3 --- /dev/null +++ b/pages/text-to-speach.html @@ -0,0 +1,29 @@ + + + + + + Text-to-Speech Converter - Digital Services Hub + + + +
+

Text-to-Speech Converter

+ + +
+ + +
+ +
+ + + From 95a25c81d600bcd17a10670035f43e2084e6a0e3 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 00:19:52 -0400 Subject: [PATCH 054/113] Create TO-DO.md --- TO-DO.md | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 TO-DO.md diff --git a/TO-DO.md b/TO-DO.md new file mode 100644 index 0000000..376e42a --- /dev/null +++ b/TO-DO.md @@ -0,0 +1,114 @@ +# Digital Services Hub Enhancement Plan + +## 1. Text-to-Speech Converter +- **Status**: Implemented +- **Description**: Convert text input to spoken audio using browser's Web Speech API +- **Key Features**: + - Text input area + - Voice selection dropdown + - Speak button +- **Implementation Notes**: + - Used Web Speech API (SpeechSynthesis) + - Created separate HTML and JS files +- **Next Steps**: + - Consider adding options for pitch and rate adjustment + - Explore adding a pause/resume functionality + +## 2. File Format Converter +- **Status**: Planned +- **Description**: Convert files between common formats +- **Key Features**: + - File upload interface + - Format selection (input and output) + - Convert button + - Download converted file +- **Potential Formats**: + - PDF to DOCX + - DOCX to PDF + - JPG to PNG + - PNG to JPG +- **Implementation Considerations**: + - Will require server-side processing + - Need to research open-source libraries for file conversion + - Consider file size limits and security measures + +## 3. Password Generator +- **Status**: Planned +- **Description**: Generate strong, random passwords based on user criteria +- **Key Features**: + - Length selection + - Character type checkboxes (uppercase, lowercase, numbers, symbols) + - Generate button + - Copy to clipboard functionality +- **Implementation Notes**: + - Can be implemented entirely client-side with JavaScript + - Ensure cryptographically secure random number generation + +## 4. Markdown Editor +- **Status**: Planned +- **Description**: Simple interface for writing and previewing Markdown +- **Key Features**: + - Text input area for Markdown + - Live preview pane + - Basic formatting toolbar (optional) +- **Implementation Notes**: + - Use a library like Marked.js for parsing Markdown + - Implement split-screen view for input and preview + +## 5. URL Shortener +- **Status**: Planned +- **Description**: Create shortened versions of long URLs +- **Key Features**: + - URL input field + - Shorten button + - Display shortened URL with copy functionality +- **Implementation Considerations**: + - Requires backend service to store and redirect URLs + - Need to consider longevity and maintenance of shortened links + - Implement rate limiting to prevent abuse + +## 6. Pixel Art Creator +- **Status**: Planned +- **Description**: Tool for creating simple pixel art designs +- **Key Features**: + - Customizable grid size + - Color palette selection + - Drawing tools (pencil, fill, eraser) + - Export functionality (PNG) +- **Implementation Notes**: + - Can be implemented using HTML5 Canvas or SVG + - Consider adding undo/redo functionality + +## 7. Meme Generator +- **Status**: Planned +- **Description**: Create memes by adding text to images +- **Key Features**: + - Image upload or selection from templates + - Text input fields for top and bottom text + - Font and color selection + - Generate and download buttons +- **Implementation Notes**: + - Use Canvas API for image manipulation + - Consider adding text positioning and sizing options + +## 8. Unit Converter +- **Status**: Planned +- **Description**: Convert between different units of measurement +- **Key Features**: + - Category selection (length, weight, temperature, etc.) + - Input and output unit selection + - Conversion calculation +- **Implementation Notes**: + - Can be implemented client-side with JavaScript + - Ensure accurate conversion formulas for all unit types + +## Next Steps +1. Implement File Format Converter +2. Develop Password Generator +3. Create Markdown Editor interface +4. Set up URL Shortener service +5. Design Pixel Art Creator tool +6. Build Meme Generator functionality +7. Implement Unit Converter + +Remember to thoroughly test each feature before moving on to the next, and update the site's navigation and homepage to include links to new tools as they are added. From e9e7d5b67a9532c1bbd5316693131b5123cc8ebb Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:20:33 -0400 Subject: [PATCH 055/113] Update text-to-speech.js --- js/text-to-speech.js | 61 ++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/js/text-to-speech.js b/js/text-to-speech.js index 32478ca..bce5780 100644 --- a/js/text-to-speech.js +++ b/js/text-to-speech.js @@ -1,38 +1,31 @@ -// Initialize speech synthesis -const synth = window.speechSynthesis; + + + + + + Digital Services Hub + + + + +
+

Digital Services Hub

-// Get DOM elements -const textInput = document.getElementById('text-input'); -const voiceSelect = document.getElementById('voice-select'); -const speakButton = document.getElementById('speak-button'); +
+

Welcome to our Digital Services Hub!

-// Populate voice list -function populateVoiceList() { - voices = synth.getVoices(); - voiceSelect.innerHTML = ''; - voices.forEach((voice, i) => { - const option = new Option(voice.name, i); - voiceSelect.options.add(option); - }); -} +

We offer a range of free tools to help with your digital media needs:

-populateVoiceList(); -if (speechSynthesis.onvoiceschanged !== undefined) { - speechSynthesis.onvoiceschanged = populateVoiceList; -} + -// Speak function -function speak() { - if (synth.speaking) { - console.error('speechSynthesis.speaking'); - return; - } - if (textInput.value !== '') { - const utterThis = new SpeechSynthesisUtterance(textInput.value); - utterThis.voice = voices[voiceSelect.selectedIndex]; - synth.speak(utterThis); - } -} - -// Event listener -speakButton.addEventListener('click', speak); +

Choose a service above to get started, or visit our About page to learn more.

+
+
+ + From 8428d15af46b5ceab23ad4068d44f8da57ec95d0 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:23:51 -0400 Subject: [PATCH 056/113] Update styles.css --- css/styles.css | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/css/styles.css b/css/styles.css index 76dccb0..0316021 100644 --- a/css/styles.css +++ b/css/styles.css @@ -9,7 +9,6 @@ body { align-items: center; min-height: 100vh; } - .container { background-color: rgba(16, 24, 39, 0.8); padding: 2rem; @@ -18,7 +17,6 @@ body { max-width: 600px; width: 100%; } - .neon-text { font-family: 'Orbitron', sans-serif; color: #fff; @@ -26,13 +24,11 @@ body { text-align: center; margin-bottom: 2rem; } - nav { display: flex; justify-content: center; margin-bottom: 2rem; } - nav a { color: #00fff2; text-decoration: none; @@ -40,36 +36,32 @@ nav a { font-weight: bold; transition: color 0.3s ease; } - nav a:hover { color: #fff; text-shadow: 0 0 5px #00fff2; } - .content { display: flex; flex-direction: column; gap: 1rem; } - .input-group { display: flex; flex-direction: column; } - label { margin-bottom: 0.5rem; color: #00fff2; } - -input[type="file"], input[type="number"], input[type="text"], textarea { +input[type="file"], input[type="number"], input[type="text"], textarea, select { background-color: #1f2937; border: 1px solid #374151; color: #fff; padding: 0.5rem; border-radius: 5px; + width: 100%; + box-sizing: border-box; } - .tech-button { background: linear-gradient(45deg, #00a3ff, #00fff2); color: #000; @@ -84,57 +76,62 @@ input[type="file"], input[type="number"], input[type="text"], textarea { text-align: center; text-decoration: none; } - .tech-button:hover { background: linear-gradient(45deg, #00fff2, #00a3ff); box-shadow: 0 0 10px #00fff2; } - .instructions { text-align: center; color: #9ca3af; font-size: 0.9rem; } - .welcome-text { font-size: 1.2rem; text-align: center; margin-bottom: 1rem; } - .services-list { list-style-type: none; padding: 0; } - .services-list li { margin-bottom: 1rem; } - .services-list a { color: #00fff2; text-decoration: none; font-weight: bold; transition: color 0.3s ease; } - .services-list a:hover { color: #fff; text-shadow: 0 0 5px #00fff2; } - .cta-text { text-align: center; margin-top: 2rem; } - .cta-text a { color: #00fff2; text-decoration: none; font-weight: bold; } - .cta-text a:hover { color: #fff; text-shadow: 0 0 5px #00fff2; } + +/* Text-to-Speech specific styles */ +#text-input { + width: 100%; + height: 100px; + resize: vertical; +} + +#voice-select { + margin-bottom: 1rem; +} + +#speak-button { + width: 100%; +} From 1c907dd4cc8601646b7b7906bccc38c5196031ef Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:24:35 -0400 Subject: [PATCH 057/113] Create text-to-speech.css --- css/text-to-speech.css | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 css/text-to-speech.css diff --git a/css/text-to-speech.css b/css/text-to-speech.css new file mode 100644 index 0000000..5544070 --- /dev/null +++ b/css/text-to-speech.css @@ -0,0 +1,14 @@ +/* Text-to-Speech specific styles */ +#text-input { + width: 100%; + height: 100px; + resize: vertical; +} + +#voice-select { + margin-bottom: 1rem; +} + +#speak-button { + width: 100%; +} From 6a37e9e745f1a1a2ace193ad7facae0ea1c89f98 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:28:18 -0400 Subject: [PATCH 058/113] Update text-to-speach.html --- pages/text-to-speach.html | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pages/text-to-speach.html b/pages/text-to-speach.html index d5fd8d3..84ee367 100644 --- a/pages/text-to-speach.html +++ b/pages/text-to-speach.html @@ -9,14 +9,15 @@

Text-to-Speech Converter

- +
From 52d730b8bcb8b7ccaa3676c47125ba037ef8fc2d Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:28:43 -0400 Subject: [PATCH 059/113] Update color-palette.html --- pages/color-palette.html | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pages/color-palette.html b/pages/color-palette.html index 0a4ade2..8b0d43d 100644 --- a/pages/color-palette.html +++ b/pages/color-palette.html @@ -11,7 +11,15 @@

Color Palette Generator

- +
From 5bdd5f13a5e2d50be0fd51529ce11d1297d5a08e Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:29:00 -0400 Subject: [PATCH 060/113] Update image-resizer.html --- pages/image-resizer.html | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pages/image-resizer.html b/pages/image-resizer.html index 62edf76..8f3d864 100644 --- a/pages/image-resizer.html +++ b/pages/image-resizer.html @@ -13,7 +13,15 @@

Futuristic Image Resizer

- +
From c50923802d04fbd90c4bead56b1ce00581505d6d Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:29:24 -0400 Subject: [PATCH 061/113] Update about.html --- pages/about.html | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pages/about.html b/pages/about.html index d3a0dd9..5e3876b 100644 --- a/pages/about.html +++ b/pages/about.html @@ -29,7 +29,15 @@

About Digital Services Hub

- +

Welcome to the Digital Services Hub - your gateway to cutting-edge digital tools and AI-powered services!

From bcc9f8e630bad615fdee9bd59815d22044f07bb5 Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:29:40 -0400 Subject: [PATCH 062/113] Update qr-generator.html --- pages/qr-generator.html | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pages/qr-generator.html b/pages/qr-generator.html index 7c9c251..b4ba03a 100644 --- a/pages/qr-generator.html +++ b/pages/qr-generator.html @@ -12,7 +12,15 @@

QR Code Generator

- +
From 7826b39c2cc2e758c36d9061579d37d1a197210a Mon Sep 17 00:00:00 2001 From: T <154358121+TMHSDigital@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:30:21 -0400 Subject: [PATCH 063/113] Update index.html --- index.html | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/index.html b/index.html index 4665cac..385edd1 100644 --- a/index.html +++ b/index.html @@ -14,19 +14,20 @@

Digital Services Hub

-

Welcome to our Digital Services Hub!

-

We offer a range of free tools to help with your digital media needs:

- - +

Welcome to our Digital Services Hub!

-

Choose a service above to get started, or visit our About page to learn more.

-
-
+

We offer a range of free tools to help with your digital media needs:

+ + + +

Choose a service above to get started, or visit our About page to learn more.

+
From da3b9c9bab67e5a6c97a64583dfb329a4c68c768 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Tue, 24 Dec 2024 14:17:10 -0500 Subject: [PATCH 064/113] Update text-to-speech.html and js/text-to-speech.js --- css/styles.css | 56 ++++++++++++++++++- index.html | 10 ++-- js/common.js | 20 +++++-- js/text-to-speech.js | 115 ++++++++++++++++++++++++++++---------- pages/text-to-speach.html | 30 ---------- pages/text-to-speech.html | 33 +++++++++++ 6 files changed, 191 insertions(+), 73 deletions(-) delete mode 100644 pages/text-to-speach.html create mode 100644 pages/text-to-speech.html diff --git a/css/styles.css b/css/styles.css index 0316021..d041141 100644 --- a/css/styles.css +++ b/css/styles.css @@ -124,14 +124,66 @@ input[type="file"], input[type="number"], input[type="text"], textarea, select { /* Text-to-Speech specific styles */ #text-input { width: 100%; - height: 100px; + height: 150px; resize: vertical; + margin-bottom: 1rem; + font-family: 'Roboto', sans-serif; + padding: 1rem; + line-height: 1.5; } #voice-select { - margin-bottom: 1rem; + width: 100%; + margin-bottom: 1.5rem; } #speak-button { width: 100%; + position: relative; + overflow: hidden; +} + +#speak-button.speaking { + background: linear-gradient(45deg, #ff0000, #ff6b6b); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 5px #ff0000; + } + 50% { + box-shadow: 0 0 20px #ff0000; + } + 100% { + box-shadow: 0 0 5px #ff0000; + } +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .container { + margin: 1rem; + padding: 1rem; + } + + nav a { + margin: 0.5rem; + font-size: 0.9rem; + } +} + +@media (max-width: 480px) { + nav { + flex-direction: column; + align-items: center; + } + + nav a { + margin: 0.25rem; + } + + .neon-text { + font-size: 1.5rem; + } } diff --git a/index.html b/index.html index 385edd1..360df1c 100644 --- a/index.html +++ b/index.html @@ -19,11 +19,11 @@

Digital Services Hub

We offer a range of free tools to help with your digital media needs:

Choose a service above to get started, or visit our About page to learn more.

diff --git a/js/common.js b/js/common.js index c3a70ec..7f9ff6d 100644 --- a/js/common.js +++ b/js/common.js @@ -8,22 +8,32 @@ document.addEventListener('DOMContentLoaded', (event) => { { href: "pages/color-palette.html", text: "Color Palette" }, { href: "pages/ascii-art.html", text: "ASCII Art" }, { href: "pages/qr-generator.html", text: "QR Code" }, + { href: "pages/text-to-speech.html", text: "Text-to-Speech" }, { href: "pages/about.html", text: "About" } ]; + // Helper function to normalize paths + const normalizePath = (path) => path.replace(/\\/g, '/').toLowerCase(); + + // Get the current page name for active state + const currentPage = normalizePath(currentPath).split('/').pop(); + const navHTML = navItems.map(item => { let href = item.href; + const isCurrentPage = normalizePath(href).endsWith(currentPage); + + // Adjust paths based on current location if (currentPath.includes('/pages/')) { href = href.replace('pages/', ''); if (item.href === 'index.html') { href = '../' + href; } - } else if (currentPath.endsWith('/') || currentPath.endsWith('index.html')) { - if (item.href !== 'index.html') { - href = 'pages/' + href.replace('pages/', ''); - } } - return `${item.text}`; + + // Add active class if current page + const activeClass = isCurrentPage ? ' class="active"' : ''; + + return `${item.text}`; }).join(''); nav.innerHTML = navHTML; diff --git a/js/text-to-speech.js b/js/text-to-speech.js index bce5780..9752563 100644 --- a/js/text-to-speech.js +++ b/js/text-to-speech.js @@ -1,31 +1,84 @@ - - - - - - Digital Services Hub - - - - -
-

Digital Services Hub

- -
-

Welcome to our Digital Services Hub!

- -

We offer a range of free tools to help with your digital media needs:

- - - -

Choose a service above to get started, or visit our About page to learn more.

-
-
- - +document.addEventListener('DOMContentLoaded', () => { + const textInput = document.getElementById('text-input'); + const voiceSelect = document.getElementById('voice-select'); + const speakButton = document.getElementById('speak-button'); + let voices = []; + let synthesis = window.speechSynthesis; + + // Function to populate voice list + function populateVoiceList() { + voices = synthesis.getVoices(); + voiceSelect.innerHTML = ''; + + voices.forEach((voice, index) => { + const option = document.createElement('option'); + option.textContent = `${voice.name} (${voice.lang})`; + option.value = index; + voiceSelect.appendChild(option); + }); + } + + // Initialize voices + populateVoiceList(); + if (speechSynthesis.onvoiceschanged !== undefined) { + speechSynthesis.onvoiceschanged = populateVoiceList; + } + + // Speak function + function speak() { + if (synthesis.speaking) { + synthesis.cancel(); + } + + const text = textInput.value.trim(); + if (!text) { + alert('Please enter some text to speak.'); + return; + } + + const utterance = new SpeechSynthesisUtterance(text); + const selectedVoice = voices[voiceSelect.value]; + if (selectedVoice) { + utterance.voice = selectedVoice; + } + + // Add event handlers + utterance.onstart = () => { + speakButton.textContent = 'Stop'; + speakButton.classList.add('speaking'); + }; + + utterance.onend = () => { + speakButton.textContent = 'Speak'; + speakButton.classList.remove('speaking'); + }; + + utterance.onerror = (event) => { + console.error('SpeechSynthesis Error:', event); + speakButton.textContent = 'Speak'; + speakButton.classList.remove('speaking'); + }; + + synthesis.speak(utterance); + } + + // Event listeners + speakButton.addEventListener('click', () => { + if (synthesis.speaking) { + synthesis.cancel(); + speakButton.textContent = 'Speak'; + speakButton.classList.remove('speaking'); + } else { + speak(); + } + }); + + // Add keyboard shortcuts + textInput.addEventListener('keydown', (e) => { + // Ctrl/Cmd + Enter to speak + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault(); + speak(); + } + }); +}); diff --git a/pages/text-to-speach.html b/pages/text-to-speach.html deleted file mode 100644 index 84ee367..0000000 --- a/pages/text-to-speach.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - Text-to-Speech Converter - Digital Services Hub - - - -
-

Text-to-Speech Converter

- - -
- - -
- -
- - - diff --git a/pages/text-to-speech.html b/pages/text-to-speech.html new file mode 100644 index 0000000..c8b007a --- /dev/null +++ b/pages/text-to-speech.html @@ -0,0 +1,33 @@ + + + + + + Text-to-Speech Converter - Digital Services Hub + + + + +
+

Text-to-Speech Converter

+ + + +
+
+ + +
+ +
+ + +
+ + +
+
+ + + + From ce9d22f8dcd80ca6bfc8630ce7975c5f9401727f Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Tue, 24 Dec 2024 14:17:24 -0500 Subject: [PATCH 065/113] Add .vscode/settings.json to ignore main branch in GitHub pull requests --- .vscode/settings.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b242572 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "main" + ] +} \ No newline at end of file From 940d1ff55a4795e4a7c131f9d98ef9ff24a64d5a Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Tue, 24 Dec 2024 14:57:26 -0500 Subject: [PATCH 066/113] NEW STRUCTURE - VERSION 1 --- .gitignore | 36 ++ CHANGELOG.md | 36 ++ CONTRIBUTING.md | 57 ++++ LICENSE | 2 +- css/ascii-art.css | 26 -- css/color-palette.css | 25 -- css/components/about.css | 150 +++++++++ css/components/ascii-art.css | 124 +++++++ css/components/buttons.css | 29 ++ css/components/color-palette.css | 222 ++++++++++++ css/components/image-resizer.css | 123 +++++++ css/components/inputs.css | 43 +++ css/components/navigation.css | 26 ++ css/components/progress.css | 29 ++ css/components/qr-code.css | 91 +++++ css/image.resizer.css | 11 - css/qr-generator.css | 43 --- css/styles.css | 223 +++++-------- css/text-to-speech.css | 14 - css/themes/theme-variables.css | 26 ++ css/utils/animations.css | 15 + docs/FUTURE-FEATURES.md | 242 ++++++++++---- docs/PROJECT-DESCRIPTION.md | 225 ++++++++----- docs/PROJECT-OVERVIEW.md | 557 +++++++++++++------------------ docs/README.md | 259 +++++++------- index.html | 44 ++- js/ascii-art.js | 18 - js/color-palette.js | 47 --- js/common.js | 2 +- js/features/about.js | 166 +++++++++ js/features/ascii-art.js | 224 +++++++++++++ js/features/base-tool.js | 230 +++++++++++++ js/features/color-palette.js | 321 ++++++++++++++++++ js/features/image-resizer.js | 246 ++++++++++++++ js/features/qr-code.js | 210 ++++++++++++ js/features/text-to-speech.js | 255 ++++++++++++++ js/image-resizer.js | 46 --- js/qr-generator.js | 48 --- js/text-to-speech.js | 84 ----- js/utils/constants.js | 98 ++++++ js/utils/helpers.js | 217 ++++++++++++ js/utils/ui.js | 259 ++++++++++++++ js/utils/validation.js | 178 ++++++++++ pages/about.html | 130 ++++---- pages/ascii-art.html | 78 ++++- pages/color-palette.html | 57 ++-- pages/image-resizer.html | 83 +++-- pages/qr-code.html | 99 ++++++ pages/qr-generator.html | 76 ----- pages/text-to-speech.html | 68 +++- 50 files changed, 4600 insertions(+), 1318 deletions(-) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md delete mode 100644 css/ascii-art.css delete mode 100644 css/color-palette.css create mode 100644 css/components/about.css create mode 100644 css/components/ascii-art.css create mode 100644 css/components/buttons.css create mode 100644 css/components/color-palette.css create mode 100644 css/components/image-resizer.css create mode 100644 css/components/inputs.css create mode 100644 css/components/navigation.css create mode 100644 css/components/progress.css create mode 100644 css/components/qr-code.css delete mode 100644 css/image.resizer.css delete mode 100644 css/qr-generator.css delete mode 100644 css/text-to-speech.css create mode 100644 css/themes/theme-variables.css create mode 100644 css/utils/animations.css delete mode 100644 js/ascii-art.js delete mode 100644 js/color-palette.js create mode 100644 js/features/about.js create mode 100644 js/features/ascii-art.js create mode 100644 js/features/base-tool.js create mode 100644 js/features/color-palette.js create mode 100644 js/features/image-resizer.js create mode 100644 js/features/qr-code.js create mode 100644 js/features/text-to-speech.js delete mode 100644 js/image-resizer.js delete mode 100644 js/qr-generator.js delete mode 100644 js/text-to-speech.js create mode 100644 js/utils/constants.js create mode 100644 js/utils/helpers.js create mode 100644 js/utils/ui.js create mode 100644 js/utils/validation.js create mode 100644 pages/qr-code.html delete mode 100644 pages/qr-generator.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4b7f93e --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules/ +package-lock.json +yarn.lock + +# Environment +.env +.env.local +.env.*.local + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store +Thumbs.db + +# Build output +dist/ +build/ +out/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ + +# Temporary files +*.tmp +*.temp +.cache/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7fbf6cd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,36 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2024-01-01 + +### Added +- Initial release of Digital Services Hub +- Text to Speech tool with multiple voice options +- Image Resizer with aspect ratio preservation +- Color Palette generator with harmony options +- ASCII Art converter with customization +- QR Code generator with styling options +- Base Tool class for shared functionality +- Utility modules for common operations +- Theme support (light/dark) +- Keyboard shortcuts +- Responsive design +- Accessibility features + +### Security +- Input sanitization +- XSS prevention +- CSRF protection +- Content Security Policy + +### Documentation +- README.md with feature documentation +- PROJECT-DESCRIPTION.md with technical details +- PROJECT-OVERVIEW.md with architecture details +- FUTURE-FEATURES.md with roadmap +- Code documentation with JSDoc +- Contributing guidelines \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cd581d7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing to Digital Services Hub + +We love your input! We want to make contributing to Digital Services Hub as easy and transparent as possible, whether it's: + +- Reporting a bug +- Discussing the current state of the code +- Submitting a fix +- Proposing new features +- Becoming a maintainer + +## Development Process + +We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. + +1. Fork the repo and create your branch from `main`. +2. If you've added code that should be tested, add tests. +3. If you've changed APIs, update the documentation. +4. Ensure the test suite passes. +5. Make sure your code lints. +6. Issue that pull request! + +## Pull Request Process + +1. Update the README.md with details of changes to the interface, if applicable. +2. Update the CHANGELOG.md with a note describing your changes. +3. The PR will be merged once you have the sign-off of at least one maintainer. + +## Any contributions you make will be under the MIT Software License + +In short, when you submit code changes, your submissions are understood to be under the same [MIT License](LICENSE) that covers the project. Feel free to contact the maintainers if that's a concern. + +## Report bugs using GitHub's [issue tracker] + +We use GitHub issues to track public bugs. Report a bug by [opening a new issue](). + +## Write bug reports with detail, background, and sample code + +**Great Bug Reports** tend to have: + +- A quick summary and/or background +- Steps to reproduce + - Be specific! + - Give sample code if you can. +- What you expected would happen +- What actually happens +- Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) + +## Code Style + +* Use 4 spaces for indentation +* Use camelCase for variable and function names +* Add JSDoc comments for functions +* Follow ESLint rules + +## License + +By contributing, you agree that your contributions will be licensed under its MIT License. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 7d22a6d..0665dd2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 T +Copyright (c) 2024 Digital Services Hub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/css/ascii-art.css b/css/ascii-art.css deleted file mode 100644 index 72780b4..0000000 --- a/css/ascii-art.css +++ /dev/null @@ -1,26 +0,0 @@ -.ascii-output { - background-color: #1a1a1a; - border: 1px solid #00fff2; - border-radius: 5px; - padding: 1rem; - margin-top: 1rem; - font-family: monospace; - white-space: pre-wrap; - overflow-x: auto; -} - -#ascii-output { - color: #00fff2; - font-size: 0.8rem; - line-height: 1; -} - -#text-input { - width: 100%; - background-color: #1f2937; - border: 1px solid #374151; - color: #fff; - padding: 0.5rem; - border-radius: 5px; - resize: vertical; -} diff --git a/css/color-palette.css b/css/color-palette.css deleted file mode 100644 index a85ab27..0000000 --- a/css/color-palette.css +++ /dev/null @@ -1,25 +0,0 @@ -#image-container img { - max-width: 100%; - height: auto; - margin-top: 1rem; - border-radius: 5px; -} - -#palette-container { - display: flex; - justify-content: space-between; - margin-top: 1rem; -} - -.color-swatch { - width: 50px; - height: 50px; - border-radius: 50%; -} - -.color-info { - text-align: center; - font-size: 0.8rem; - margin-top: 0.5rem; - color: #fff; -} diff --git a/css/components/about.css b/css/components/about.css new file mode 100644 index 0000000..517cfeb --- /dev/null +++ b/css/components/about.css @@ -0,0 +1,150 @@ +.about-content { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.about-section { + text-align: center; + max-width: 800px; + margin: 0 auto; +} + +.about-section p { + font-size: 1.1rem; + line-height: 1.6; + color: var(--text-color); +} + +.features-section { + padding: 2rem 0; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-top: 1.5rem; +} + +.feature-card { + background-color: var(--container-bg); + border: 2px solid var(--accent-color); + border-radius: 10px; + padding: 1.5rem; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.feature-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(45deg, var(--accent-color-transparent), transparent); + opacity: 0; + transition: opacity 0.3s ease; +} + +.feature-card:hover::before, +.feature-card.hover::before { + opacity: 0.1; +} + +.feature-card:hover, +.feature-card.hover { + transform: translateY(-5px); + box-shadow: 0 5px 15px var(--shadow-color); +} + +.feature-icon { + font-size: 2.5rem; + margin-bottom: 1rem; +} + +.feature-card h3 { + color: var(--accent-color); + margin: 0.5rem 0; +} + +.feature-card p { + font-size: 0.9rem; + color: var(--text-color); + margin: 0; +} + +.tech-section { + background-color: var(--container-bg); + border-radius: 10px; + padding: 2rem; + margin-top: 2rem; +} + +.tech-list { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + list-style: none; + padding: 0; + margin: 1rem 0; +} + +.tech-list li { + background-color: var(--input-bg); + padding: 0.8rem; + border-radius: 5px; + text-align: center; + font-family: 'Orbitron', sans-serif; + font-size: 0.9rem; + color: var(--accent-color); +} + +.info-section { + text-align: center; + padding: 2rem; + background-color: var(--container-bg); + border-radius: 10px; +} + +.version-info { + font-family: 'Orbitron', sans-serif; + color: var(--accent-color); + margin: 1rem 0; +} + +.version-info p { + margin: 0.5rem 0; +} + +@media (max-width: 768px) { + .feature-grid { + grid-template-columns: 1fr; + } + + .tech-list { + grid-template-columns: 1fr 1fr; + } + + .about-section p { + font-size: 1rem; + } +} + +@media (max-width: 480px) { + .tech-list { + grid-template-columns: 1fr; + } + + .feature-icon { + font-size: 2rem; + } + + .feature-card { + padding: 1rem; + } +} \ No newline at end of file diff --git a/css/components/ascii-art.css b/css/components/ascii-art.css new file mode 100644 index 0000000..997a6bd --- /dev/null +++ b/css/components/ascii-art.css @@ -0,0 +1,124 @@ +.ascii-controls { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.drop-zone { + border: 2px dashed var(--accent-color); + border-radius: 10px; + padding: 2rem; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; + position: relative; +} + +.drop-zone:hover, .drop-zone.drag-over { + background-color: var(--accent-color-transparent); + border-style: solid; +} + +.drop-zone-text { + pointer-events: none; +} + +.drop-zone-text p { + margin: 0.5rem 0; +} + +.drop-zone-text .small { + font-size: 0.8rem; + opacity: 0.7; +} + +.file-input { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + cursor: pointer; +} + +.preview-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 100px; + background-color: var(--input-bg); + border-radius: 10px; + overflow: hidden; +} + +.preview-container canvas { + max-width: 100%; + height: auto; +} + +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.ascii-output { + font-family: monospace; + white-space: pre; + overflow-x: auto; + background-color: var(--input-bg); + padding: 1rem; + border-radius: 10px; + margin: 0; + line-height: 1; + font-size: 0.7rem; +} + +.output-container { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.output-actions { + display: flex; + gap: 1rem; + justify-content: center; +} + +@media (max-width: 768px) { + .settings-grid { + grid-template-columns: 1fr; + } + + .output-actions { + flex-direction: column; + } + + .ascii-output { + font-size: 0.6rem; + } +} + +@media (max-width: 480px) { + .drop-zone { + padding: 1rem; + } + + .ascii-output { + font-size: 0.5rem; + } +} \ No newline at end of file diff --git a/css/components/buttons.css b/css/components/buttons.css new file mode 100644 index 0000000..2fd9d62 --- /dev/null +++ b/css/components/buttons.css @@ -0,0 +1,29 @@ +.tech-button { + background: linear-gradient(45deg, var(--button-gradient-1), var(--button-gradient-2)); + color: var(--bg-color); + border: none; + padding: 10px 20px; + font-size: 16px; + font-weight: bold; + text-transform: uppercase; + border-radius: 5px; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + text-decoration: none; +} + +.tech-button:hover { + background: linear-gradient(45deg, var(--button-gradient-2), var(--button-gradient-1)); + box-shadow: 0 0 10px var(--shadow-color); +} + +.tech-button.small { + padding: 5px 10px; + font-size: 14px; +} + +.tech-button.speaking { + background: linear-gradient(45deg, var(--error-color), #ff6b6b); + animation: pulse 2s infinite; +} \ No newline at end of file diff --git a/css/components/color-palette.css b/css/components/color-palette.css new file mode 100644 index 0000000..841ba55 --- /dev/null +++ b/css/components/color-palette.css @@ -0,0 +1,222 @@ +.palette-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + margin: 1rem 0; +} + +.color-box { + aspect-ratio: 1; + border-radius: 10px; + position: relative; + cursor: pointer; + transition: all 0.3s ease; + overflow: hidden; +} + +.color-box:hover { + transform: scale(1.05); + box-shadow: 0 0 15px var(--shadow-color); +} + +.color-info { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background: rgba(0, 0, 0, 0.7); + color: white; + padding: 0.5rem; + font-family: monospace; + font-size: 0.9rem; + opacity: 0; + transition: opacity 0.3s ease; + display: flex; + justify-content: space-between; + align-items: center; +} + +.color-box:hover .color-info { + opacity: 1; +} + +.color-info .copy-icon { + cursor: pointer; + padding: 0.2rem; + border-radius: 3px; + transition: all 0.3s ease; +} + +.color-info .copy-icon:hover { + background: rgba(255, 255, 255, 0.2); +} + +.palette-controls { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin-bottom: 1rem; +} + +.color-picker-container { + position: relative; + width: 100%; + aspect-ratio: 1; + border-radius: 10px; + overflow: hidden; + border: 2px solid var(--accent-color); +} + +.color-picker-container canvas { + width: 100%; + height: 100%; + cursor: crosshair; +} + +.selected-colors { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 1rem 0; +} + +.selected-color { + width: 40px; + height: 40px; + border-radius: 5px; + position: relative; + cursor: pointer; +} + +.selected-color::after { + content: '×'; + position: absolute; + top: -8px; + right: -8px; + background: var(--error-color); + color: white; + width: 20px; + height: 20px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + opacity: 0; + transition: opacity 0.3s ease; +} + +.selected-color:hover::after { + opacity: 1; +} + +.palette-actions { + display: flex; + gap: 1rem; + flex-wrap: wrap; + justify-content: center; + margin: 1rem 0; +} + +.harmony-options { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 1rem 0; +} + +.harmony-option { + flex: 1; + min-width: 100px; + text-align: center; +} + +.export-options { + display: flex; + flex-wrap: wrap; + gap: 1rem; + margin: 1rem 0; +} + +.saved-palettes { + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid var(--input-border); +} + +.saved-palette-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 1rem; + margin-top: 1rem; +} + +.saved-palette { + border-radius: 10px; + overflow: hidden; + cursor: pointer; + transition: all 0.3s ease; +} + +.saved-palette:hover { + transform: scale(1.05); + box-shadow: 0 0 15px var(--shadow-color); +} + +.saved-palette-colors { + display: flex; + height: 100px; +} + +.saved-palette-color { + flex: 1; +} + +.saved-palette-info { + padding: 0.5rem; + background: var(--container-bg); + font-size: 0.9rem; +} + +.notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 1rem; + background: var(--accent-color); + color: var(--bg-color); + border-radius: 5px; + opacity: 0; + transform: translateY(20px); + transition: all 0.3s ease; +} + +.notification.show { + opacity: 1; + transform: translateY(0); +} + +@media (max-width: 768px) { + .palette-container { + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + } + + .palette-controls { + grid-template-columns: 1fr; + } +} + +@media (max-width: 480px) { + .color-info { + font-size: 0.8rem; + padding: 0.3rem; + } + + .harmony-options { + flex-direction: column; + } + + .export-options { + flex-direction: column; + } +} \ No newline at end of file diff --git a/css/components/image-resizer.css b/css/components/image-resizer.css new file mode 100644 index 0000000..60fdfe3 --- /dev/null +++ b/css/components/image-resizer.css @@ -0,0 +1,123 @@ +.image-container { + width: 100%; + min-height: 200px; + border: 2px dashed var(--accent-color); + border-radius: 10px; + margin: 1rem 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 1rem; + transition: all 0.3s ease; + position: relative; +} + +.image-container.drag-over { + background-color: var(--accent-color); + opacity: 0.7; +} + +.image-container img { + max-width: 100%; + max-height: 400px; + object-fit: contain; + margin: 1rem 0; +} + +.image-controls { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + width: 100%; + margin-top: 1rem; +} + +.dimension-controls { + display: flex; + gap: 1rem; + align-items: center; +} + +.dimension-controls input[type="number"] { + width: 80px; +} + +.aspect-ratio-lock { + color: var(--accent-color); + cursor: pointer; + transition: all 0.3s ease; +} + +.aspect-ratio-lock.locked { + color: var(--error-color); +} + +.format-controls { + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; + flex-wrap: wrap; +} + +.quality-slider { + width: 100%; + margin: 1rem 0; +} + +.preview-container { + position: relative; + width: 100%; + margin: 1rem 0; +} + +.preview-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; +} + +.preview-container:hover .preview-overlay { + opacity: 1; +} + +.file-info { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + margin-top: 0.5rem; + font-size: 0.9rem; + color: var(--accent-color); +} + +.drop-message { + font-size: 1.2rem; + color: var(--accent-color); + text-align: center; + margin: 1rem 0; +} + +@media (max-width: 480px) { + .dimension-controls { + flex-direction: column; + align-items: stretch; + } + + .dimension-controls input[type="number"] { + width: 100%; + } + + .format-controls { + flex-direction: column; + } +} \ No newline at end of file diff --git a/css/components/inputs.css b/css/components/inputs.css new file mode 100644 index 0000000..feac525 --- /dev/null +++ b/css/components/inputs.css @@ -0,0 +1,43 @@ +.input-group { + display: flex; + flex-direction: column; +} + +label { + margin-bottom: 0.5rem; + color: var(--accent-color); +} + +input, textarea, select { + background-color: var(--input-bg); + border: 1px solid var(--input-border); + color: var(--text-color); + padding: 0.5rem; + border-radius: 5px; + width: 100%; + box-sizing: border-box; + font-family: inherit; +} + +input[type="range"] { + height: 5px; + border-radius: 5px; + background: var(--input-bg); + outline: none; + -webkit-appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 15px; + height: 15px; + border-radius: 50%; + background: var(--accent-color); + cursor: pointer; +} + +#text-input { + height: 150px; + resize: vertical; + line-height: 1.5; +} \ No newline at end of file diff --git a/css/components/navigation.css b/css/components/navigation.css new file mode 100644 index 0000000..5d124ab --- /dev/null +++ b/css/components/navigation.css @@ -0,0 +1,26 @@ +nav { + display: flex; + justify-content: center; + margin-bottom: 2rem; + flex-wrap: wrap; +} + +nav a { + color: var(--accent-color); + text-decoration: none; + margin: 0.5rem 1rem; + font-weight: bold; + transition: all 0.3s ease; +} + +nav a:hover, nav a.active { + color: var(--text-color); + text-shadow: 0 0 5px var(--accent-color); +} + +@media (max-width: 480px) { + nav { + flex-direction: column; + align-items: center; + } +} \ No newline at end of file diff --git a/css/components/progress.css b/css/components/progress.css new file mode 100644 index 0000000..b8ba993 --- /dev/null +++ b/css/components/progress.css @@ -0,0 +1,29 @@ +.progress-container { + margin: 1rem 0; +} + +.progress-container.hidden { + display: none; +} + +.progress-bar { + width: 100%; + height: 10px; + background-color: var(--input-bg); + border-radius: 5px; + overflow: hidden; +} + +.progress { + width: 0%; + height: 100%; + background: linear-gradient(45deg, var(--button-gradient-1), var(--button-gradient-2)); + transition: width 0.3s ease; +} + +.progress-text { + display: block; + text-align: center; + margin-top: 0.5rem; + font-size: 0.9rem; +} \ No newline at end of file diff --git a/css/components/qr-code.css b/css/components/qr-code.css new file mode 100644 index 0000000..6ea1ad2 --- /dev/null +++ b/css/components/qr-code.css @@ -0,0 +1,91 @@ +.qr-controls { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; +} + +.qr-output { + display: flex; + justify-content: center; + align-items: center; + min-height: 256px; + background-color: var(--input-bg); + border-radius: 10px; + padding: 1rem; + overflow: hidden; +} + +.qr-output svg, .qr-output canvas { + max-width: 100%; + height: auto; +} + +.output-container { + display: flex; + flex-direction: column; + gap: 1rem; + align-items: center; +} + +.output-actions { + display: flex; + gap: 1rem; + justify-content: center; +} + +/* Color input styling */ +input[type="color"] { + -webkit-appearance: none; + width: 100%; + height: 40px; + border: none; + border-radius: 5px; + padding: 0; + cursor: pointer; +} + +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 5px; +} + +input[type="color"]::-moz-color-swatch { + border: none; + border-radius: 5px; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .settings-grid { + grid-template-columns: 1fr; + } + + .output-actions { + flex-direction: column; + } + + .qr-output { + min-height: 200px; + } +} + +@media (max-width: 480px) { + input[type="color"] { + height: 32px; + } + + .qr-output { + min-height: 150px; + padding: 0.5rem; + } +} \ No newline at end of file diff --git a/css/image.resizer.css b/css/image.resizer.css deleted file mode 100644 index 4eb9685..0000000 --- a/css/image.resizer.css +++ /dev/null @@ -1,11 +0,0 @@ -#image-container { - max-height: 300px; - overflow: hidden; - margin-bottom: 1rem; - border-radius: 5px; -} - -#image-container img { - max-width: 100%; - height: auto; -} diff --git a/css/qr-generator.css b/css/qr-generator.css deleted file mode 100644 index 94b273a..0000000 --- a/css/qr-generator.css +++ /dev/null @@ -1,43 +0,0 @@ -#qr-input, #qr-size, #qr-correction { - width: 100%; - background-color: #1f2937; - border: 1px solid #374151; - color: #fff; - padding: 0.5rem; - border-radius: 5px; -} - -#qr-output { - display: flex; - justify-content: center; - margin-top: 1rem; - background-color: #fff; - padding: 1rem; - border-radius: 5px; -} - -#download-link { - display: block; - margin-top: 1rem; - text-align: center; -} - -.options-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 1rem; - margin-bottom: 1rem; -} - -#qr-color, #qr-bg-color { - width: 100%; - height: 40px; - padding: 0; - border: none; - cursor: pointer; -} - -#qr-rounded { - width: 20px; - height: 20px; -} diff --git a/css/styles.css b/css/styles.css index d041141..e2aeca9 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,189 +1,140 @@ +/* Theme variables */ +@import 'themes/theme-variables.css'; + +/* Components */ +@import 'components/buttons.css'; +@import 'components/inputs.css'; +@import 'components/progress.css'; +@import 'components/navigation.css'; +@import 'components/image-resizer.css'; +@import 'components/color-palette.css'; +@import 'components/ascii-art.css'; +@import 'components/qr-code.css'; +@import 'components/about.css'; + +/* Utils */ +@import 'utils/animations.css'; + +/* Base styles */ body { font-family: 'Roboto', sans-serif; - background-color: #000; - color: #fff; + background-color: var(--bg-color); + color: var(--text-color); margin: 0; padding: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; + transition: all 0.3s ease; } + .container { - background-color: rgba(16, 24, 39, 0.8); + background-color: var(--container-bg); padding: 2rem; border-radius: 10px; - box-shadow: 0 0 20px #00fff2; - max-width: 600px; + box-shadow: 0 0 20px var(--shadow-color); + max-width: 800px; width: 100%; + margin: 1rem; + transition: all 0.3s ease; + position: relative; } -.neon-text { - font-family: 'Orbitron', sans-serif; - color: #fff; - text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #00fff2, 0 0 35px #00fff2, 0 0 40px #00fff2; - text-align: center; - margin-bottom: 2rem; -} -nav { - display: flex; - justify-content: center; - margin-bottom: 2rem; -} -nav a { - color: #00fff2; - text-decoration: none; - margin: 0 1rem; - font-weight: bold; - transition: color 0.3s ease; -} -nav a:hover { - color: #fff; - text-shadow: 0 0 5px #00fff2; -} + .content { display: flex; flex-direction: column; gap: 1rem; } -.input-group { - display: flex; - flex-direction: column; + +/* Text-to-Speech specific layouts */ +.controls-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin: 1rem 0; } -label { + +.text-controls { + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 0.5rem; - color: #00fff2; -} -input[type="file"], input[type="number"], input[type="text"], textarea, select { - background-color: #1f2937; - border: 1px solid #374151; - color: #fff; - padding: 0.5rem; - border-radius: 5px; - width: 100%; - box-sizing: border-box; -} -.tech-button { - background: linear-gradient(45deg, #00a3ff, #00fff2); - color: #000; - border: none; - padding: 10px 20px; - font-size: 16px; - font-weight: bold; - text-transform: uppercase; - border-radius: 5px; - cursor: pointer; - transition: all 0.3s ease; - text-align: center; - text-decoration: none; -} -.tech-button:hover { - background: linear-gradient(45deg, #00fff2, #00a3ff); - box-shadow: 0 0 10px #00fff2; -} -.instructions { - text-align: center; - color: #9ca3af; - font-size: 0.9rem; -} -.welcome-text { - font-size: 1.2rem; - text-align: center; - margin-bottom: 1rem; } -.services-list { - list-style-type: none; - padding: 0; -} -.services-list li { - margin-bottom: 1rem; -} -.services-list a { - color: #00fff2; - text-decoration: none; - font-weight: bold; - transition: color 0.3s ease; -} -.services-list a:hover { - color: #fff; - text-shadow: 0 0 5px #00fff2; + +.text-actions { + display: flex; + gap: 0.5rem; } -.cta-text { - text-align: center; + +.history-section, .templates-section { margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid var(--input-border); } -.cta-text a { - color: #00fff2; - text-decoration: none; - font-weight: bold; -} -.cta-text a:hover { - color: #fff; - text-shadow: 0 0 5px #00fff2; + +.history-list { + max-height: 200px; + overflow-y: auto; + margin-top: 1rem; } -/* Text-to-Speech specific styles */ -#text-input { - width: 100%; - height: 150px; - resize: vertical; - margin-bottom: 1rem; - font-family: 'Roboto', sans-serif; - padding: 1rem; - line-height: 1.5; +.history-item { + padding: 0.5rem; + margin-bottom: 0.5rem; + background-color: var(--input-bg); + border-radius: 5px; + cursor: pointer; + transition: all 0.3s ease; } -#voice-select { - width: 100%; - margin-bottom: 1.5rem; +.history-item:hover { + background-color: var(--accent-color); + color: var(--bg-color); } -#speak-button { - width: 100%; - position: relative; - overflow: hidden; +.template-buttons { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 1rem; } -#speak-button.speaking { - background: linear-gradient(45deg, #ff0000, #ff6b6b); - animation: pulse 2s infinite; +#language-detect { + margin-top: 0.5rem; + font-size: 0.9rem; + color: var(--accent-color); } -@keyframes pulse { - 0% { - box-shadow: 0 0 5px #ff0000; - } - 50% { - box-shadow: 0 0 20px #ff0000; - } - 100% { - box-shadow: 0 0 5px #ff0000; - } +.theme-toggle { + position: absolute; + top: 1rem; + right: 1rem; } /* Responsive adjustments */ @media (max-width: 768px) { .container { - margin: 1rem; + margin: 0.5rem; padding: 1rem; } - nav a { - margin: 0.5rem; - font-size: 0.9rem; + .controls-grid { + grid-template-columns: 1fr; } } @media (max-width: 480px) { - nav { - flex-direction: column; - align-items: center; + .neon-text { + font-size: 1.5rem; } - nav a { - margin: 0.25rem; + .text-controls { + flex-direction: column; + align-items: stretch; } - .neon-text { - font-size: 1.5rem; + .text-actions { + margin-top: 0.5rem; } } diff --git a/css/text-to-speech.css b/css/text-to-speech.css deleted file mode 100644 index 5544070..0000000 --- a/css/text-to-speech.css +++ /dev/null @@ -1,14 +0,0 @@ -/* Text-to-Speech specific styles */ -#text-input { - width: 100%; - height: 100px; - resize: vertical; -} - -#voice-select { - margin-bottom: 1rem; -} - -#speak-button { - width: 100%; -} diff --git a/css/themes/theme-variables.css b/css/themes/theme-variables.css new file mode 100644 index 0000000..6533158 --- /dev/null +++ b/css/themes/theme-variables.css @@ -0,0 +1,26 @@ +:root { + /* Dark theme (default) */ + --bg-color: #000; + --container-bg: rgba(16, 24, 39, 0.8); + --text-color: #fff; + --accent-color: #00fff2; + --input-bg: #1f2937; + --input-border: #374151; + --button-gradient-1: #00a3ff; + --button-gradient-2: #00fff2; + --shadow-color: #00fff2; + --error-color: #ff0000; +} + +[data-theme="light"] { + --bg-color: #f0f2f5; + --container-bg: rgba(255, 255, 255, 0.9); + --text-color: #1a1a1a; + --accent-color: #0066cc; + --input-bg: #ffffff; + --input-border: #cccccc; + --button-gradient-1: #0066cc; + --button-gradient-2: #0099ff; + --shadow-color: #0099ff; + --error-color: #cc0000; +} \ No newline at end of file diff --git a/css/utils/animations.css b/css/utils/animations.css new file mode 100644 index 0000000..11da4f0 --- /dev/null +++ b/css/utils/animations.css @@ -0,0 +1,15 @@ +@keyframes pulse { + 0%, 100% { box-shadow: 0 0 5px var(--error-color); } + 50% { box-shadow: 0 0 20px var(--error-color); } +} + +.neon-text { + font-family: 'Orbitron', sans-serif; + color: var(--text-color); + text-shadow: 0 0 5px var(--text-color), + 0 0 10px var(--text-color), + 0 0 15px var(--accent-color), + 0 0 20px var(--accent-color); + text-align: center; + margin-bottom: 2rem; +} \ No newline at end of file diff --git a/docs/FUTURE-FEATURES.md b/docs/FUTURE-FEATURES.md index 3f9a905..1f70cf7 100644 --- a/docs/FUTURE-FEATURES.md +++ b/docs/FUTURE-FEATURES.md @@ -1,75 +1,171 @@ -# 🚀 Future Features Roadmap - -Welcome to the exciting frontier of Digital Services Hub! Here's a glimpse into the cutting-edge innovations we're exploring. While we can't promise specific release dates, we're thrilled about the possibilities these features could bring to our platform. - -![Experimental](https://img.shields.io/badge/Status-Experimental-yellow?style=for-the-badge) -![Innovation](https://img.shields.io/badge/Innovation-Ongoing-blue?style=for-the-badge) -![AI Integration](https://img.shields.io/badge/AI-Integration-red?style=for-the-badge) - -## 🧠 AI-Powered Enhancements - -> ### Neural Text Alchemy -> _Transform your words with the power of advanced language models._ - -> ### Sentiment Spectrum Analyzer -> _Uncover the hidden emotions behind any text._ - -> ### Vocal Canvas -> _Paint with your voice, create with your words._ - -## 🎨 Visual Wizardry - -> ### Pixel Perfect Enhancer -> _Breathe new life into your images with AI-driven enhancements._ - -> ### Reality Architect -> _Craft virtual worlds limited only by your imagination._ - -> ### StyleFusion Generator -> _Blend artistic styles to create unique visual masterpieces._ - -## 💻 Code Sorcery - -> ### Syntax Whisperer -> _Decode the mysteries of complex code with AI assistance._ - -> ### Logic Loom -> _Weave intricate algorithms with intuitive visual tools._ - -## 🔐 Digital Fortress - -> ### Quantum Shield -> _Explore next-gen security concepts for your digital assets._ - -> ### Chameleon Cloak -> _Adaptable privacy features for the privacy-conscious user._ - -## 🌐 Web Alchemy - -> ### Responsive Shapeshifter -> _Websites that mold themselves to any device, any screen._ - -> ### SEO Sage -> _Uncover the secrets of search engine visibility._ - -## 🤖 Automagic Assistance - -> ### Task Tessellation -> _Seamlessly interweave your digital tasks for maximum efficiency._ - -> ### Insight Oracle -> _Gain profound insights from your data through advanced analytics._ - -## 🌟 Personalization Paradigm - -> ### Digital DNA -> _Services that adapt to your unique digital fingerprint._ - -> ### Mood Maestro -> _Experience interfaces that resonate with your emotional state._ - ---- - -These potential features represent the bleeding edge of digital innovation. While we're excited about their possibilities, the nature of cutting-edge development means that the final implementation may evolve. Stay tuned for updates as we continue to push the boundaries of what's possible in the digital realm! +# Future Features Roadmap + +## Core Enhancements + +### Progressive Web App (PWA) +- Offline functionality +- Install as native app +- Push notifications +- Background sync + +### Performance Optimization +- Lazy loading for large files +- WebAssembly for intensive operations +- Service worker caching +- Resource compression + +### User Experience +- Customizable keyboard shortcuts +- Undo/redo functionality +- Batch processing +- Drag and drop everywhere +- Touch gestures support + +## Tool-Specific Features + +### Text to Speech +- Voice customization +- Emotion detection and expression +- Multiple language support +- Background music mixing +- Subtitle generation + +### Image Resizer +- Batch processing +- AI-powered upscaling +- Format conversion +- Metadata preservation +- Advanced cropping tools + +### Color Palette +- AI color suggestions +- Accessibility contrast checking +- Brand color extraction +- Pattern generation +- CSS gradient creator + +### ASCII Art +- Animation support +- Custom character sets +- Color optimization +- Style presets +- SVG export + +### QR Code +- Custom design templates +- Logo integration +- Animated QR codes +- Tracking analytics +- Batch generation + +## Technical Improvements + +### Testing +- Unit test coverage +- E2E testing suite +- Performance benchmarks +- Accessibility testing +- Cross-browser testing + +### Documentation +- API documentation +- User guides +- Video tutorials +- Code examples +- Contributing guidelines + +### Infrastructure +- CI/CD pipeline +- Automated deployment +- Error tracking +- Usage analytics +- Performance monitoring + +### Security +- Input sanitization +- CSRF protection +- Rate limiting +- Content security policy +- Security headers + +## Integration Features + +### Cloud Storage +- Google Drive +- Dropbox +- OneDrive +- iCloud +- Local storage sync + +### Social Sharing +- Direct sharing +- Social media preview +- Embed codes +- Share analytics +- Custom branding + +### Export Options +- Multiple formats +- Batch export +- Custom templates +- Metadata inclusion +- Compression options + +## Accessibility + +### Screen Readers +- ARIA labels +- Focus management +- Skip links +- Semantic HTML +- Voice navigation + +### Keyboard Navigation +- Custom shortcuts +- Focus indicators +- Tab order +- Keyboard traps prevention +- Shortcut help + +### Visual Accessibility +- High contrast mode +- Font size controls +- Color blind modes +- Motion reduction +- Text spacing + +## Mobile Support + +### Touch Interface +- Touch gestures +- Mobile-first design +- Offline support +- Share integration +- Camera access + +### Responsive Design +- Fluid layouts +- Breakpoint optimization +- Touch targets +- Mobile navigation +- Performance optimization + +## Internationalization + +### Language Support +- Multiple languages +- RTL support +- Date/time formats +- Number formats +- Currency handling + +### Cultural Adaptation +- Color meanings +- Icon localization +- Content adaptation +- Regional preferences +- Local standards + +These features represent our vision for the future of Digital Services Hub. While we're excited about implementing them, the actual development timeline and final implementation may vary based on user feedback and technical considerations. [Return to Main README](README.md) diff --git a/docs/PROJECT-DESCRIPTION.md b/docs/PROJECT-DESCRIPTION.md index c4eb3c0..f1f9f37 100644 --- a/docs/PROJECT-DESCRIPTION.md +++ b/docs/PROJECT-DESCRIPTION.md @@ -1,81 +1,144 @@ -Project Overview: Digital Services HUB - -This project appears to be a web-based platform offering various digital tools and services. Based on the file structure and names, it seems to include several standalone tools accessible through a common interface. - -Project Structure: - -1. Root Directory: - - index.html (main entry point) - - LICENSE file - -2. CSS Directory: - - ascii-art.css - - color-palette.css - - image-resizer.css - - qr-generator.css - - styles.css (likely the main stylesheet) - -3. JavaScript Directory (js): - - ascii-art.js - - color-palette.js - - common.js (shared functionality across pages) - - image-resizer.js - - qr-generator.js - -4. HTML Pages Directory (pages): - - about.html - - ascii-art.html - - color-palette.html - - image-resizer.html - - qr-generator.html - -5. Images Directory: - - Contains .png file(s) - -6. Docs Directory: - - FUTURE-FEATURES.md - - README.md - -7. Configuration: - - config.yml - -Main Features: - -1. Image Resizer: Allows users to resize images. -2. Color Palette Generator: Likely generates color schemes or palettes. -3. ASCII Art Converter: Converts images or text into ASCII art. -4. QR Code Generator: Creates QR codes from input data. -5. About Page: Provides information about the project or services. - -Recent Updates: -- We've added localStorage functionality to common.js to allow for saving user preferences and recent operations across sessions. -- The navigation is dynamically generated in common.js, ensuring consistency across all pages. - -Key Components: - -1. common.js: - - Handles site-wide functionality like navigation. - - Includes localStorage utilities for data persistence. - -2. Tool-specific JS files (e.g., image-resizer.js): - - Contain the core logic for each tool. - - Have been updated to use localStorage for saving user preferences. - -3. HTML files: - - Provide the structure for each tool's interface. - - Link to both common and tool-specific CSS and JS files. - -4. CSS files: - - Style the interface for each tool and the overall site. - -Next Steps: - -1. Review and update each tool-specific JS file to implement localStorage functionality similar to image-resizer.js. -2. Ensure all HTML files are properly structured and linked to the correct CSS and JS files. -3. Test each tool thoroughly to ensure proper functionality and data persistence. -4. Consider implementing additional features or improvements as outlined in FUTURE-FEATURES.md. -5. Update the README.md with any new information about the project's features and usage. - -This project appears to be a well-structured, modular web application providing various digital services. The use of separate HTML, CSS, and JS files for each tool allows for easy maintenance and scalability. The recent addition of localStorage functionality enhances user experience by remembering preferences and recent operations. - -Is there any specific area of the project you'd like to focus on or any particular feature you'd like to implement or improve? \ No newline at end of file +# Digital Services Hub - Project Description + +## Overview + +Digital Services Hub is a modern web application that provides a collection of digital tools and services. The project is built with a focus on modularity, accessibility, and user experience, utilizing modern web technologies and best practices. + +## Core Features + +### Text to Speech +A powerful text-to-speech converter that supports: +- Multiple voices and languages +- Adjustable speech parameters (speed, pitch, volume) +- History management +- Audio export capabilities + +### Image Resizer +A versatile image resizing tool offering: +- Aspect ratio preservation +- Multiple output formats +- Quality control +- Drag and drop support +- Real-time preview + +### Color Palette +An advanced color palette generator featuring: +- Color harmony generation +- Palette management +- Multiple export formats +- Interactive color picker +- Real-time preview + +### ASCII Art +A creative ASCII art generator with: +- Image to ASCII conversion +- Multiple character sets +- Color support +- Size customization +- Export options + +### QR Code +A flexible QR code generator providing: +- Customizable appearance +- Error correction levels +- Size options +- Real-time preview +- PNG export + +## Technical Architecture + +### Base Tool Class +The foundation of all tools, providing: +- Theme management +- File handling +- Notification system +- Keyboard shortcuts +- Error handling + +### Utility Modules + +#### constants.js +- Application configuration +- Theme definitions +- File limits +- Error messages +- API endpoints +- Keyboard shortcuts + +#### helpers.js +- HTML sanitization +- Email validation +- UID generation +- Deep cloning +- Browser detection +- Storage management +- Image handling +- Viewport utilities + +#### validation.js +- Input validation +- File validation +- Error handling +- Custom validation rules +- Validation error formatting + +#### ui.js +- Notification management +- Theme handling +- Modal system +- Loading indicators +- Responsive helpers + +## Project Structure + +``` +digital-services-hub/ +├── css/ +│ ├── components/ # Tool-specific styles +│ ├── themes/ # Theme definitions +│ └── utils/ # Shared styles +├── js/ +│ ├── features/ # Tool implementations +│ └── utils/ # Shared utilities +├── pages/ # Tool pages +└── index.html # Main entry point +``` + +## Recent Updates + +1. Implemented modular architecture with BaseTool class +2. Added comprehensive utility modules +3. Enhanced error handling and validation +4. Improved accessibility features +5. Added keyboard shortcuts +6. Updated theme management +7. Enhanced documentation + +## Next Steps + +1. Add unit tests for core functionality +2. Implement E2E testing +3. Add PWA support +4. Enhance offline capabilities +5. Add more language support +6. Implement user preferences sync + +## Development Guidelines + +1. Follow modular architecture +2. Maintain consistent code style +3. Write comprehensive documentation +4. Include accessibility features +5. Support keyboard navigation +6. Implement error handling +7. Add appropriate logging +8. Follow security best practices + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines on: +- Code style +- Pull request process +- Development setup +- Testing requirements +- Documentation standards \ No newline at end of file diff --git a/docs/PROJECT-OVERVIEW.md b/docs/PROJECT-OVERVIEW.md index 0f25c9a..e373af3 100644 --- a/docs/PROJECT-OVERVIEW.md +++ b/docs/PROJECT-OVERVIEW.md @@ -1,322 +1,235 @@ -1. Root Directory: - ---- START OF index.html --- - - - - - - Digital Services Hub - - - - -
-

Digital Services Hub

- - -
-

Welcome to our Digital Services Hub!

-

We offer a range of free tools to help with your digital media needs:

- - -

Choose a service above to get started, or visit our About page to learn more.

-
-
- - - ---- END OF index.html --- - ---- START OF LICENSE --- -MIT License - -Copyright (c) [year] [fullname] - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. ---- END OF LICENSE --- - -Now, let's move to the CSS directory: - -2. CSS Directory: - ---- START OF styles.css --- -body { - font-family: 'Roboto', sans-serif; - background-color: #0c0c0c; - color: #ffffff; - line-height: 1.6; - margin: 0; - padding: 0; -} - -.container { - width: 80%; - margin: auto; - overflow: hidden; - padding: 20px; -} - -h1, h2, h3 { - font-family: 'Orbitron', sans-serif; - margin-bottom: 20px; -} - -.neon-text { - color: #fff; - text-shadow: 0 0 5px #fff, 0 0 10px #fff, 0 0 15px #fff, 0 0 20px #ff00de, 0 0 35px #ff00de, 0 0 40px #ff00de, 0 0 50px #ff00de, 0 0 75px #ff00de; -} - -nav { - background: #1a1a1a; - padding: 10px 0; -} - -nav a { - color: #ffffff; - text-decoration: none; - padding: 10px 15px; - margin: 0 5px; - transition: 0.3s; -} - -nav a:hover { - background: #ff00de; - border-radius: 5px; -} - -.content { - background: #1a1a1a; - padding: 20px; - margin-top: 20px; - border-radius: 5px; -} - -.welcome-text { - font-size: 1.5em; - margin-bottom: 20px; -} - -.services-list { - list-style-type: none; - padding: 0; -} - -.services-list li { - margin-bottom: 10px; -} - -.services-list a { - color: #00ffff; - text-decoration: none; - transition: 0.3s; -} - -.services-list a:hover { - color: #ff00de; -} - -.cta-text { - margin-top: 20px; - font-style: italic; -} - -/* Add more styles as needed for specific pages */ ---- END OF styles.css --- - ---- START OF ascii-art.css --- -/* Styles specific to the ASCII Art page */ -.ascii-container { - background: #0f0f0f; - border: 1px solid #2a2a2a; - border-radius: 5px; - padding: 20px; - margin-top: 20px; -} - -#input-text, #output-ascii { - width: 100%; - padding: 10px; - margin-bottom: 10px; - background: #1a1a1a; - border: 1px solid #2a2a2a; - color: #ffffff; - font-family: monospace; -} - -#output-ascii { - white-space: pre; - overflow-x: auto; -} - -#convert-button { - background: #ff00de; - color: #ffffff; - border: none; - padding: 10px 20px; - cursor: pointer; - transition: 0.3s; -} - -#convert-button:hover { - background: #00ffff; - color: #0c0c0c; -} ---- END OF ascii-art.css --- - ---- START OF color-palette.css --- -/* Styles specific to the Color Palette Generator page */ -.palette-container { - display: flex; - flex-wrap: wrap; - justify-content: space-around; - margin-top: 20px; -} - -.color-swatch { - width: 100px; - height: 100px; - margin: 10px; - border-radius: 5px; - display: flex; - align-items: center; - justify-content: center; - font-family: monospace; - color: #ffffff; - text-shadow: 1px 1px 1px rgba(0,0,0,0.5); -} - -#generate-button { - background: #ff00de; - color: #ffffff; - border: none; - padding: 10px 20px; - cursor: pointer; - transition: 0.3s; - margin-top: 20px; -} - -#generate-button:hover { - background: #00ffff; - color: #0c0c0c; -} ---- END OF color-palette.css --- - ---- START OF image-resizer.css --- -/* Styles specific to the Image Resizer page */ -.resizer-container { - background: #0f0f0f; - border: 1px solid #2a2a2a; - border-radius: 5px; - padding: 20px; - margin-top: 20px; -} - -#image-input, #width, #height { - width: 100%; - padding: 10px; - margin-bottom: 10px; - background: #1a1a1a; - border: 1px solid #2a2a2a; - color: #ffffff; -} - -#resize-button { - background: #ff00de; - color: #ffffff; - border: none; - padding: 10px 20px; - cursor: pointer; - transition: 0.3s; -} - -#resize-button:hover { - background: #00ffff; - color: #0c0c0c; -} - -#output-image { - max-width: 100%; - height: auto; - margin-top: 20px; - border: 1px solid #2a2a2a; -} - -#download-link { - display: inline-block; - margin-top: 10px; - color: #00ffff; - text-decoration: none; -} - -#download-link:hover { - color: #ff00de; -} ---- END OF image-resizer.css --- - ---- START OF qr-generator.css --- -/* Styles specific to the QR Code Generator page */ -.qr-container { - background: #0f0f0f; - border: 1px solid #2a2a2a; - border-radius: 5px; - padding: 20px; - margin-top: 20px; -} - -#qr-text { - width: 100%; - padding: 10px; - margin-bottom: 10px; - background: #1a1a1a; - border: 1px solid #2a2a2a; - color: #ffffff; -} - -#generate-button { - background: #ff00de; - color: #ffffff; - border: none; - padding: 10px 20px; - cursor: pointer; - transition: 0.3s; -} - -#generate-button:hover { - background: #00ffff; - color: #0c0c0c; -} - -#qr-code { - margin-top: 20px; - text-align: center; -} - -#qr-code img { - max-width: 100%; - height: auto; -} ---- END OF qr-generator.css --- - -That covers the CSS directory. Would you like me to continue with the JavaScript (js) directory next? \ No newline at end of file +# Digital Services Hub - Technical Overview + +## Project Architecture + +### Core Components + +1. **Base Tool Class** + ```javascript + class BaseTool { + constructor() { + this.initializeElements(); + this.initializeState(); + this.bindEvents(); + } + + initializeElements() { /* ... */ } + initializeState() { /* ... */ } + bindEvents() { /* ... */ } + toggleTheme() { /* ... */ } + showNotification() { /* ... */ } + handleError() { /* ... */ } + } + ``` + +2. **Utility Modules** + ```javascript + // constants.js + export const APP_CONFIG = { /* ... */ }; + export const STORAGE_KEYS = { /* ... */ }; + export const THEMES = { /* ... */ }; + + // helpers.js + export const sanitizeHTML = (html) => { /* ... */ }; + export const isValidEmail = (email) => { /* ... */ }; + export const generateUID = () => { /* ... */ }; + + // validation.js + export class ValidationError extends Error { /* ... */ } + export const validateInput = (input) => { /* ... */ }; + + // ui.js + export const notifications = { /* ... */ }; + export const themeManager = { /* ... */ }; + ``` + +### Directory Structure + +``` +digital-services-hub/ +├── css/ +│ ├── components/ +│ │ ├── ascii-art.css +│ │ ├── color-palette.css +│ │ ├── image-resizer.css +│ │ ├── qr-code.css +│ │ └── text-to-speech.css +│ ├── themes/ +│ │ ├── dark.css +│ │ └── light.css +│ └── utils/ +│ ├── animations.css +│ └── layout.css +├── js/ +│ ├── features/ +│ │ ├── ascii-art.js +│ │ ├── color-palette.js +│ │ ├── image-resizer.js +│ │ ├── qr-code.js +│ │ └── text-to-speech.js +│ └── utils/ +│ ├── constants.js +│ ├── helpers.js +│ ├── validation.js +│ └── ui.js +├── pages/ +│ ├── ascii-art.html +│ ├── color-palette.html +│ ├── image-resizer.html +│ ├── qr-code.html +│ └── text-to-speech.html +└── index.html +``` + +## Implementation Details + +### Feature Modules + +1. **Text to Speech** + ```javascript + class TextToSpeech extends BaseTool { + speak() { /* ... */ } + updateProgress() { /* ... */ } + downloadAudio() { /* ... */ } + } + ``` + +2. **Image Resizer** + ```javascript + class ImageResizer extends BaseTool { + resizeImage() { /* ... */ } + updateDimensions() { /* ... */ } + downloadImage() { /* ... */ } + } + ``` + +3. **Color Palette** + ```javascript + class ColorPalette extends BaseTool { + generateHarmony() { /* ... */ } + savePalette() { /* ... */ } + exportColors() { /* ... */ } + } + ``` + +4. **ASCII Art** + ```javascript + class AsciiArt extends BaseTool { + generateArt() { /* ... */ } + updatePreview() { /* ... */ } + downloadResult() { /* ... */ } + } + ``` + +5. **QR Code** + ```javascript + class QRCode extends BaseTool { + generateCode() { /* ... */ } + updateOptions() { /* ... */ } + downloadQR() { /* ... */ } + } + ``` + +### Common Patterns + +1. **Event Handling** + ```javascript + bindEvents() { + this.element.addEventListener('click', this.handleClick); + this.input.addEventListener('change', this.handleChange); + document.addEventListener('keydown', this.handleKeyboard); + } + ``` + +2. **State Management** + ```javascript + initializeState() { + this.state = { + theme: localStorage.getItem(STORAGE_KEYS.THEME), + history: JSON.parse(localStorage.getItem(STORAGE_KEYS.HISTORY)), + settings: JSON.parse(localStorage.getItem(STORAGE_KEYS.SETTINGS)) + }; + } + ``` + +3. **Error Handling** + ```javascript + try { + await this.processData(); + } catch (error) { + this.handleError(error); + this.showNotification('error', error.message); + } + ``` + +## Technical Specifications + +### Browser Support +- Chrome 80+ +- Firefox 75+ +- Safari 13+ +- Edge 80+ + +### Performance Targets +- Initial load: < 2s +- Tool initialization: < 500ms +- Operation response: < 100ms + +### Security Measures +- Input sanitization +- Content Security Policy +- CORS configuration +- XSS prevention +- CSRF protection + +### Accessibility +- ARIA labels +- Keyboard navigation +- Screen reader support +- High contrast mode +- Focus management + +## Development Guidelines + +### Code Style +```javascript +// Use meaningful names +const generateUniqueIdentifier = () => { + return Date.now().toString(36) + Math.random().toString(36).substr(2); +}; + +// Add JSDoc comments +/** + * Validates user input and returns sanitized data + * @param {string} input - Raw user input + * @returns {string} Sanitized input + * @throws {ValidationError} If input is invalid + */ +const validateAndSanitize = (input) => { + // Implementation +}; + +// Use consistent error handling +try { + await processUserInput(input); +} catch (error) { + logger.error('Failed to process user input:', error); + throw new ValidationError('Invalid input provided'); +} +``` + +### Testing Requirements +- Unit tests for all utility functions +- Integration tests for feature modules +- E2E tests for critical paths +- Accessibility testing +- Performance testing + +### Documentation Standards +- JSDoc for all functions +- README for each module +- API documentation +- Usage examples +- Change log + +This technical overview provides a foundation for understanding the project's architecture and implementation details. For specific implementation details, refer to the individual module documentation. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index ce434b9..a1a8859 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,121 +1,154 @@ # Digital Services Hub -Welcome to the Digital Services Hub repository! This project contains a set of web tools designed to enhance digital experiences. Below you'll find details about the different themes available, features, and usage instructions. - -## Documentation - -- [README](docs/README.md) -- [Future Features](docs/FUTURE-FEATURES.md) - -## Repository Stats - -![GitHub Stars](https://img.shields.io/github/stars/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![GitHub Forks](https://img.shields.io/github/forks/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Watchers](https://img.shields.io/github/watchers/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Contributors](https://img.shields.io/github/contributors/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Issues](https://img.shields.io/github/issues/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Open Issues](https://img.shields.io/github/issues-raw/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Closed Issues](https://img.shields.io/github/issues-closed-raw/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Pull Requests](https://img.shields.io/github/issues-pr/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Last Commit](https://img.shields.io/github/last-commit/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Commit Activity](https://img.shields.io/github/commit-activity/m/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![License](https://img.shields.io/github/license/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Code Size](https://img.shields.io/github/languages/code-size/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Repository Size](https://img.shields.io/github/repo-size/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Language Count](https://img.shields.io/github/languages/count/TMHSDigital/Digital_Services.HUB?style=for-the-badge) -![Top Language](https://img.shields.io/github/languages/top/TMHSDigital/Digital_Services.HUB?style=for-the-badge) - -## Themes - -> ### [![Cutting-Edge-Theme](https://img.shields.io/badge/Cutting--Edge-Theme-blue?style=for-the-badge)](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Cutting-Edge-Theme) -> ___A futuristic and vibrant theme with glowing elements and modern design aesthetics.___ - -___ - -> ### [![Minimalistic-Theme](https://img.shields.io/badge/Minimalistic-Theme-green?style=for-the-badge)](https://github.com/TMHSDigital/Digital_Services.HUB/tree/Minimalistic-Theme) -> ___A clean and simple theme focused on usability and minimalist design principles.___ +A modern web-based platform offering various digital tools and services, built with a focus on modularity, accessibility, and user experience. ## Features -- **Image Resizer:** Easily resize images by specifying width and height. -- **Color Palette Generator:** Create and explore various color palettes for design projects. -- **ASCII Art Converter:** Transform text into ASCII masterpieces. -- **QR Code Generator:** Create custom QR codes for various purposes. - -## 🚀 Upcoming Features - -We're constantly innovating! Check out our [Future Features Roadmap](docs/FUTURE-FEATURES.md) for exciting upcoming additions. - -## Usage - -
-How to Use - -### General Usage Instructions: - -1. **Visit the GitHub Pages Site:** - Click on the button below to visit our GitHub Pages site where all tools are hosted: -

- - Visit Site - -

- -2. **Select a Theme:** - Choose your preferred theme from the available options. Each theme offers a unique look and feel to enhance your user experience. - -3. **Navigate to the Desired Tool:** - Browse through the list of available tools. Click on the tool you want to use. Each tool is designed to be intuitive and user-friendly. - -4. **Follow On-Screen Instructions:** - Each tool comes with its own set of instructions. Follow these instructions to utilize the tool effectively. For instance: - - - **Image Resizer:** Upload an image, specify the desired width and height, and click "Resize" to get the resized image. - - **Color Palette Generator:** Choose or input base colors, and the tool will generate a palette of complementary colors. - - **ASCII Art Converter:** Input your text, choose formatting options, and click "Convert" to generate ASCII art. - - **QR Code Generator:** Input the data you want encoded, choose customization options, and generate the QR code. - -5. **Enjoy Enhanced Digital Experience:** - Utilize the results as needed. Download images, copy text, or use the generated content in your projects. - -### Detailed Tool Instructions: - -#### Image Resizer: -- **Upload an Image:** Click on the "Upload" button to select an image from your device. -- **Specify Dimensions:** Enter the desired width and height for the image. -- **Resize:** Click "Resize" to process the image. The resized image will be available for download. - -#### Color Palette Generator: -- **Select Base Colors:** Either select colors using a color picker or input hex values. -- **Generate Palette:** Click "Generate" to see a palette of complementary colors. -- **Explore Variations:** Adjust the base colors and regenerate as needed. - -#### ASCII Art Converter: -- **Input Text:** Type or paste the text you want to convert. -- **Choose Options:** Select font style, size, and other formatting options. -- **Convert:** Click "Convert" to see your text in ASCII art format. Copy the art for use. - -#### QR Code Generator: -- **Input Data:** Enter the URL or text you want to encode in the QR code. -- **Customize:** Choose color, size, and error correction level. -- **Generate:** Click "Generate" to create the QR code. Download or share it directly. - -
- -## Connect - -
-Get in Touch -

- - GitHub Profile - -

-
+1. **Text to Speech** + - Convert text to natural-sounding speech + - Multiple voices and languages + - Adjustable speed, pitch, and volume + - Save and load history + - Export audio files + +2. **Image Resizer** + - Resize images with aspect ratio preservation + - Multiple output formats + - Quality control + - Drag and drop support + - Preview functionality + +3. **Color Palette** + - Generate color harmonies + - Save and load palettes + - Export in multiple formats (HEX, RGB, CSS) + - Color picker with gradient + - Real-time preview + +4. **ASCII Art** + - Convert images to ASCII art + - Multiple character sets + - Color support + - Size customization + - Export functionality + +5. **QR Code** + - Generate customizable QR codes + - Error correction levels + - Custom colors and size + - Real-time preview + - Download as PNG + +## Getting Started + +1. Clone the repository: + ```bash + git clone https://github.com/yourusername/digital-services-hub.git + ``` + +2. Open index.html in your browser or set up a local server: + ```bash + python -m http.server 8000 + ``` + +3. Visit http://localhost:8000 in your browser + +## Architecture + +### Base Tool Class +All tools extend the BaseTool class which provides: +- Theme management +- File handling +- Notifications +- Keyboard shortcuts +- Error handling + +### Utility Modules +- **constants.js**: Configuration values +- **helpers.js**: Common functions +- **validation.js**: Input validation +- **ui.js**: UI components + +### Features +Each tool is implemented as a module with: +- Consistent interface +- Error handling +- Accessibility support +- Keyboard navigation +- Theme support + +## Keyboard Shortcuts + +### Global +- `Alt + 1-5`: Navigate to tools +- `Ctrl + T`: Toggle theme + +### Text to Speech +- `Ctrl + Enter`: Start/Stop speech +- `Ctrl + S`: Save text + +### Image Resizer +- `Ctrl + S`: Download image +- `Ctrl + L`: Toggle aspect ratio lock + +### Color Palette +- `Ctrl + S`: Save palette +- `Ctrl + E`: Export colors +- `Ctrl + G`: Generate harmony + +### ASCII Art +- `Ctrl + G`: Generate art +- `Ctrl + C`: Copy to clipboard +- `Ctrl + S`: Download result + +### QR Code +- `Ctrl + G`: Generate code +- `Ctrl + S`: Download QR code + +## Browser Support + +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) + +## Development + +### Prerequisites +- Modern web browser +- Text editor +- Basic understanding of HTML, CSS, and JavaScript + +### Project Structure +``` +digital-services-hub/ +├── css/ +│ ├── components/ +│ ├── themes/ +│ └── utils/ +├── js/ +│ ├── features/ +│ └── utils/ +├── pages/ +└── index.html +``` + +### Adding New Features +1. Create feature files: + - `js/features/your-feature.js` + - `css/components/your-feature.css` + - `pages/your-feature.html` +2. Extend BaseTool class +3. Add to navigation +4. Update documentation + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Submit a pull request ## License -
-MIT License -

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

-
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/index.html b/index.html index 360df1c..62ed25c 100644 --- a/index.html +++ b/index.html @@ -5,30 +5,28 @@ Digital Services Hub - + + -
-

Digital Services Hub

- - - -
-

Welcome to our Digital Services Hub!

- -

We offer a range of free tools to help with your digital media needs:

- - - -

Choose a service above to get started, or visit our About page to learn more.

-
- - +
+

Digital Services Hub

+ +
+
+
+

Welcome to Digital Services Hub

+

A collection of useful web-based tools for everyday digital tasks.

+
+
diff --git a/js/ascii-art.js b/js/ascii-art.js deleted file mode 100644 index 9519a3f..0000000 --- a/js/ascii-art.js +++ /dev/null @@ -1,18 +0,0 @@ -const textInput = document.getElementById('text-input'); -const convertButton = document.getElementById('convert-button'); -const asciiOutput = document.getElementById('ascii-output'); - -const asciiChars = ['@', '#', 'S', '%', '?', '*', '+', ';', ':', ',', '.']; - -function textToAscii(text) { - return text.split('').map(char => { - const index = Math.floor(Math.random() * asciiChars.length); - return asciiChars[index]; - }).join(''); -} - -convertButton.addEventListener('click', () => { - const inputText = textInput.value; - const asciiArt = inputText.split('\n').map(line => textToAscii(line)).join('\n'); - asciiOutput.textContent = asciiArt; -}); diff --git a/js/color-palette.js b/js/color-palette.js deleted file mode 100644 index 3b92b64..0000000 --- a/js/color-palette.js +++ /dev/null @@ -1,47 +0,0 @@ -const fileInput = document.getElementById('file-input'); -const imageContainer = document.getElementById('image-container'); -const paletteContainer = document.getElementById('palette-container'); -const generateButton = document.getElementById('generate-button'); - -fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - imageContainer.innerHTML = ``; - }; - reader.readAsDataURL(file); - } -}); - -generateButton.addEventListener('click', () => { - const image = document.getElementById('uploaded-image'); - if (image) { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - canvas.width = image.width; - canvas.height = image.height; - ctx.drawImage(image, 0, 0, image.width, image.height); - - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data; - const colorMap = {}; - - for (let i = 0; i < imageData.length; i += 4) { - const r = imageData[i]; - const g = imageData[i + 1]; - const b = imageData[i + 2]; - const rgb = `rgb(${r},${g},${b})`; - colorMap[rgb] = (colorMap[rgb] || 0) + 1; - } - - const sortedColors = Object.entries(colorMap).sort((a, b) => b[1] - a[1]); - const topColors = sortedColors.slice(0, 5); - - paletteContainer.innerHTML = topColors.map(([color, count]) => ` -
-
-
${color}
-
- `).join(''); - } -}); diff --git a/js/common.js b/js/common.js index 7f9ff6d..ed5a402 100644 --- a/js/common.js +++ b/js/common.js @@ -7,7 +7,7 @@ document.addEventListener('DOMContentLoaded', (event) => { { href: "pages/image-resizer.html", text: "Image Resizer" }, { href: "pages/color-palette.html", text: "Color Palette" }, { href: "pages/ascii-art.html", text: "ASCII Art" }, - { href: "pages/qr-generator.html", text: "QR Code" }, + { href: "pages/qr-code.html", text: "QR Code" }, { href: "pages/text-to-speech.html", text: "Text-to-Speech" }, { href: "pages/about.html", text: "About" } ]; diff --git a/js/features/about.js b/js/features/about.js new file mode 100644 index 0000000..c939ab5 --- /dev/null +++ b/js/features/about.js @@ -0,0 +1,166 @@ +import { BaseTool } from './base-tool.js'; +import { notifications, modal } from '../utils/ui.js'; +import { STORAGE_KEYS, APP_CONFIG } from '../utils/constants.js'; +import utils from '../utils/helpers.js'; + +class About extends BaseTool { + initializeElements() { + return { + themeButton: document.getElementById('theme-button'), + notification: document.querySelector('.notification'), + featureCards: document.querySelectorAll('.feature-card'), + versionInfo: document.getElementById('version-info'), + githubLink: document.getElementById('github-link') + }; + } + + initializeState() { + return { + currentTheme: utils.getStorageItem(STORAGE_KEYS.THEME) || 'dark', + version: APP_CONFIG.VERSION, + lastUpdated: '2024-02', + features: [ + { + name: 'Text to Speech', + description: 'Convert text to natural-sounding speech with multiple voices and languages.', + icon: '🗣️', + path: 'text-to-speech' + }, + { + name: 'Image Resizer', + description: 'Resize and optimize images with aspect ratio preservation and format conversion.', + icon: '🖼️', + path: 'image-resizer' + }, + { + name: 'Color Palette', + description: 'Generate and customize color palettes with harmony rules and export options.', + icon: '🎨', + path: 'color-palette' + }, + { + name: 'ASCII Art', + description: 'Convert images into ASCII art with customizable settings and color support.', + icon: '🎯', + path: 'ascii-art' + }, + { + name: 'QR Code', + description: 'Generate customizable QR codes with error correction and styling options.', + icon: '📱', + path: 'qr-code' + } + ] + }; + } + + updateVersionInfo() { + if (this.elements.versionInfo) { + this.elements.versionInfo.innerHTML = ` +

Version: ${utils.sanitizeHTML(this.state.version)}

+

Last Updated: ${utils.sanitizeHTML(this.state.lastUpdated)}

+

Author: ${utils.sanitizeHTML(APP_CONFIG.AUTHOR)}

+ `; + } + + if (this.elements.githubLink) { + this.elements.githubLink.href = APP_CONFIG.GITHUB_URL; + } + } + + addFeatureCardEffects() { + this.elements.featureCards.forEach(card => { + // Mouse hover effects + card.addEventListener('mouseenter', () => { + card.classList.add('hover'); + }); + + card.addEventListener('mouseleave', () => { + card.classList.remove('hover'); + }); + + // Click navigation + card.addEventListener('click', () => { + const feature = card.getAttribute('data-feature'); + if (feature) { + try { + const featureInfo = this.state.features.find(f => f.path === feature); + if (featureInfo) { + window.location.href = `${feature}.html`; + notifications.info(`Navigating to ${featureInfo.name}...`); + } + } catch (error) { + console.error('Error navigating to feature:', error); + notifications.error('Failed to navigate to feature. Please try again.'); + } + } + }); + + // Keyboard navigation + card.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + card.click(); + } + }); + + // Accessibility + card.setAttribute('role', 'button'); + card.setAttribute('tabindex', '0'); + + const feature = card.getAttribute('data-feature'); + if (feature) { + const featureInfo = this.state.features.find(f => f.path === feature); + if (featureInfo) { + card.setAttribute('aria-label', `Open ${featureInfo.name} tool`); + } + } + }); + } + + showFeatureInfo(feature) { + const featureInfo = this.state.features.find(f => f.path === feature); + if (featureInfo) { + modal.show({ + title: featureInfo.name, + content: ` +
+
${featureInfo.icon}
+

${utils.sanitizeHTML(featureInfo.description)}

+
+ `, + buttons: { + 'Open Tool': () => window.location.href = `${feature}.html`, + 'Close': () => {} + } + }); + } + } + + bindEvents() { + // Theme toggle + this.elements.themeButton.addEventListener('click', () => { + this.toggleTheme(STORAGE_KEYS.THEME); + }); + + // Feature card interactions + this.addFeatureCardEffects(); + + // Keyboard shortcuts + this.state.features.forEach((feature, index) => { + this.addKeyboardShortcut((index + 1).toString(), () => { + window.location.href = `${feature.path}.html`; + }, { alt: true }); + }); + } + + initialize() { + document.documentElement.setAttribute('data-theme', this.state.currentTheme); + this.updateVersionInfo(); + } +} + +// Initialize the feature when the DOM is ready +document.addEventListener('DOMContentLoaded', () => { + new About(); +}); \ No newline at end of file diff --git a/js/features/ascii-art.js b/js/features/ascii-art.js new file mode 100644 index 0000000..f29054f --- /dev/null +++ b/js/features/ascii-art.js @@ -0,0 +1,224 @@ +import { BaseTool } from './base-tool.js'; +import { notifications } from '../utils/ui.js'; +import { STORAGE_KEYS, FILE_LIMITS, UI_CONSTANTS } from '../utils/constants.js'; +import { fileValidation } from '../utils/validation.js'; +import utils from '../utils/helpers.js'; + +class AsciiArt extends BaseTool { + initializeElements() { + return { + fileInput: document.getElementById('file-input'), + dropZone: document.querySelector('.drop-zone'), + preview: document.getElementById('preview'), + output: document.getElementById('ascii-output'), + widthInput: document.getElementById('width-input'), + charsetSelect: document.getElementById('charset-select'), + invertCheckbox: document.getElementById('invert-checkbox'), + colorCheckbox: document.getElementById('color-checkbox'), + generateButton: document.getElementById('generate-button'), + copyButton: document.getElementById('copy-button'), + downloadButton: document.getElementById('download-button'), + themeButton: document.getElementById('theme-button'), + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + currentImage: null, + currentTheme: utils.getStorageItem(STORAGE_KEYS.THEME) || 'dark', + charsets: { + standard: '@%#*+=-:. ', + blocks: '█▓▒░ ', + simple: '#@$*. ', + dots: '●○◐◑◒◓◔◕. ', + custom: '@QB#NgWM8RDHdKA$kbq&pmtxjf[]{}?wyl<>i!;:,"^`. ' + } + }; + } + + async handleFileSelect(file) { + try { + // Validate file + await fileValidation.validateFileSize(file); + fileValidation.validateFileType(file, FILE_LIMITS.SUPPORTED_IMAGE_TYPES); + + const img = await utils.loadImage(URL.createObjectURL(file)); + await fileValidation.validateImageDimensions(img); + + this.state.currentImage = img; + this.displayPreview(); + notifications.success('Image loaded successfully'); + } catch (error) { + console.error('Error loading image:', error); + notifications.error(error.message || 'Failed to load image'); + this.resetState(); + } + } + + resetState() { + this.state.currentImage = null; + this.elements.preview.innerHTML = ''; + this.elements.output.innerHTML = ''; + } + + displayPreview() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const maxWidth = 300; + const scale = maxWidth / this.state.currentImage.width; + + canvas.width = maxWidth; + canvas.height = this.state.currentImage.height * scale; + + ctx.drawImage(this.state.currentImage, 0, 0, canvas.width, canvas.height); + this.elements.preview.innerHTML = ''; + this.elements.preview.appendChild(canvas); + } + + getPixelBrightness(r, g, b, invert) { + const brightness = (0.299 * r + 0.587 * g + 0.114 * b) / 255; + return invert ? 1 - brightness : brightness; + } + + rgbToHex(r, g, b) { + return '#' + [r, g, b].map(x => { + const hex = x.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); + } + + generateAsciiArt() { + if (!this.state.currentImage) { + notifications.error('Please select an image first.'); + return; + } + + const width = parseInt(this.elements.widthInput.value) || 100; + const charset = this.state.charsets[this.elements.charsetSelect.value]; + const invert = this.elements.invertCheckbox.checked; + const useColor = this.elements.colorCheckbox.checked; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const scale = width / this.state.currentImage.width; + + canvas.width = width; + canvas.height = Math.floor(this.state.currentImage.height * scale); + + ctx.drawImage(this.state.currentImage, 0, 0, canvas.width, canvas.height); + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const pixels = imageData.data; + + let ascii = ''; + for (let y = 0; y < canvas.height; y++) { + for (let x = 0; x < canvas.width; x++) { + const offset = (y * canvas.width + x) * 4; + const r = pixels[offset]; + const g = pixels[offset + 1]; + const b = pixels[offset + 2]; + + const brightness = this.getPixelBrightness(r, g, b, invert); + const charIndex = Math.floor(brightness * (charset.length - 1)); + + if (useColor) { + const color = this.rgbToHex(r, g, b); + ascii += `${utils.sanitizeHTML(charset[charIndex])}`; + } else { + ascii += utils.sanitizeHTML(charset[charIndex]); + } + } + ascii += '\\n'; + } + + this.elements.output.innerHTML = ascii; + notifications.success('ASCII art generated successfully!'); + } + + async copyToClipboard() { + try { + const text = this.elements.output.innerText; + await utils.copyToClipboard(text); + notifications.success('ASCII art copied to clipboard!'); + } catch (error) { + console.error('Error copying to clipboard:', error); + notifications.error('Failed to copy to clipboard. Please try again.'); + } + } + + downloadAsciiArt() { + try { + const text = this.elements.output.innerText; + const blob = new Blob([text], { type: 'text/plain' }); + const filename = `ascii-art_${new Date().toISOString().slice(0,10)}.txt`; + this.downloadFile(blob, filename); + notifications.success('ASCII art downloaded successfully!'); + } catch (error) { + console.error('Error downloading ASCII art:', error); + notifications.error('Failed to download ASCII art. Please try again.'); + } + } + + bindEvents() { + // File input events + this.elements.fileInput.addEventListener('change', (e) => { + if (e.target.files.length > 0) { + this.handleFileSelect(e.target.files[0]); + } + }); + + // Drag and drop events + const dragOverHandler = this.debounce((e) => { + e.preventDefault(); + this.elements.dropZone.classList.add('drag-over'); + }, UI_CONSTANTS.DEBOUNCE_DELAY); + + this.elements.dropZone.addEventListener('dragover', dragOverHandler); + + this.elements.dropZone.addEventListener('dragleave', () => { + this.elements.dropZone.classList.remove('drag-over'); + }); + + this.elements.dropZone.addEventListener('drop', (e) => { + e.preventDefault(); + this.elements.dropZone.classList.remove('drag-over'); + if (e.dataTransfer.files.length > 0) { + this.handleFileSelect(e.dataTransfer.files[0]); + } + }); + + // Generate button + this.elements.generateButton.addEventListener('click', + this.debounce(() => this.generateAsciiArt(), UI_CONSTANTS.DEBOUNCE_DELAY) + ); + + // Copy button + this.elements.copyButton.addEventListener('click', () => { + this.copyToClipboard(); + }); + + // Download button + this.elements.downloadButton.addEventListener('click', () => { + this.downloadAsciiArt(); + }); + + // Theme toggle + this.elements.themeButton.addEventListener('click', () => { + this.toggleTheme(STORAGE_KEYS.THEME); + }); + + // Keyboard shortcuts + this.addKeyboardShortcut('g', () => this.generateAsciiArt(), { ctrl: true }); + this.addKeyboardShortcut('c', () => this.copyToClipboard(), { ctrl: true }); + this.addKeyboardShortcut('s', () => this.downloadAsciiArt(), { ctrl: true }); + } + + initialize() { + document.documentElement.setAttribute('data-theme', this.state.currentTheme); + } +} + +// Initialize the feature when the DOM is ready +document.addEventListener('DOMContentLoaded', () => { + new AsciiArt(); +}); \ No newline at end of file diff --git a/js/features/base-tool.js b/js/features/base-tool.js new file mode 100644 index 0000000..5eb2360 --- /dev/null +++ b/js/features/base-tool.js @@ -0,0 +1,230 @@ +class BaseTool { + constructor() { + if (new.target === BaseTool) { + throw new Error('BaseTool is an abstract class and cannot be instantiated directly'); + } + this.elements = this.initializeElements(); + this.state = this.initializeState(); + this.bindEvents(); + this.initialize(); + } + + /** + * Initialize DOM elements used by the tool + * @abstract + * @returns {Object} Map of element references + */ + initializeElements() { + throw new Error('initializeElements must be implemented by subclass'); + } + + /** + * Initialize tool state + * @abstract + * @returns {Object} Initial state object + */ + initializeState() { + throw new Error('initializeState must be implemented by subclass'); + } + + /** + * Bind event listeners + * @abstract + */ + bindEvents() { + throw new Error('bindEvents must be implemented by subclass'); + } + + /** + * Initialize the tool + * @abstract + */ + initialize() { + throw new Error('initialize must be implemented by subclass'); + } + + /** + * Show a notification to the user + * @param {string} message - Message to display + * @param {'success' | 'error' | 'info'} [type='info'] - Type of notification + * @param {number} [duration=3000] - Duration in milliseconds + */ + showNotification(message, type = 'info', duration = 3000) { + if (!this.elements.notification) { + console.warn('Notification element not found'); + return; + } + + const notification = this.elements.notification; + notification.textContent = message; + notification.className = 'notification'; + notification.classList.add(type, 'show'); + + // Ensure proper ARIA attributes + notification.setAttribute('role', 'alert'); + notification.setAttribute('aria-live', type === 'error' ? 'assertive' : 'polite'); + + // Clear any existing timeout + if (this._notificationTimeout) { + clearTimeout(this._notificationTimeout); + } + + // Set new timeout + this._notificationTimeout = setTimeout(() => { + notification.classList.remove('show'); + }, duration); + } + + /** + * Toggle theme between dark and light + * @param {string} storageKey - LocalStorage key for theme preference + */ + toggleTheme(storageKey) { + try { + this.state.currentTheme = this.state.currentTheme === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', this.state.currentTheme); + localStorage.setItem(storageKey, this.state.currentTheme); + this.showNotification(`${this.state.currentTheme.charAt(0).toUpperCase() + this.state.currentTheme.slice(1)} theme activated`, 'success'); + } catch (error) { + console.error('Error toggling theme:', error); + this.showNotification('Failed to toggle theme', 'error'); + } + } + + /** + * Handle file selection with validation + * @param {File} file - The selected file + * @param {Object} options - Validation options + * @param {string[]} options.allowedTypes - Allowed MIME types + * @param {number} options.maxSize - Maximum file size in bytes + * @returns {Promise} Whether the file is valid + */ + async validateFile(file, { allowedTypes, maxSize }) { + if (!file) { + this.showNotification('No file selected', 'error'); + return false; + } + + if (!allowedTypes.includes(file.type)) { + this.showNotification( + `Unsupported file type. Please use: ${allowedTypes.join(', ')}`, + 'error' + ); + return false; + } + + if (file.size > maxSize) { + this.showNotification( + `File too large. Maximum size is ${this.formatFileSize(maxSize)}`, + 'error' + ); + return false; + } + + return true; + } + + /** + * Format file size in human-readable format + * @param {number} bytes - Size in bytes + * @returns {string} Formatted size + */ + formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; + } + + /** + * Debounce function calls + * @param {Function} func - Function to debounce + * @param {number} wait - Wait time in milliseconds + * @returns {Function} Debounced function + */ + debounce(func, wait) { + let timeout; + return (...args) => { + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(this, args), wait); + }; + } + + /** + * Add keyboard shortcut + * @param {string} key - Key to listen for + * @param {Function} callback - Function to call + * @param {Object} options - Options object + * @param {boolean} options.ctrl - Whether Ctrl key is required + * @param {boolean} options.alt - Whether Alt key is required + * @param {boolean} options.shift - Whether Shift key is required + */ + addKeyboardShortcut(key, callback, { ctrl = false, alt = false, shift = false } = {}) { + document.addEventListener('keydown', (e) => { + if ( + e.key.toLowerCase() === key.toLowerCase() && + e.ctrlKey === ctrl && + e.altKey === alt && + e.shiftKey === shift + ) { + e.preventDefault(); + callback.call(this); + } + }); + } + + /** + * Download a file + * @param {Blob} blob - File data + * @param {string} filename - Name for the downloaded file + */ + downloadFile(blob, filename) { + try { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + this.showNotification('File downloaded successfully', 'success'); + } catch (error) { + console.error('Error downloading file:', error); + this.showNotification('Failed to download file', 'error'); + } + } + + /** + * Load a script dynamically + * @param {string} src - Script URL + * @returns {Promise} Promise that resolves when script is loaded + */ + loadScript(src) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = src; + script.onload = resolve; + script.onerror = () => reject(new Error(`Failed to load script: ${src}`)); + document.head.appendChild(script); + }); + } + + /** + * Clean up resources + */ + destroy() { + // Clear notification timeout + if (this._notificationTimeout) { + clearTimeout(this._notificationTimeout); + } + + // Remove event listeners + if (this._boundEvents) { + this._boundEvents.forEach(({ element, type, listener }) => { + element.removeEventListener(type, listener); + }); + } + } +} \ No newline at end of file diff --git a/js/features/color-palette.js b/js/features/color-palette.js new file mode 100644 index 0000000..5c87557 --- /dev/null +++ b/js/features/color-palette.js @@ -0,0 +1,321 @@ +import { BaseTool } from './base-tool.js'; +import { notifications, modal } from '../utils/ui.js'; +import { STORAGE_KEYS, UI_CONSTANTS } from '../utils/constants.js'; +import utils from '../utils/helpers.js'; + +class ColorPalette extends BaseTool { + initializeElements() { + return { + colorPicker: document.querySelector('.color-picker-container canvas'), + paletteContainer: document.querySelector('.palette-container'), + selectedColors: document.querySelector('.selected-colors'), + harmonySelect: document.getElementById('harmony'), + generateButton: document.getElementById('generate-button'), + saveButton: document.getElementById('save-button'), + exportButton: document.getElementById('export-button'), + savedPalettes: document.querySelector('.saved-palette-grid'), + themeButton: document.getElementById('theme-button'), + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + currentColor: '#000000', + selectedColors: [], + maxColors: 5, + currentTheme: utils.getStorageItem(STORAGE_KEYS.THEME) || 'dark', + savedPalettes: utils.getStorageItem('saved-palettes') || [], + colorPickerContext: null + }; + } + + initializeColorPicker() { + const canvas = this.elements.colorPicker; + const ctx = canvas.getContext('2d'); + this.state.colorPickerContext = ctx; + + // Set canvas size + canvas.width = canvas.offsetWidth; + canvas.height = canvas.offsetHeight; + + // Create gradient + const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); + for (let i = 0; i <= 360; i += 60) { + gradient.addColorStop(i / 360, `hsl(${i}, 100%, 50%)`); + } + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + // Add black to white vertical gradient + const bwGradient = ctx.createLinearGradient(0, 0, 0, canvas.height); + bwGradient.addColorStop(0, 'rgba(255, 255, 255, 1)'); + bwGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0)'); + bwGradient.addColorStop(0.5, 'rgba(0, 0, 0, 0)'); + bwGradient.addColorStop(1, 'rgba(0, 0, 0, 1)'); + ctx.fillStyle = bwGradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + + getColorFromCanvas(x, y) { + const pixel = this.state.colorPickerContext.getImageData(x, y, 1, 1).data; + return `#${[...pixel].slice(0, 3).map(x => x.toString(16).padStart(2, '0')).join('')}`; + } + + addColor(color) { + if (this.state.selectedColors.length >= this.state.maxColors) { + notifications.warning('Maximum colors reached. Remove some to add more.'); + return; + } + + if (!this.state.selectedColors.includes(color)) { + this.state.selectedColors.push(color); + this.updateSelectedColors(); + notifications.success('Color added to palette'); + } + } + + removeColor(index) { + this.state.selectedColors.splice(index, 1); + this.updateSelectedColors(); + notifications.info('Color removed from palette'); + } + + updateSelectedColors() { + this.elements.selectedColors.innerHTML = this.state.selectedColors + .map((color, index) => ` +
+
+ `).join(''); + } + + generateHarmony() { + const harmony = this.elements.harmonySelect.value; + const baseColor = this.state.selectedColors[0]; + if (!baseColor) { + notifications.error('Please select a base color first.'); + return; + } + + const hsl = this.hexToHSL(baseColor); + let colors = [baseColor]; + + switch (harmony) { + case 'complementary': + colors.push(this.HSLToHex((hsl[0] + 180) % 360, hsl[1], hsl[2])); + break; + case 'analogous': + colors.push(this.HSLToHex((hsl[0] + 30) % 360, hsl[1], hsl[2])); + colors.push(this.HSLToHex((hsl[0] - 30 + 360) % 360, hsl[1], hsl[2])); + break; + case 'triadic': + colors.push(this.HSLToHex((hsl[0] + 120) % 360, hsl[1], hsl[2])); + colors.push(this.HSLToHex((hsl[0] + 240) % 360, hsl[1], hsl[2])); + break; + case 'split-complementary': + colors.push(this.HSLToHex((hsl[0] + 150) % 360, hsl[1], hsl[2])); + colors.push(this.HSLToHex((hsl[0] + 210) % 360, hsl[1], hsl[2])); + break; + case 'tetradic': + colors.push(this.HSLToHex((hsl[0] + 90) % 360, hsl[1], hsl[2])); + colors.push(this.HSLToHex((hsl[0] + 180) % 360, hsl[1], hsl[2])); + colors.push(this.HSLToHex((hsl[0] + 270) % 360, hsl[1], hsl[2])); + break; + } + + this.state.selectedColors = colors; + this.updateSelectedColors(); + notifications.success(`Generated ${harmony} color harmony`); + } + + hexToHSL(hex) { + let r = parseInt(hex.slice(1, 3), 16) / 255; + let g = parseInt(hex.slice(3, 5), 16) / 255; + let b = parseInt(hex.slice(5, 7), 16) / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h, s, l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h *= 60; + } + + return [Math.round(h), Math.round(s * 100), Math.round(l * 100)]; + } + + HSLToHex(h, s, l) { + s /= 100; + l /= 100; + const a = s * Math.min(l, 1 - l); + const f = n => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * color).toString(16).padStart(2, '0'); + }; + return `#${f(0)}${f(8)}${f(4)}`; + } + + savePalette() { + if (this.state.selectedColors.length === 0) { + notifications.error('Please select some colors first.'); + return; + } + + const palette = { + colors: [...this.state.selectedColors], + timestamp: Date.now() + }; + + this.state.savedPalettes.unshift(palette); + if (this.state.savedPalettes.length > UI_CONSTANTS.MAX_RECENT_FILES) { + this.state.savedPalettes.pop(); + } + + utils.setStorageItem('saved-palettes', this.state.savedPalettes); + this.updateSavedPalettes(); + notifications.success('Palette saved successfully!'); + } + + updateSavedPalettes() { + this.elements.savedPalettes.innerHTML = this.state.savedPalettes + .map((palette, index) => ` +
+
+ ${palette.colors.map(color => ` +
+
+ `).join('')} +
+
+ ${new Date(palette.timestamp).toLocaleDateString(utils.getBrowserLanguage())} +
+
+ `).join(''); + } + + async exportPalette(format = 'hex') { + if (this.state.selectedColors.length === 0) { + notifications.error('Please select some colors first.'); + return; + } + + let output = ''; + switch (format) { + case 'hex': + output = this.state.selectedColors.join(', '); + break; + case 'rgb': + output = this.state.selectedColors + .map(color => { + const r = parseInt(color.slice(1, 3), 16); + const g = parseInt(color.slice(3, 5), 16); + const b = parseInt(color.slice(5, 7), 16); + return `rgb(${r}, ${g}, ${b})`; + }) + .join(', '); + break; + case 'css': + output = `:root {\n${this.state.selectedColors + .map((color, i) => ` --color-${i + 1}: ${color};`) + .join('\n')}\n}`; + break; + } + + try { + await utils.copyToClipboard(output); + notifications.success(`Copied ${format.toUpperCase()} values to clipboard!`); + } catch (error) { + console.error('Error copying to clipboard:', error); + notifications.error('Failed to copy to clipboard. Please try again.'); + } + } + + bindEvents() { + // Color picker events + this.elements.colorPicker.addEventListener('click', + this.debounce((e) => { + const rect = e.target.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const color = this.getColorFromCanvas(x, y); + this.addColor(color); + }, UI_CONSTANTS.DEBOUNCE_DELAY) + ); + + // Selected colors events + this.elements.selectedColors.addEventListener('click', (e) => { + const colorElement = e.target.closest('.selected-color'); + if (colorElement) { + const index = parseInt(colorElement.dataset.index); + this.removeColor(index); + } + }); + + // Generate harmony + this.elements.generateButton.addEventListener('click', () => { + this.generateHarmony(); + }); + + // Save palette + this.elements.saveButton.addEventListener('click', () => { + this.savePalette(); + }); + + // Export options + this.elements.exportButton.addEventListener('click', (e) => { + const format = e.target.dataset.format || 'hex'; + this.exportPalette(format); + }); + + // Load saved palette + this.elements.savedPalettes.addEventListener('click', (e) => { + const palette = e.target.closest('.saved-palette'); + if (palette) { + const index = parseInt(palette.dataset.index); + this.state.selectedColors = [...this.state.savedPalettes[index].colors]; + this.updateSelectedColors(); + notifications.success('Palette loaded successfully'); + } + }); + + // Theme toggle + this.elements.themeButton.addEventListener('click', () => { + this.toggleTheme(STORAGE_KEYS.THEME); + }); + + // Window resize + window.addEventListener('resize', + this.debounce(() => this.initializeColorPicker(), UI_CONSTANTS.DEBOUNCE_DELAY) + ); + + // Keyboard shortcuts + this.addKeyboardShortcut('s', () => this.savePalette(), { ctrl: true }); + this.addKeyboardShortcut('e', () => this.exportPalette('hex'), { ctrl: true }); + this.addKeyboardShortcut('g', () => this.generateHarmony(), { ctrl: true }); + } + + initialize() { + document.documentElement.setAttribute('data-theme', this.state.currentTheme); + this.initializeColorPicker(); + this.updateSavedPalettes(); + } +} + +// Initialize the feature when the DOM is ready +document.addEventListener('DOMContentLoaded', () => { + new ColorPalette(); +}); \ No newline at end of file diff --git a/js/features/image-resizer.js b/js/features/image-resizer.js new file mode 100644 index 0000000..cd487f1 --- /dev/null +++ b/js/features/image-resizer.js @@ -0,0 +1,246 @@ +import { BaseTool } from './base-tool.js'; +import { notifications } from '../utils/ui.js'; +import { STORAGE_KEYS, FILE_LIMITS, UI_CONSTANTS } from '../utils/constants.js'; +import { fileValidation } from '../utils/validation.js'; +import utils from '../utils/helpers.js'; + +class ImageResizer extends BaseTool { + initializeElements() { + return { + container: document.querySelector('.image-container'), + preview: document.querySelector('.preview-container img'), + widthInput: document.getElementById('width'), + heightInput: document.getElementById('height'), + qualityInput: document.getElementById('quality'), + qualityValue: document.getElementById('quality-value'), + formatSelect: document.getElementById('format'), + aspectRatioLock: document.querySelector('.aspect-ratio-lock'), + downloadButton: document.getElementById('download-button'), + fileInfo: document.querySelector('.file-info'), + dropMessage: document.querySelector('.drop-message'), + themeButton: document.getElementById('theme-button'), + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + currentImage: null, + originalDimensions: { width: 0, height: 0 }, + aspectRatioLocked: true, + aspectRatio: 1, + currentTheme: utils.getStorageItem(STORAGE_KEYS.THEME) || 'dark', + supportedFormats: FILE_LIMITS.SUPPORTED_IMAGE_TYPES + }; + } + + updateFileInfo(file) { + const size = this.formatFileSize(file.size); + this.elements.fileInfo.textContent = `${file.name} - ${size}`; + } + + handleImageLoad(img) { + try { + // Validate image dimensions + if (img.naturalWidth > FILE_LIMITS.MAX_IMAGE_DIMENSION || + img.naturalHeight > FILE_LIMITS.MAX_IMAGE_DIMENSION) { + throw new Error(`Image dimensions exceed the maximum limit of ${FILE_LIMITS.MAX_IMAGE_DIMENSION}px`); + } + + this.state.originalDimensions = { + width: img.naturalWidth, + height: img.naturalHeight + }; + this.state.aspectRatio = img.naturalWidth / img.naturalHeight; + + this.elements.widthInput.value = img.naturalWidth; + this.elements.heightInput.value = img.naturalHeight; + this.elements.preview.src = img.src; + this.elements.container.classList.remove('drag-over'); + this.elements.downloadButton.disabled = false; + + notifications.success('Image loaded successfully'); + } catch (error) { + console.error('Error loading image:', error); + notifications.error(error.message || 'Failed to load image'); + this.resetState(); + } + } + + async loadImage(file) { + try { + // Validate file + await fileValidation.validateFileSize(file); + fileValidation.validateFileType(file, this.state.supportedFormats); + + this.state.currentImage = file; + this.updateFileInfo(file); + + const img = await utils.loadImage(URL.createObjectURL(file)); + this.handleImageLoad(img); + } catch (error) { + console.error('Error loading image:', error); + notifications.error(error.message || 'Failed to load image'); + this.resetState(); + } + } + + resetState() { + this.state.currentImage = null; + this.state.originalDimensions = { width: 0, height: 0 }; + this.state.aspectRatio = 1; + this.elements.preview.src = ''; + this.elements.fileInfo.textContent = ''; + this.elements.downloadButton.disabled = true; + this.elements.container.classList.remove('drag-over'); + } + + updateDimension(dimension, value) { + if (this.state.aspectRatioLocked) { + if (dimension === 'width') { + this.elements.heightInput.value = Math.round(value / this.state.aspectRatio); + } else { + this.elements.widthInput.value = Math.round(value * this.state.aspectRatio); + } + } + } + + toggleAspectRatio() { + this.state.aspectRatioLocked = !this.state.aspectRatioLocked; + this.elements.aspectRatioLock.classList.toggle('locked', this.state.aspectRatioLocked); + this.elements.aspectRatioLock.textContent = + this.state.aspectRatioLocked ? '🔒' : '🔓'; + + notifications.info( + this.state.aspectRatioLocked ? + 'Aspect ratio locked' : + 'Aspect ratio unlocked' + ); + } + + async resizeImage() { + if (!this.state.currentImage) return null; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const width = parseInt(this.elements.widthInput.value); + const height = parseInt(this.elements.heightInput.value); + + canvas.width = width; + canvas.height = height; + + const img = await utils.loadImage(this.elements.preview.src); + ctx.drawImage(img, 0, 0, width, height); + + const format = this.elements.formatSelect.value; + const quality = parseInt(this.elements.qualityInput.value) / 100; + + return new Promise((resolve) => { + canvas.toBlob( + (blob) => resolve(blob), + `image/${format}`, + format === 'jpeg' ? quality : undefined + ); + }); + } + + async downloadImage() { + try { + const blob = await this.resizeImage(); + if (!blob) { + notifications.error('No image to download'); + return; + } + + const format = this.elements.formatSelect.value; + const filename = this.state.currentImage.name.replace( + /\.[^/.]+$/, + `.${format}` + ); + + this.downloadFile(blob, filename); + notifications.success('Image downloaded successfully'); + } catch (error) { + console.error('Error downloading image:', error); + notifications.error('Failed to download image. Please try again.'); + } + } + + bindEvents() { + // Drag and drop events + const dragOverHandler = this.debounce((e) => { + e.preventDefault(); + this.elements.container.classList.add('drag-over'); + }, UI_CONSTANTS.DEBOUNCE_DELAY); + + this.elements.container.addEventListener('dragover', dragOverHandler); + + this.elements.container.addEventListener('dragleave', () => { + this.elements.container.classList.remove('drag-over'); + }); + + this.elements.container.addEventListener('drop', (e) => { + e.preventDefault(); + const file = e.dataTransfer.files[0]; + if (file) this.loadImage(file); + }); + + // File input event + this.elements.container.addEventListener('click', () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/*'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (file) this.loadImage(file); + }; + input.click(); + }); + + // Dimension inputs + this.elements.widthInput.addEventListener('input', + this.debounce((e) => this.updateDimension('width', parseInt(e.target.value)), + UI_CONSTANTS.DEBOUNCE_DELAY) + ); + + this.elements.heightInput.addEventListener('input', + this.debounce((e) => this.updateDimension('height', parseInt(e.target.value)), + UI_CONSTANTS.DEBOUNCE_DELAY) + ); + + // Quality slider + this.elements.qualityInput.addEventListener('input', (e) => { + this.elements.qualityValue.textContent = `${e.target.value}%`; + }); + + // Aspect ratio lock + this.elements.aspectRatioLock.addEventListener('click', () => { + this.toggleAspectRatio(); + }); + + // Download button + this.elements.downloadButton.addEventListener('click', () => { + this.downloadImage(); + }); + + // Theme toggle + this.elements.themeButton.addEventListener('click', () => { + this.toggleTheme(STORAGE_KEYS.THEME); + }); + + // Keyboard shortcuts + this.addKeyboardShortcut('s', () => this.downloadImage(), { ctrl: true }); + this.addKeyboardShortcut('l', () => this.toggleAspectRatio(), { ctrl: true }); + } + + initialize() { + document.documentElement.setAttribute('data-theme', this.state.currentTheme); + this.elements.aspectRatioLock.textContent = '🔒'; + this.elements.downloadButton.disabled = true; + } +} + +// Initialize the feature when the DOM is ready +document.addEventListener('DOMContentLoaded', () => { + new ImageResizer(); +}); \ No newline at end of file diff --git a/js/features/qr-code.js b/js/features/qr-code.js new file mode 100644 index 0000000..9e0ff08 --- /dev/null +++ b/js/features/qr-code.js @@ -0,0 +1,210 @@ +import { BaseTool } from './base-tool.js'; +import { notifications } from '../utils/ui.js'; +import { STORAGE_KEYS, UI_CONSTANTS } from '../utils/constants.js'; +import { inputValidation } from '../utils/validation.js'; +import utils from '../utils/helpers.js'; + +class QRCode extends BaseTool { + initializeElements() { + return { + textInput: document.getElementById('text-input'), + sizeInput: document.getElementById('size-input'), + errorCorrectionSelect: document.getElementById('error-correction-select'), + darkColorInput: document.getElementById('dark-color-input'), + lightColorInput: document.getElementById('light-color-input'), + generateButton: document.getElementById('generate-button'), + downloadButton: document.getElementById('download-button'), + qrContainer: document.getElementById('qr-container'), + themeButton: document.getElementById('theme-button'), + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + currentTheme: utils.getStorageItem(STORAGE_KEYS.THEME) || 'dark', + qrInstance: null, + defaultOptions: { + width: 256, + height: 256, + type: 'svg', + data: '', + margin: 1, + qrOptions: { + errorCorrectionLevel: 'H' + }, + imageOptions: { + hideBackgroundDots: true, + imageSize: 0.4, + margin: 0 + }, + dotsOptions: { + type: 'rounded', + color: '#000000', + gradient: null + }, + backgroundOptions: { + color: '#ffffff', + } + } + }; + } + + validateInput() { + try { + const text = this.elements.textInput.value.trim(); + inputValidation.validateRequired(text, 'Text or URL'); + + // If it looks like a URL, validate it + if (text.includes('://') || text.includes('www.')) { + inputValidation.validateURL(text); + } + + const size = parseInt(this.elements.sizeInput.value); + inputValidation.validateNumberRange(size, 128, 1024, 'QR Code size'); + + return true; + } catch (error) { + notifications.error(error.message); + return false; + } + } + + getQROptions() { + const size = parseInt(this.elements.sizeInput.value) || 256; + const errorLevel = this.elements.errorCorrectionSelect.value; + const darkColor = this.elements.darkColorInput.value; + const lightColor = this.elements.lightColorInput.value; + + return { + ...this.state.defaultOptions, + width: size, + height: size, + data: this.elements.textInput.value.trim(), + qrOptions: { + ...this.state.defaultOptions.qrOptions, + errorCorrectionLevel: errorLevel + }, + dotsOptions: { + ...this.state.defaultOptions.dotsOptions, + color: darkColor + }, + backgroundOptions: { + ...this.state.defaultOptions.backgroundOptions, + color: lightColor + } + }; + } + + async generateQRCode() { + if (!this.validateInput()) return; + + try { + const options = this.getQROptions(); + + // Clear previous QR code + this.elements.qrContainer.innerHTML = ''; + + // Generate new QR code + const qrCode = await QRCodeStyling.create(options); + this.state.qrInstance = qrCode; + + // Render QR code + await qrCode.append(this.elements.qrContainer); + + this.elements.downloadButton.disabled = false; + notifications.success('QR code generated successfully!'); + } catch (error) { + console.error('Error generating QR code:', error); + notifications.error('Error generating QR code. Please try again.'); + this.resetState(); + } + } + + resetState() { + this.state.qrInstance = null; + this.elements.qrContainer.innerHTML = ''; + this.elements.downloadButton.disabled = true; + } + + async downloadQRCode() { + if (!this.state.qrInstance) { + notifications.error('Please generate a QR code first.'); + return; + } + + try { + const text = this.elements.textInput.value.trim(); + const sanitizedText = text.replace(/[^a-z0-9]/gi, '_').toLowerCase(); + const fileName = `qr_${sanitizedText}_${new Date().toISOString().slice(0,10)}.png`; + + await this.state.qrInstance.download({ + extension: 'png', + name: fileName + }); + + notifications.success('QR code downloaded successfully!'); + } catch (error) { + console.error('Error downloading QR code:', error); + notifications.error('Error downloading QR code. Please try again.'); + } + } + + bindEvents() { + // Generate QR code + this.elements.generateButton.addEventListener('click', + this.debounce(() => this.generateQRCode(), UI_CONSTANTS.DEBOUNCE_DELAY) + ); + + // Auto-generate on input change + const inputElements = [ + this.elements.textInput, + this.elements.sizeInput, + this.elements.errorCorrectionSelect, + this.elements.darkColorInput, + this.elements.lightColorInput + ]; + + inputElements.forEach(element => { + element.addEventListener('input', + this.debounce(() => { + if (this.elements.textInput.value.trim()) { + this.generateQRCode(); + } + }, UI_CONSTANTS.DEBOUNCE_DELAY) + ); + }); + + // Download QR code + this.elements.downloadButton.addEventListener('click', () => { + this.downloadQRCode(); + }); + + // Theme toggle + this.elements.themeButton.addEventListener('click', () => { + this.toggleTheme(STORAGE_KEYS.THEME); + }); + + // Enter key in text input + this.elements.textInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + this.generateQRCode(); + } + }); + + // Keyboard shortcuts + this.addKeyboardShortcut('g', () => this.generateQRCode(), { ctrl: true }); + this.addKeyboardShortcut('s', () => this.downloadQRCode(), { ctrl: true }); + } + + initialize() { + document.documentElement.setAttribute('data-theme', this.state.currentTheme); + this.elements.downloadButton.disabled = true; + } +} + +// Initialize the feature when the DOM is ready +document.addEventListener('DOMContentLoaded', () => { + new QRCode(); +}); \ No newline at end of file diff --git a/js/features/text-to-speech.js b/js/features/text-to-speech.js new file mode 100644 index 0000000..bf3f2ad --- /dev/null +++ b/js/features/text-to-speech.js @@ -0,0 +1,255 @@ +import { BaseTool } from './base-tool.js'; +import { notifications } from '../utils/ui.js'; +import { STORAGE_KEYS, UI_CONSTANTS, KEYBOARD_SHORTCUTS } from '../utils/constants.js'; +import utils from '../utils/helpers.js'; + +class TextToSpeech extends BaseTool { + initializeElements() { + return { + textInput: document.getElementById('text-input'), + voiceSelect: document.getElementById('voice-select'), + speakButton: document.getElementById('speak-button'), + rateInput: document.getElementById('rate'), + pitchInput: document.getElementById('pitch'), + volumeInput: document.getElementById('volume'), + rateValue: document.getElementById('rate-value'), + pitchValue: document.getElementById('pitch-value'), + volumeValue: document.getElementById('volume-value'), + charCount: document.getElementById('char-count'), + progressContainer: document.querySelector('.progress-container'), + progress: document.querySelector('.progress'), + progressText: document.querySelector('.progress-text'), + historyList: document.getElementById('history-list'), + languageDetect: document.getElementById('language-detect').querySelector('span'), + themeButton: document.getElementById('theme-button'), + clearButton: document.getElementById('clear-button'), + saveTextButton: document.getElementById('save-text'), + previewButton: document.getElementById('preview-voice'), + downloadButton: document.getElementById('download-audio'), + notification: document.querySelector('.notification') + }; + } + + initializeState() { + return { + voices: [], + synthesis: window.speechSynthesis, + currentTheme: utils.getStorageItem(STORAGE_KEYS.THEME) || 'dark', + history: utils.getStorageItem('tts-history') || [], + templates: { + greeting: "Hello! How are you today?", + introduction: "My name is [Name] and I'm pleased to meet you.", + farewell: "Thank you for your time. Have a great day!" + }, + lngDetector: new LanguageDetector() + }; + } + + populateVoiceList() { + this.state.voices = this.state.synthesis.getVoices(); + this.elements.voiceSelect.innerHTML = this.state.voices + .map((voice, index) => ``) + .join(''); + } + + updateTextInfo() { + const text = this.elements.textInput.value; + this.elements.charCount.textContent = `(${text.length} characters)`; + + const detected = text.trim() ? this.state.lngDetector.detect(text, 1) : []; + this.elements.languageDetect.textContent = detected.length ? detected[0][0] : 'None'; + } + + addToHistory(text) { + if (!text.trim() || this.state.history.includes(text)) return; + + this.state.history.unshift(text); + if (this.state.history.length > UI_CONSTANTS.MAX_RECENT_FILES) { + this.state.history.pop(); + } + utils.setStorageItem('tts-history', this.state.history); + this.updateHistoryList(); + } + + updateHistoryList() { + this.elements.historyList.innerHTML = this.state.history + .map(text => { + const displayText = text.substring(0, 50) + (text.length > 50 ? '...' : ''); + return `
${utils.sanitizeHTML(displayText)}
`; + }) + .join(''); + + this.elements.historyList.querySelectorAll('.history-item').forEach((item, index) => { + item.addEventListener('click', () => { + this.elements.textInput.value = this.state.history[index]; + this.updateTextInfo(); + }); + }); + } + + createUtterance(text, isPreview = false) { + const utterance = new SpeechSynthesisUtterance( + isPreview ? "This is a preview of the selected voice." : text + ); + + const selectedVoice = this.state.voices[this.elements.voiceSelect.value]; + if (selectedVoice) utterance.voice = selectedVoice; + + utterance.rate = parseFloat(this.elements.rateInput.value); + utterance.pitch = parseFloat(this.elements.pitchInput.value); + utterance.volume = parseFloat(this.elements.volumeInput.value); + + return utterance; + } + + updateProgress(value) { + this.elements.progress.style.width = `${value}%`; + this.elements.progressText.textContent = `${Math.round(value)}%`; + } + + async speak(isPreview = false) { + if (this.state.synthesis.speaking) { + this.state.synthesis.cancel(); + return; + } + + const text = this.elements.textInput.value.trim(); + if (!text && !isPreview) { + notifications.error('Please enter some text to speak.'); + return; + } + + const utterance = this.createUtterance(text, isPreview); + + utterance.onstart = () => { + this.elements.speakButton.textContent = 'Stop'; + this.elements.speakButton.classList.add('speaking'); + if (!isPreview) { + this.elements.progressContainer.classList.remove('hidden'); + this.updateProgress(0); + } + }; + + utterance.onend = () => { + this.elements.speakButton.textContent = 'Speak'; + this.elements.speakButton.classList.remove('speaking'); + this.elements.progressContainer.classList.add('hidden'); + if (!isPreview) { + this.addToHistory(text); + notifications.success('Speech completed successfully'); + } + }; + + utterance.onboundary = (event) => { + if (!isPreview) { + this.updateProgress((event.charIndex / event.target.text.length) * 100); + } + }; + + utterance.onerror = (event) => { + console.error('SpeechSynthesis Error:', event); + this.elements.speakButton.textContent = 'Speak'; + this.elements.speakButton.classList.remove('speaking'); + this.elements.progressContainer.classList.add('hidden'); + notifications.error('An error occurred while speaking. Please try again.'); + }; + + this.state.synthesis.speak(utterance); + } + + async downloadAudio() { + const text = this.elements.textInput.value.trim(); + if (!text) { + notifications.error('Please enter some text to convert.'); + return; + } + + try { + const utterance = this.createUtterance(text); + const audioBlob = await new Promise((resolve, reject) => { + const audioChunks = []; + const mediaRecorder = new MediaRecorder( + new AudioContext().createMediaStreamDestination().stream + ); + + mediaRecorder.ondataavailable = (event) => audioChunks.push(event.data); + mediaRecorder.onstop = () => resolve(new Blob(audioChunks, { type: 'audio/wav' })); + + this.state.synthesis.speak(utterance); + mediaRecorder.start(); + utterance.onend = () => mediaRecorder.stop(); + }); + + const filename = `speech_${new Date().toISOString().slice(0,10)}.wav`; + this.downloadFile(audioBlob, filename); + notifications.success('Audio file downloaded successfully'); + } catch (error) { + console.error('Error downloading audio:', error); + notifications.error('Failed to download audio. Please try again.'); + } + } + + bindEvents() { + // Voice events + if (this.state.synthesis.onvoiceschanged !== undefined) { + this.state.synthesis.onvoiceschanged = () => this.populateVoiceList(); + } + + // Control events + this.elements.rateInput.addEventListener('input', () => { + this.elements.rateValue.textContent = `${this.elements.rateInput.value}x`; + }); + + this.elements.pitchInput.addEventListener('input', () => { + this.elements.pitchValue.textContent = this.elements.pitchInput.value; + }); + + this.elements.volumeInput.addEventListener('input', () => { + this.elements.volumeValue.textContent = + `${Math.round(this.elements.volumeInput.value * 100)}%`; + }); + + // Button events + this.elements.speakButton.addEventListener('click', () => this.speak()); + this.elements.previewButton.addEventListener('click', () => this.speak(true)); + this.elements.themeButton.addEventListener('click', () => this.toggleTheme(STORAGE_KEYS.THEME)); + this.elements.clearButton.addEventListener('click', () => { + this.elements.textInput.value = ''; + this.updateTextInfo(); + }); + this.elements.saveTextButton.addEventListener('click', () => { + const text = this.elements.textInput.value.trim(); + if (text) { + this.addToHistory(text); + notifications.success('Text saved to history'); + } + }); + this.elements.downloadButton.addEventListener('click', () => this.downloadAudio()); + + // Template buttons + document.querySelectorAll('[data-template]').forEach(button => { + button.addEventListener('click', () => { + this.elements.textInput.value = this.state.templates[button.dataset.template]; + this.updateTextInfo(); + }); + }); + + // Text input events + this.elements.textInput.addEventListener('input', this.debounce(() => this.updateTextInfo(), 300)); + + // Keyboard shortcuts + this.addKeyboardShortcut('Enter', () => this.speak(), { ctrl: true }); + } + + initialize() { + document.documentElement.setAttribute('data-theme', this.state.currentTheme); + this.populateVoiceList(); + this.updateTextInfo(); + this.updateHistoryList(); + } +} + +// Initialize the feature when the DOM is ready +document.addEventListener('DOMContentLoaded', () => { + new TextToSpeech(); +}); \ No newline at end of file diff --git a/js/image-resizer.js b/js/image-resizer.js deleted file mode 100644 index c7cae41..0000000 --- a/js/image-resizer.js +++ /dev/null @@ -1,46 +0,0 @@ -let cropper; -const fileInput = document.getElementById('file-input'); -const imageContainer = document.getElementById('image-container'); -const resizeButton = document.getElementById('resize-button'); -const downloadLink = document.getElementById('download-link'); -const widthInput = document.getElementById('width'); -const heightInput = document.getElementById('height'); - -fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - imageContainer.innerHTML = ``; - const image = document.getElementById('image'); - cropper = new Cropper(image, { - aspectRatio: NaN, - viewMode: 1, - }); - }; - reader.readAsDataURL(file); - } -}); - -resizeButton.addEventListener('click', () => { - if (cropper) { - resizeButton.disabled = true; - resizeButton.textContent = 'Processing...'; - - const width = parseInt(widthInput.value); - const height = parseInt(heightInput.value); - const canvas = cropper.getCroppedCanvas({ - width: width, - height: height - }); - - canvas.toBlob((blob) => { - const url = URL.createObjectURL(blob); - downloadLink.href = url; - downloadLink.download = 'resized-image.png'; - downloadLink.style.display = 'inline-block'; - resizeButton.disabled = false; - resizeButton.textContent = 'Resize Image'; - }); - } -}); diff --git a/js/qr-generator.js b/js/qr-generator.js deleted file mode 100644 index 1556cd8..0000000 --- a/js/qr-generator.js +++ /dev/null @@ -1,48 +0,0 @@ -const qrInput = document.getElementById('qr-input'); -const qrSize = document.getElementById('qr-size'); -const qrCorrection = document.getElementById('qr-correction'); -const qrColor = document.getElementById('qr-color'); -const qrBgColor = document.getElementById('qr-bg-color'); -const qrRounded = document.getElementById('qr-rounded'); -const generateButton = document.getElementById('generate-button'); -const qrOutput = document.getElementById('qr-output'); -const downloadLink = document.getElementById('download-link'); - -let qr = null; - -generateButton.addEventListener('click', () => { - const inputText = qrInput.value; - if (inputText) { - qrOutput.innerHTML = ''; // Clear previous QR code - - qr = new QRCode(qrOutput, { - text: inputText, - width: parseInt(qrSize.value), - height: parseInt(qrSize.value), - colorDark: qrColor.value, - colorLight: qrBgColor.value, - correctLevel: QRCode.CorrectLevel[qrCorrection.value] - }); - - // Apply rounded corners if selected - if (qrRounded.checked) { - setTimeout(() => { - const qrImage = qrOutput.querySelector('img'); - qrImage.style.borderRadius = '15px'; - }, 50); - } - - // Enable download after a short delay to ensure QR code is generated - setTimeout(() => { - const qrImage = qrOutput.querySelector('img'); - downloadLink.href = qrImage.src; - downloadLink.download = 'qrcode.png'; - downloadLink.style.display = 'inline-block'; - }, 100); - } -}); - -// Update QR code in real-time as options change -[qrSize, qrCorrection, qrColor, qrBgColor, qrRounded].forEach(element => { - element.addEventListener('change', () => generateButton.click()); -}); diff --git a/js/text-to-speech.js b/js/text-to-speech.js deleted file mode 100644 index 9752563..0000000 --- a/js/text-to-speech.js +++ /dev/null @@ -1,84 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const textInput = document.getElementById('text-input'); - const voiceSelect = document.getElementById('voice-select'); - const speakButton = document.getElementById('speak-button'); - let voices = []; - let synthesis = window.speechSynthesis; - - // Function to populate voice list - function populateVoiceList() { - voices = synthesis.getVoices(); - voiceSelect.innerHTML = ''; - - voices.forEach((voice, index) => { - const option = document.createElement('option'); - option.textContent = `${voice.name} (${voice.lang})`; - option.value = index; - voiceSelect.appendChild(option); - }); - } - - // Initialize voices - populateVoiceList(); - if (speechSynthesis.onvoiceschanged !== undefined) { - speechSynthesis.onvoiceschanged = populateVoiceList; - } - - // Speak function - function speak() { - if (synthesis.speaking) { - synthesis.cancel(); - } - - const text = textInput.value.trim(); - if (!text) { - alert('Please enter some text to speak.'); - return; - } - - const utterance = new SpeechSynthesisUtterance(text); - const selectedVoice = voices[voiceSelect.value]; - if (selectedVoice) { - utterance.voice = selectedVoice; - } - - // Add event handlers - utterance.onstart = () => { - speakButton.textContent = 'Stop'; - speakButton.classList.add('speaking'); - }; - - utterance.onend = () => { - speakButton.textContent = 'Speak'; - speakButton.classList.remove('speaking'); - }; - - utterance.onerror = (event) => { - console.error('SpeechSynthesis Error:', event); - speakButton.textContent = 'Speak'; - speakButton.classList.remove('speaking'); - }; - - synthesis.speak(utterance); - } - - // Event listeners - speakButton.addEventListener('click', () => { - if (synthesis.speaking) { - synthesis.cancel(); - speakButton.textContent = 'Speak'; - speakButton.classList.remove('speaking'); - } else { - speak(); - } - }); - - // Add keyboard shortcuts - textInput.addEventListener('keydown', (e) => { - // Ctrl/Cmd + Enter to speak - if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { - e.preventDefault(); - speak(); - } - }); -}); diff --git a/js/utils/constants.js b/js/utils/constants.js new file mode 100644 index 0000000..a3bec7c --- /dev/null +++ b/js/utils/constants.js @@ -0,0 +1,98 @@ +/** + * Constants and configuration values for Digital Services Hub + */ + +export const APP_CONFIG = { + NAME: 'Digital Services Hub', + VERSION: '1.0.0', + AUTHOR: 'Digital Services Team', + GITHUB_URL: 'https://github.com/yourusername/digital-services-hub' +}; + +export const STORAGE_KEYS = { + THEME: 'ds_hub_theme', + LANGUAGE: 'ds_hub_language', + USER_PREFERENCES: 'ds_hub_preferences', + RECENT_FILES: 'ds_hub_recent_files' +}; + +export const THEMES = { + LIGHT: 'light', + DARK: 'dark', + SYSTEM: 'system' +}; + +export const FILE_LIMITS = { + MAX_FILE_SIZE: 10 * 1024 * 1024, // 10MB + MAX_IMAGE_DIMENSION: 4096, + SUPPORTED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/gif'], + SUPPORTED_TEXT_TYPES: ['text/plain', 'text/html', 'text/css', 'text/javascript'] +}; + +export const UI_CONSTANTS = { + NOTIFICATION_DURATION: 3000, + MAX_RECENT_FILES: 10, + DEBOUNCE_DELAY: 300, + MOBILE_BREAKPOINT: 768 +}; + +export const ERROR_MESSAGES = { + FILE_TOO_LARGE: 'File size exceeds the maximum limit of 10MB', + UNSUPPORTED_FILE_TYPE: 'File type is not supported', + INVALID_DIMENSIONS: 'Image dimensions exceed the maximum limit', + NETWORK_ERROR: 'Network error occurred. Please check your connection', + STORAGE_ERROR: 'Error accessing local storage', + GENERIC_ERROR: 'An unexpected error occurred' +}; + +export const API_ENDPOINTS = { + BASE_URL: 'https://api.example.com', + ROUTES: { + AUTH: '/auth', + FILES: '/files', + CONVERT: '/convert', + GENERATE: '/generate' + } +}; + +export const KEYBOARD_SHORTCUTS = { + SAVE: { + key: 's', + ctrl: true, + description: 'Save current work' + }, + UNDO: { + key: 'z', + ctrl: true, + description: 'Undo last action' + }, + REDO: { + key: 'y', + ctrl: true, + description: 'Redo last action' + }, + TOGGLE_THEME: { + key: 't', + ctrl: true, + shift: true, + description: 'Toggle dark/light theme' + } +}; + +export const ACCESSIBILITY = { + ARIA_LABELS: { + MAIN_NAVIGATION: 'Main navigation', + THEME_TOGGLE: 'Toggle theme', + LANGUAGE_SELECTOR: 'Select language', + FILE_UPLOAD: 'Upload file', + SETTINGS_MENU: 'Settings menu' + }, + ROLES: { + MAIN: 'main', + NAVIGATION: 'navigation', + BUTTON: 'button', + MENU: 'menu', + MENUITEM: 'menuitem', + DIALOG: 'dialog' + } +}; \ No newline at end of file diff --git a/js/utils/helpers.js b/js/utils/helpers.js new file mode 100644 index 0000000..0fdfc0e --- /dev/null +++ b/js/utils/helpers.js @@ -0,0 +1,217 @@ +/** + * Utility functions for Digital Services Hub + */ + +const utils = { + /** + * Sanitize HTML string to prevent XSS + * @param {string} html - HTML string to sanitize + * @returns {string} Sanitized HTML + */ + sanitizeHTML(html) { + const div = document.createElement('div'); + div.textContent = html; + return div.innerHTML; + }, + + /** + * Validate email address + * @param {string} email - Email to validate + * @returns {boolean} Whether email is valid + */ + isValidEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return re.test(String(email).toLowerCase()); + }, + + /** + * Generate a unique ID + * @returns {string} Unique ID + */ + generateUID() { + return Date.now().toString(36) + Math.random().toString(36).substr(2); + }, + + /** + * Deep clone an object + * @param {Object} obj - Object to clone + * @returns {Object} Cloned object + */ + deepClone(obj) { + if (obj === null || typeof obj !== 'object') return obj; + if (obj instanceof Date) return new Date(obj); + if (obj instanceof Array) return obj.map(item => this.deepClone(item)); + if (obj instanceof Object) { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, this.deepClone(value)]) + ); + } + throw new Error(`Unable to clone object of type ${typeof obj}`); + }, + + /** + * Check if running in mobile browser + * @returns {boolean} Whether browser is mobile + */ + isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); + }, + + /** + * Get browser language + * @returns {string} Browser language code + */ + getBrowserLanguage() { + return navigator.language || navigator.userLanguage; + }, + + /** + * Format date to locale string + * @param {Date|string|number} date - Date to format + * @param {Object} options - Intl.DateTimeFormat options + * @returns {string} Formatted date + */ + formatDate(date, options = {}) { + const d = new Date(date); + return d.toLocaleDateString(this.getBrowserLanguage(), options); + }, + + /** + * Check if storage is available + * @param {string} type - Storage type ('localStorage' or 'sessionStorage') + * @returns {boolean} Whether storage is available + */ + isStorageAvailable(type) { + try { + const storage = window[type]; + const x = '__storage_test__'; + storage.setItem(x, x); + storage.removeItem(x); + return true; + } catch (e) { + return false; + } + }, + + /** + * Safe storage getter + * @param {string} key - Storage key + * @param {string} type - Storage type ('local' or 'session') + * @returns {any} Stored value or null + */ + getStorageItem(key, type = 'local') { + try { + const storage = type === 'local' ? localStorage : sessionStorage; + const item = storage.getItem(key); + return item ? JSON.parse(item) : null; + } catch (error) { + console.error(`Error reading from ${type}Storage:`, error); + return null; + } + }, + + /** + * Safe storage setter + * @param {string} key - Storage key + * @param {any} value - Value to store + * @param {string} type - Storage type ('local' or 'session') + * @returns {boolean} Whether operation was successful + */ + setStorageItem(key, value, type = 'local') { + try { + const storage = type === 'local' ? localStorage : sessionStorage; + storage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) { + console.error(`Error writing to ${type}Storage:`, error); + return false; + } + }, + + /** + * Load image as Promise + * @param {string} src - Image URL + * @returns {Promise} Loaded image + */ + loadImage(src) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => resolve(img); + img.onerror = reject; + img.src = src; + }); + }, + + /** + * Check if element is in viewport + * @param {HTMLElement} element - Element to check + * @param {number} [offset=0] - Offset from viewport edges + * @returns {boolean} Whether element is in viewport + */ + isInViewport(element, offset = 0) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 - offset && + rect.left >= 0 - offset && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + offset && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + offset + ); + }, + + /** + * Copy text to clipboard + * @param {string} text - Text to copy + * @returns {Promise} + */ + async copyToClipboard(text) { + if (navigator.clipboard) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch (error) { + console.error('Failed to copy using Clipboard API:', error); + } + } + + // Fallback + try { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + return true; + } catch (error) { + console.error('Failed to copy using fallback:', error); + return false; + } + }, + + /** + * Detect file type from array buffer + * @param {ArrayBuffer} buffer - File data + * @returns {string|null} MIME type or null + */ + detectFileType(buffer) { + const arr = new Uint8Array(buffer).subarray(0, 4); + let header = ''; + for (let i = 0; i < arr.length; i++) { + header += arr[i].toString(16); + } + + switch (header) { + case '89504e47': return 'image/png'; + case '47494638': return 'image/gif'; + case 'ffd8ffe0': + case 'ffd8ffe1': + case 'ffd8ffe2': return 'image/jpeg'; + default: return null; + } + } +}; + +// Export for ES modules +export default utils; \ No newline at end of file diff --git a/js/utils/ui.js b/js/utils/ui.js new file mode 100644 index 0000000..c47e94b --- /dev/null +++ b/js/utils/ui.js @@ -0,0 +1,259 @@ +/** + * UI utilities for Digital Services Hub + */ + +import { UI_CONSTANTS, THEMES } from './constants.js'; +import utils from './helpers.js'; + +export const notifications = { + /** + * Show notification message + * @param {string} message - Message to display + * @param {string} type - Notification type ('success', 'error', 'info', 'warning') + * @param {number} [duration] - Duration in milliseconds + */ + show(message, type = 'info', duration = UI_CONSTANTS.NOTIFICATION_DURATION) { + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + notification.textContent = message; + notification.setAttribute('role', 'alert'); + + document.body.appendChild(notification); + + // Trigger animation + requestAnimationFrame(() => { + notification.classList.add('show'); + }); + + // Remove notification after duration + setTimeout(() => { + notification.classList.remove('show'); + notification.addEventListener('transitionend', () => { + notification.remove(); + }); + }, duration); + }, + + /** + * Show success notification + * @param {string} message - Success message + */ + success(message) { + this.show(message, 'success'); + }, + + /** + * Show error notification + * @param {string} message - Error message + */ + error(message) { + this.show(message, 'error'); + }, + + /** + * Show warning notification + * @param {string} message - Warning message + */ + warning(message) { + this.show(message, 'warning'); + } +}; + +export const themeManager = { + /** + * Initialize theme manager + */ + init() { + const savedTheme = utils.getStorageItem('theme', 'local') || THEMES.SYSTEM; + this.setTheme(savedTheme); + this.setupThemeToggle(); + this.setupSystemThemeListener(); + }, + + /** + * Set theme + * @param {string} theme - Theme to set + */ + setTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + utils.setStorageItem('theme', theme, 'local'); + }, + + /** + * Setup theme toggle button + */ + setupThemeToggle() { + const themeToggle = document.getElementById('theme-toggle'); + if (themeToggle) { + themeToggle.addEventListener('click', () => { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === THEMES.LIGHT ? THEMES.DARK : THEMES.LIGHT; + this.setTheme(newTheme); + }); + } + }, + + /** + * Setup system theme listener + */ + setupSystemThemeListener() { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + mediaQuery.addEventListener('change', (e) => { + if (document.documentElement.getAttribute('data-theme') === THEMES.SYSTEM) { + this.setTheme(e.matches ? THEMES.DARK : THEMES.LIGHT); + } + }); + } +}; + +export const modal = { + /** + * Show modal + * @param {Object} options - Modal options + * @param {string} options.title - Modal title + * @param {string|HTMLElement} options.content - Modal content + * @param {Object} [options.buttons] - Modal buttons configuration + * @returns {Promise} Resolves when modal is closed + */ + show({ title, content, buttons = {} }) { + return new Promise((resolve) => { + const modal = document.createElement('div'); + modal.className = 'modal'; + modal.setAttribute('role', 'dialog'); + modal.setAttribute('aria-modal', 'true'); + modal.setAttribute('aria-labelledby', 'modal-title'); + + const modalContent = document.createElement('div'); + modalContent.className = 'modal-content'; + + const modalHeader = document.createElement('div'); + modalHeader.className = 'modal-header'; + + const titleElement = document.createElement('h2'); + titleElement.id = 'modal-title'; + titleElement.textContent = title; + modalHeader.appendChild(titleElement); + + const closeButton = document.createElement('button'); + closeButton.className = 'modal-close'; + closeButton.innerHTML = '×'; + closeButton.setAttribute('aria-label', 'Close modal'); + modalHeader.appendChild(closeButton); + + const modalBody = document.createElement('div'); + modalBody.className = 'modal-body'; + if (typeof content === 'string') { + modalBody.innerHTML = content; + } else { + modalBody.appendChild(content); + } + + const modalFooter = document.createElement('div'); + modalFooter.className = 'modal-footer'; + + Object.entries(buttons).forEach(([label, callback]) => { + const button = document.createElement('button'); + button.textContent = label; + button.addEventListener('click', () => { + callback(); + this.close(modal); + resolve(); + }); + modalFooter.appendChild(button); + }); + + modalContent.appendChild(modalHeader); + modalContent.appendChild(modalBody); + modalContent.appendChild(modalFooter); + modal.appendChild(modalContent); + + const closeModal = () => { + this.close(modal); + resolve(); + }; + + closeButton.addEventListener('click', closeModal); + modal.addEventListener('click', (e) => { + if (e.target === modal) closeModal(); + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeModal(); + }); + + document.body.appendChild(modal); + requestAnimationFrame(() => { + modal.classList.add('show'); + }); + }); + }, + + /** + * Close modal + * @param {HTMLElement} modal - Modal element to close + */ + close(modal) { + modal.classList.remove('show'); + modal.addEventListener('transitionend', () => { + modal.remove(); + }); + } +}; + +export const loader = { + /** + * Show loader + * @param {string} [message] - Loading message + * @returns {HTMLElement} Loader element + */ + show(message = 'Loading...') { + const loader = document.createElement('div'); + loader.className = 'loader'; + loader.setAttribute('role', 'alert'); + loader.setAttribute('aria-busy', 'true'); + + const spinner = document.createElement('div'); + spinner.className = 'loader-spinner'; + + const messageElement = document.createElement('div'); + messageElement.className = 'loader-message'; + messageElement.textContent = message; + + loader.appendChild(spinner); + loader.appendChild(messageElement); + document.body.appendChild(loader); + + return loader; + }, + + /** + * Hide loader + * @param {HTMLElement} loader - Loader element to hide + */ + hide(loader) { + if (loader && loader.parentNode) { + loader.remove(); + } + } +}; + +export const responsiveHelper = { + /** + * Check if viewport is mobile + * @returns {boolean} Whether viewport is mobile + */ + isMobile() { + return window.innerWidth < UI_CONSTANTS.MOBILE_BREAKPOINT; + }, + + /** + * Add resize listener + * @param {Function} callback - Callback function + * @returns {Function} Cleanup function + */ + addResizeListener(callback) { + const debouncedCallback = utils.debounce(callback, UI_CONSTANTS.DEBOUNCE_DELAY); + window.addEventListener('resize', debouncedCallback); + return () => window.removeEventListener('resize', debouncedCallback); + } +}; \ No newline at end of file diff --git a/js/utils/validation.js b/js/utils/validation.js new file mode 100644 index 0000000..6202600 --- /dev/null +++ b/js/utils/validation.js @@ -0,0 +1,178 @@ +/** + * Validation utilities for Digital Services Hub + */ + +import { FILE_LIMITS, ERROR_MESSAGES } from './constants.js'; + +class ValidationError extends Error { + constructor(message, field = null) { + super(message); + this.name = 'ValidationError'; + this.field = field; + } +} + +export const fileValidation = { + /** + * Validate file size + * @param {File} file - File to validate + * @throws {ValidationError} If file size exceeds limit + */ + validateFileSize(file) { + if (file.size > FILE_LIMITS.MAX_FILE_SIZE) { + throw new ValidationError(ERROR_MESSAGES.FILE_TOO_LARGE, 'file'); + } + }, + + /** + * Validate file type + * @param {File} file - File to validate + * @param {Array} allowedTypes - Array of allowed MIME types + * @throws {ValidationError} If file type is not supported + */ + validateFileType(file, allowedTypes) { + if (!allowedTypes.includes(file.type)) { + throw new ValidationError(ERROR_MESSAGES.UNSUPPORTED_FILE_TYPE, 'file'); + } + }, + + /** + * Validate image dimensions + * @param {File} imageFile - Image file to validate + * @returns {Promise} + * @throws {ValidationError} If image dimensions exceed limits + */ + async validateImageDimensions(imageFile) { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + URL.revokeObjectURL(img.src); + if (img.width > FILE_LIMITS.MAX_IMAGE_DIMENSION || + img.height > FILE_LIMITS.MAX_IMAGE_DIMENSION) { + reject(new ValidationError(ERROR_MESSAGES.INVALID_DIMENSIONS, 'image')); + } + resolve(); + }; + img.onerror = () => { + URL.revokeObjectURL(img.src); + reject(new ValidationError('Failed to load image for validation', 'image')); + }; + img.src = URL.createObjectURL(imageFile); + }); + } +}; + +export const inputValidation = { + /** + * Validate required field + * @param {string} value - Field value + * @param {string} fieldName - Name of the field + * @throws {ValidationError} If field is empty + */ + validateRequired(value, fieldName) { + if (!value || value.trim() === '') { + throw new ValidationError(`${fieldName} is required`, fieldName); + } + }, + + /** + * Validate email format + * @param {string} email - Email to validate + * @throws {ValidationError} If email format is invalid + */ + validateEmail(email) { + const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!re.test(String(email).toLowerCase())) { + throw new ValidationError('Invalid email format', 'email'); + } + }, + + /** + * Validate URL format + * @param {string} url - URL to validate + * @throws {ValidationError} If URL format is invalid + */ + validateURL(url) { + try { + new URL(url); + } catch { + throw new ValidationError('Invalid URL format', 'url'); + } + }, + + /** + * Validate number range + * @param {number} value - Number to validate + * @param {number} min - Minimum value + * @param {number} max - Maximum value + * @param {string} fieldName - Name of the field + * @throws {ValidationError} If number is out of range + */ + validateNumberRange(value, min, max, fieldName) { + const num = Number(value); + if (isNaN(num)) { + throw new ValidationError(`${fieldName} must be a number`, fieldName); + } + if (num < min || num > max) { + throw new ValidationError( + `${fieldName} must be between ${min} and ${max}`, + fieldName + ); + } + }, + + /** + * Validate string length + * @param {string} value - String to validate + * @param {number} minLength - Minimum length + * @param {number} maxLength - Maximum length + * @param {string} fieldName - Name of the field + * @throws {ValidationError} If string length is out of range + */ + validateStringLength(value, minLength, maxLength, fieldName) { + if (value.length < minLength || value.length > maxLength) { + throw new ValidationError( + `${fieldName} must be between ${minLength} and ${maxLength} characters`, + fieldName + ); + } + } +}; + +export const errorHandler = { + /** + * Handle validation errors + * @param {Error} error - Error to handle + * @returns {Object} Formatted error object + */ + handleValidationError(error) { + if (error instanceof ValidationError) { + return { + type: 'validation', + message: error.message, + field: error.field + }; + } + return { + type: 'error', + message: ERROR_MESSAGES.GENERIC_ERROR + }; + }, + + /** + * Format validation errors for display + * @param {Object} errors - Object containing validation errors + * @returns {string} Formatted error message + */ + formatValidationErrors(errors) { + if (!errors || typeof errors !== 'object') { + return ERROR_MESSAGES.GENERIC_ERROR; + } + + return Object.entries(errors) + .map(([field, message]) => `${field}: ${message}`) + .join('\n'); + } +}; + +export { ValidationError }; \ No newline at end of file diff --git a/pages/about.html b/pages/about.html index 5e3876b..a6d3cf1 100644 --- a/pages/about.html +++ b/pages/about.html @@ -5,81 +5,91 @@ About - Digital Services Hub - +

About Digital Services Hub

- +
-

Welcome to the Digital Services Hub - your gateway to cutting-edge digital tools and AI-powered services!

- -

Our Mission

-

We strive to provide innovative, user-friendly digital tools that empower creators, developers, and everyday users to unleash their creativity and productivity in the digital realm.

+
+ +
-

Current Services

-
    -
  • Futuristic Image Resizer: Resize your images with style
  • -
  • Color Palette Generator: Extract beautiful color schemes from images
  • -
  • ASCII Art Converter: Transform text into ASCII masterpieces
  • -
  • QR Code Generator: Create custom QR codes for various purposes
  • -
+
+
+

Welcome to Digital Services Hub

+

+ Digital Services Hub is a collection of powerful web-based tools designed to help you with various digital tasks. + Our tools are built with modern web technologies and focus on providing a seamless, user-friendly experience. +

+
-

Upcoming Features

-

We're constantly innovating! Here's a sneak peek at some exciting services we're working on:

-
    -
  • AI Text Summarizer (Coming Soon!)
  • -
  • Sentiment Analysis Tool (Coming Soon!)
  • -
  • AI-powered Image Enhancement (Coming Soon!)
  • -
  • Virtual Background Generator (Coming Soon!)
  • -
  • Text-to-Speech Converter (Coming Soon!)
  • -
-

And many more AI-integrated and high-tech services on the horizon!

+
+

Our Tools

+
+
+
🗣️
+

Text to Speech

+

Convert text to natural-sounding speech with multiple voices and languages.

+
-

Our Commitment

-

At Digital Services Hub, we are committed to:

-
    -
  • Providing free, accessible tools for all users
  • -
  • Continuously updating and improving our services
  • -
  • Maintaining a user-friendly, futuristic interface
  • -
  • Protecting user privacy and data security
  • -
+
+
🖼️
+

Image Resizer

+

Resize and optimize images with aspect ratio preservation and format conversion.

+
-

Get Involved

-

We welcome feedback, suggestions, and contributions from our community. If you have ideas for new features or improvements, please visit our GitHub repository and open an issue or submit a pull request.

+
+
🎨
+

Color Palette

+

Generate and customize color palettes with harmony rules and export options.

+
-

Thank you for choosing Digital Services Hub. Together, let's shape the future of digital creativity and productivity!

+
+
🎯
+

ASCII Art

+

Convert images into ASCII art with customizable settings and color support.

+
+ +
+
📱
+

QR Code

+

Generate customizable QR codes with error correction and styling options.

+
+
+
+ +
+

Technologies

+

Built with modern web technologies:

+
    +
  • HTML5 & CSS3
  • +
  • Modern JavaScript (ES6+)
  • +
  • Web Speech API
  • +
  • Canvas API
  • +
  • File API
  • +
  • Local Storage
  • +
+
+ +
+

Project Information

+
+

+ This project is open-source and continuously improving. Feel free to contribute or report issues + through our repository. +

+
+
+ + + diff --git a/pages/ascii-art.html b/pages/ascii-art.html index d1afd69..c503f03 100644 --- a/pages/ascii-art.html +++ b/pages/ascii-art.html @@ -5,7 +5,7 @@ ASCII Art Converter - Digital Services Hub - +
@@ -14,22 +14,78 @@

ASCII Art Converter

-
- - +
+
- - -
-

-            
+
+
+ + +
+

Drop an image here or click to upload

+

Supports: JPG, PNG, GIF

+
+
+ + + +
+
+ + +
+ +
+ + +
+ +
+ +
-

Enter your text and click 'Convert to ASCII Art' to see the result.

+
+ +
+
+ + + +
+

+                    
+                    
+ + +
+
+
+ + - + diff --git a/pages/color-palette.html b/pages/color-palette.html index 8b0d43d..6ed46ab 100644 --- a/pages/color-palette.html +++ b/pages/color-palette.html @@ -5,39 +5,58 @@ Color Palette Generator - Digital Services Hub - +

Color Palette Generator

- +
-
- - +
+
-
- -
+
+
+ +
- +
+ + + +
+
+ +
+ +
+ +
+ + + +
+
-

Upload an image and click 'Generate Palette' to extract colors.

+
+

Saved Palettes

+
+
+
+ - + diff --git a/pages/image-resizer.html b/pages/image-resizer.html index 8f3d864..3c933db 100644 --- a/pages/image-resizer.html +++ b/pages/image-resizer.html @@ -3,52 +3,69 @@ - Futuristic Image Resizer - Digital Services Hub + Image Resizer - Digital Services Hub - - - +
-

Futuristic Image Resizer

+

Image Resizer

- +
-
- - +
+
- -
- -
- - + +
+
+ Drop image here or click to upload +
+
+ Preview +
+ +
+
+
- -
- - + +
+
+
+
+ + +
+ 🔒 +
+ + +
+
+
+ +
+ + +
+ +
+ + +
- - - -

Click 'Resize Image' to process, then 'Download' to save your image.

+
- - + diff --git a/pages/qr-code.html b/pages/qr-code.html new file mode 100644 index 0000000..36addd4 --- /dev/null +++ b/pages/qr-code.html @@ -0,0 +1,99 @@ + + + + + + QR Code Generator - Digital Services Hub + + + + + +
+

QR Code Generator

+ + + +
+
+ +
+ +
+
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + +
+ + +
+ +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/pages/qr-generator.html b/pages/qr-generator.html deleted file mode 100644 index b4ba03a..0000000 --- a/pages/qr-generator.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - QR Code Generator - Digital Services Hub - - - - - -
-

QR Code Generator

- - - -
-
- - -
- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- - - -
- - - -

Customize your QR code options and click 'Generate QR Code' to create.

-
-
- - - - - diff --git a/pages/text-to-speech.html b/pages/text-to-speech.html index c8b007a..07f8de3 100644 --- a/pages/text-to-speech.html +++ b/pages/text-to-speech.html @@ -14,20 +14,74 @@

Text-to-Speech Converter

+
+ +
+
- +
+ +
+ + +
+
+
Detected Language: None
-
- - +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + + +
+ + + +
+

Recent Texts

+
+
+ +
+

Templates

+
+ + + +
- -
- + + From f4f29b27cf9ee1395cbe1bcd25f859b71e358d32 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Tue, 24 Dec 2024 14:58:01 -0500 Subject: [PATCH 067/113] Remove unused config.yml file --- config.yml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 config.yml diff --git a/config.yml b/config.yml deleted file mode 100644 index 95b8bd5..0000000 --- a/config.yml +++ /dev/null @@ -1,7 +0,0 @@ -theme: jekyll-theme-cayman - -navigation: - - title: "Home" - url: README.md - - title: "Future Features" - url: FUTURE-FEATURES.md From 0c4c2c0e58ef400a70b5301cc349ec24576fe80a Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Tue, 24 Dec 2024 15:08:11 -0500 Subject: [PATCH 068/113] Refactor QR code component styles --- css/components/image-resizer.css | 303 ++++++++++++++++++++------- css/components/qr-code.css | 152 +++++++++----- css/styles.css | 340 ++++++++++++++++++++++--------- index.html | 152 ++++++++++++-- js/features/image-resizer.js | 338 +++++++++++++++++------------- js/features/qr-code.js | 111 +++++++--- pages/image-resizer.html | 125 ++++++++---- pages/qr-code.html | 157 +++++++------- 8 files changed, 1149 insertions(+), 529 deletions(-) diff --git a/css/components/image-resizer.css b/css/components/image-resizer.css index 60fdfe3..d57be7d 100644 --- a/css/components/image-resizer.css +++ b/css/components/image-resizer.css @@ -1,123 +1,284 @@ -.image-container { - width: 100%; - min-height: 200px; - border: 2px dashed var(--accent-color); +.resizer-controls { + background: var(--card-bg); border-radius: 10px; - margin: 1rem 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 1rem; - transition: all 0.3s ease; + padding: 2rem; + margin-top: 2rem; +} + +/* Drop Zone */ +.drop-zone { + border: 2px dashed rgba(255, 255, 255, 0.2); + border-radius: 10px; + padding: 2rem; + text-align: center; + cursor: pointer; + transition: var(--transition); position: relative; } -.image-container.drag-over { - background-color: var(--accent-color); +.drop-zone:hover, .drop-zone.drag-over { + border-color: var(--primary-color); + background: rgba(0, 255, 255, 0.05); +} + +.drop-zone-content { + pointer-events: none; +} + +.drop-zone-content i { + font-size: 3rem; + color: var(--primary-color); + margin-bottom: 1rem; +} + +.file-types { + font-size: 0.9rem; opacity: 0.7; + margin-top: 0.5rem; +} + +.file-input { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + opacity: 0; + cursor: pointer; +} + +/* Preview Container */ +.preview-container { + margin-top: 2rem; +} + +.preview-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; } -.image-container img { +.file-info { + font-size: 0.9rem; + opacity: 0.8; +} + +.change-image { + background: none; + border: 1px solid var(--primary-color); + color: var(--primary-color); + padding: 0.5rem 1rem; +} + +.change-image:hover { + background: var(--primary-color); + color: var(--text-dark); +} + +.preview-image-container { + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + padding: 1rem; + text-align: center; +} + +#preview-image { max-width: 100%; max-height: 400px; - object-fit: contain; - margin: 1rem 0; + border-radius: 5px; } -.image-controls { +/* Settings Panel */ +.settings-panel { + margin-top: 2rem; + padding-top: 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +.settings-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 1rem; - width: 100%; - margin-top: 1rem; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; } .dimension-controls { - display: flex; + display: grid; + grid-template-columns: 1fr auto 1fr; gap: 1rem; - align-items: center; + align-items: end; } -.dimension-controls input[type="number"] { - width: 80px; +.input-group { + margin-bottom: 1rem; } +.input-group label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-light); +} + +.input-group input, +.input-group select { + width: 100%; + padding: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 5px; + background: rgba(255, 255, 255, 0.05); + color: var(--text-light); + font-family: inherit; + transition: var(--transition); +} + +.input-group input:focus, +.input-group select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 255, 255, 0.1); +} + +/* Range Input Styling */ +input[type="range"] { + -webkit-appearance: none; + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + outline: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + background: var(--primary-color); + border-radius: 50%; + cursor: pointer; + transition: var(--transition); +} + +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.1); +} + +/* Aspect Ratio Lock */ .aspect-ratio-lock { - color: var(--accent-color); + background: none; + border: none; + color: var(--text-light); + font-size: 1.2rem; cursor: pointer; - transition: all 0.3s ease; + padding: 0.5rem; + opacity: 0.7; + transition: var(--transition); +} + +.aspect-ratio-lock:hover { + opacity: 1; } .aspect-ratio-lock.locked { - color: var(--error-color); + color: var(--primary-color); + opacity: 1; } -.format-controls { +/* Action Buttons */ +.action-buttons { display: flex; gap: 1rem; - align-items: center; justify-content: center; - flex-wrap: wrap; } -.quality-slider { - width: 100%; - margin: 1rem 0; +.primary-button, +.secondary-button { + padding: 0.75rem 1.5rem; + border-radius: 5px; + font-weight: bold; + cursor: pointer; + transition: var(--transition); + display: flex; + align-items: center; + gap: 0.5rem; } -.preview-container { - position: relative; - width: 100%; - margin: 1rem 0; +.primary-button { + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + color: var(--text-dark); + border: none; } -.preview-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - opacity: 0; - transition: opacity 0.3s ease; +.secondary-button { + background: none; + border: 1px solid var(--primary-color); + color: var(--primary-color); } -.preview-container:hover .preview-overlay { - opacity: 1; +.primary-button:hover, +.secondary-button:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); } -.file-info { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - margin-top: 0.5rem; - font-size: 0.9rem; - color: var(--accent-color); +.primary-button:disabled, +.secondary-button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; } -.drop-message { - font-size: 1.2rem; - color: var(--accent-color); - text-align: center; - margin: 1rem 0; +/* Notification */ +.notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 1rem 2rem; + border-radius: 5px; + background: var(--card-bg); + color: var(--text-light); + display: none; + animation: slideIn 0.3s ease-out; + z-index: 1000; +} + +.notification.success { + background: #28a745; +} + +.notification.error { + background: #dc3545; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } } -@media (max-width: 480px) { +/* Responsive Design */ +@media (max-width: 768px) { .dimension-controls { - flex-direction: column; - align-items: stretch; + grid-template-columns: 1fr; + gap: 0.5rem; } - .dimension-controls input[type="number"] { - width: 100%; + .aspect-ratio-lock { + justify-self: center; + } + + .action-buttons { + flex-direction: column; } - .format-controls { + .preview-header { flex-direction: column; + gap: 1rem; + text-align: center; } } \ No newline at end of file diff --git a/css/components/qr-code.css b/css/components/qr-code.css index 6ea1ad2..f4b5109 100644 --- a/css/components/qr-code.css +++ b/css/components/qr-code.css @@ -1,91 +1,133 @@ .qr-controls { - display: flex; - flex-direction: column; - gap: 1.5rem; + background: var(--card-bg); + border-radius: 10px; + padding: 2rem; + margin-top: 2rem; +} + +.input-group { + margin-bottom: 1.5rem; +} + +.input-group label { + display: block; + margin-bottom: 0.5rem; + color: var(--text-light); +} + +.input-group input, +.input-group select, +.input-group textarea { + width: 100%; + padding: 0.75rem; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 5px; + background: rgba(255, 255, 255, 0.05); + color: var(--text-light); + font-family: inherit; + transition: var(--transition); +} + +.input-group input:focus, +.input-group select:focus, +.input-group textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(0, 255, 255, 0.1); +} + +.input-group input[type="color"] { + height: 40px; + padding: 0.25rem; + cursor: pointer; } .settings-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; + margin-bottom: 1.5rem; } -.qr-output { - display: flex; - justify-content: center; - align-items: center; - min-height: 256px; - background-color: var(--input-bg); - border-radius: 10px; - padding: 1rem; - overflow: hidden; -} - -.qr-output svg, .qr-output canvas { - max-width: 100%; - height: auto; +.output-container { + margin-top: 2rem; + text-align: center; } -.output-container { - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; +.qr-output { + background: white; + padding: 1rem; + border-radius: 10px; + display: inline-block; + margin-bottom: 1rem; + min-height: 256px; + min-width: 256px; } .output-actions { - display: flex; - gap: 1rem; - justify-content: center; + margin-top: 1rem; } -/* Color input styling */ -input[type="color"] { - -webkit-appearance: none; - width: 100%; - height: 40px; +button { + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + color: var(--text-dark); border: none; + padding: 0.75rem 1.5rem; border-radius: 5px; - padding: 0; cursor: pointer; + font-weight: bold; + transition: var(--transition); } -input[type="color"]::-webkit-color-swatch-wrapper { - padding: 0; +button:hover { + opacity: 0.9; + transform: translateY(-2px); } -input[type="color"]::-webkit-color-swatch { - border: none; - border-radius: 5px; +button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; } -input[type="color"]::-moz-color-swatch { - border: none; +.notification { + position: fixed; + bottom: 20px; + right: 20px; + padding: 1rem 2rem; border-radius: 5px; + background: var(--card-bg); + color: var(--text-light); + display: none; + animation: slideIn 0.3s ease-out; + z-index: 1000; } -/* Responsive adjustments */ -@media (max-width: 768px) { - .settings-grid { - grid-template-columns: 1fr; +.notification.success { + background: #28a745; +} + +.notification.error { + background: #dc3545; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; } - - .output-actions { - flex-direction: column; - } - - .qr-output { - min-height: 200px; + to { + transform: translateX(0); + opacity: 1; } } -@media (max-width: 480px) { - input[type="color"] { - height: 32px; +@media (max-width: 768px) { + .settings-grid { + grid-template-columns: 1fr; } - + .qr-output { - min-height: 150px; - padding: 0.5rem; + max-width: 100%; } } \ No newline at end of file diff --git a/css/styles.css b/css/styles.css index e2aeca9..15b7289 100644 --- a/css/styles.css +++ b/css/styles.css @@ -1,140 +1,290 @@ -/* Theme variables */ -@import 'themes/theme-variables.css'; - -/* Components */ -@import 'components/buttons.css'; -@import 'components/inputs.css'; -@import 'components/progress.css'; -@import 'components/navigation.css'; -@import 'components/image-resizer.css'; -@import 'components/color-palette.css'; -@import 'components/ascii-art.css'; -@import 'components/qr-code.css'; -@import 'components/about.css'; - -/* Utils */ -@import 'utils/animations.css'; - -/* Base styles */ -body { - font-family: 'Roboto', sans-serif; - background-color: var(--bg-color); - color: var(--text-color); +:root { + --primary-color: #00ffff; + --secondary-color: #ff00de; + --bg-dark: #0c0c0c; + --bg-light: #ffffff; + --text-dark: #1a1a1a; + --text-light: #ffffff; + --card-bg: #1a1a1a; + --card-hover: #2a2a2a; + --transition: all 0.3s ease; +} + +/* Base Styles */ +* { margin: 0; padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Roboto', sans-serif; + background-color: var(--bg-dark); + color: var(--text-light); + line-height: 1.6; +} + +h1, h2, h3, h4 { + font-family: 'Orbitron', sans-serif; + margin-bottom: 1rem; +} + +a { + color: var(--primary-color); + text-decoration: none; + transition: var(--transition); +} + +a:hover { + color: var(--secondary-color); +} + +/* Header & Navigation */ +.hero { + background: linear-gradient(135deg, var(--bg-dark), #1a1a1a); + padding: 2rem 0; + text-align: center; + position: relative; + overflow: hidden; +} + +.nav-container { display: flex; - justify-content: center; + justify-content: space-between; align-items: center; - min-height: 100vh; - transition: all 0.3s ease; + padding: 0 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.logo h1 { + font-size: 2rem; + margin: 0; + background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.theme-toggle button { + background: none; + border: none; + color: var(--text-light); + font-size: 1.5rem; + cursor: pointer; + padding: 0.5rem; + transition: var(--transition); +} + +.theme-toggle button:hover { + color: var(--primary-color); +} + +.hero-content { + padding: 4rem 2rem; + max-width: 800px; + margin: 0 auto; } +.hero-title { + font-size: 3rem; + margin-bottom: 1rem; + text-shadow: 0 0 10px rgba(0, 255, 255, 0.5); +} + +.hero-subtitle { + font-size: 1.5rem; + opacity: 0.9; +} + +/* Main Content */ .container { - background-color: var(--container-bg); - padding: 2rem; + max-width: 1200px; + margin: 0 auto; + padding: 4rem 2rem; +} + +/* Tools Grid */ +.tools-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 2rem; + margin-bottom: 4rem; +} + +.tool-card { + background: var(--card-bg); border-radius: 10px; - box-shadow: 0 0 20px var(--shadow-color); - max-width: 800px; - width: 100%; - margin: 1rem; - transition: all 0.3s ease; + padding: 2rem; + text-align: center; + transition: var(--transition); + cursor: pointer; position: relative; + overflow: hidden; +} + +.tool-card:hover { + transform: translateY(-5px); + background: var(--card-hover); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); +} + +.tool-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 4px; + background: linear-gradient(90deg, var(--primary-color), var(--secondary-color)); + opacity: 0; + transition: var(--transition); +} + +.tool-card:hover::before { + opacity: 1; +} + +.tool-icon { + font-size: 2.5rem; + margin-bottom: 1rem; + color: var(--primary-color); +} + +.tool-card h3 { + font-size: 1.5rem; + margin-bottom: 1rem; } -.content { +.tool-card p { + margin-bottom: 1.5rem; + opacity: 0.9; +} + +.tool-features { display: flex; - flex-direction: column; + justify-content: center; gap: 1rem; } -/* Text-to-Speech specific layouts */ -.controls-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1rem; - margin: 1rem 0; +.tool-features span { + background: rgba(255, 255, 255, 0.1); + padding: 0.25rem 0.75rem; + border-radius: 15px; + font-size: 0.9rem; } -.text-controls { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.5rem; +/* Features Section */ +.features { + text-align: center; + padding: 4rem 0; } -.text-actions { - display: flex; - gap: 0.5rem; +.features h2 { + margin-bottom: 3rem; } -.history-section, .templates-section { - margin-top: 2rem; - padding-top: 1rem; - border-top: 1px solid var(--input-border); +.features-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; } -.history-list { - max-height: 200px; - overflow-y: auto; - margin-top: 1rem; +.feature { + padding: 2rem; } -.history-item { - padding: 0.5rem; - margin-bottom: 0.5rem; - background-color: var(--input-bg); - border-radius: 5px; - cursor: pointer; - transition: all 0.3s ease; +.feature i { + font-size: 2.5rem; + color: var(--primary-color); + margin-bottom: 1rem; } -.history-item:hover { - background-color: var(--accent-color); - color: var(--bg-color); +/* Footer */ +.footer { + background: var(--card-bg); + padding: 4rem 2rem 2rem; } -.template-buttons { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-top: 1rem; +.footer-content { + max-width: 1200px; + margin: 0 auto; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2rem; + margin-bottom: 2rem; } -#language-detect { - margin-top: 0.5rem; - font-size: 0.9rem; - color: var(--accent-color); +.footer-section h4 { + color: var(--primary-color); + margin-bottom: 1rem; } -.theme-toggle { - position: absolute; - top: 1rem; - right: 1rem; +.footer-section ul { + list-style: none; } -/* Responsive adjustments */ +.footer-section ul li { + margin-bottom: 0.5rem; +} + +.footer-bottom { + text-align: center; + padding-top: 2rem; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +/* Responsive Design */ @media (max-width: 768px) { + .hero-title { + font-size: 2rem; + } + + .hero-subtitle { + font-size: 1.2rem; + } + .container { - margin: 0.5rem; - padding: 1rem; + padding: 2rem 1rem; + } + + .tools-grid { + gap: 1rem; } - - .controls-grid { + + .footer-content { grid-template-columns: 1fr; + text-align: center; } } -@media (max-width: 480px) { - .neon-text { - font-size: 1.5rem; +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); } - - .text-controls { - flex-direction: column; - align-items: stretch; - } - - .text-actions { - margin-top: 0.5rem; + to { + opacity: 1; + transform: translateY(0); } } + +.tool-card { + animation: fadeIn 0.5s ease-out forwards; +} + +.tool-card:nth-child(2) { + animation-delay: 0.1s; +} + +.tool-card:nth-child(3) { + animation-delay: 0.2s; +} + +.tool-card:nth-child(4) { + animation-delay: 0.3s; +} + +.tool-card:nth-child(5) { + animation-delay: 0.4s; +} diff --git a/index.html b/index.html index 62ed25c..621f135 100644 --- a/index.html +++ b/index.html @@ -3,30 +3,146 @@ - Digital Services Hub + Digital Services Hub - Web Tools for Digital Tasks + - - + + -
-

Digital Services Hub

-
-
+ + + +
+

Key Features

+
+
+
+ +
+

Fast & Efficient

+

All tools are optimized for speed and performance, ensuring quick results without delays.

+
+ +
+
+ +
+

Secure

+

Your data stays in your browser. No server uploads, no tracking, complete privacy.

+
+ +
+
+ +
+

Accessible

+

Built with accessibility in mind, following WCAG guidelines for all users.

+
+ +
+
+ +
+

Responsive

+

Works seamlessly across all devices and screen sizes.

+
+ +
+
+ +
+

Open Source

+

Fully open source and free to use, modify, and distribute.

+
+ +
+
+ +
+

Regular Updates

+

Continuously improved with new features and security updates.

+
+
+
+ + +
+

Contribute

+

Digital Services Hub is an open-source project. We welcome contributions from developers of all skill levels.

+ +
+ - +
+ + +
- - + diff --git a/pages/ascii-art.html b/pages/ascii-art.html index c503f03..04a8f29 100644 --- a/pages/ascii-art.html +++ b/pages/ascii-art.html @@ -3,81 +3,191 @@ - ASCII Art Converter - Digital Services Hub + ASCII Art Generator - Digital Services Hub + + + -
-

ASCII Art Converter

- - - -
+
+ +
+

ASCII Art Generator

+

Transform your images into text-based art

+
+
-
-
- +
+
+ +
+ +
+ +

Drop an image here or click to upload

+

Supports: JPG, PNG, GIF (max 5MB)

+
+
+ + +
+ Preview of the uploaded image +
+ + +
+
+ -
-

Drop an image here or click to upload

-

Supports: JPG, PNG, GIF

-
- +
+ + +
+ + -
-
- - -
+
+ + +
-
- - -
+
+ + +
-
- -
+
+ +
-
- -
+
+
+
+ + + - + +
+

+                
+                
+ + + +
+
+
+
-
-

-                    
-                    
- - -
+ + +
diff --git a/pages/url-shortener.html b/pages/url-shortener.html index 7047815..c7e5cc0 100644 --- a/pages/url-shortener.html +++ b/pages/url-shortener.html @@ -7,6 +7,7 @@ + diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 0000000..ba1b1a5 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,22 @@ +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import terser from '@rollup/plugin-terser'; + +export default { + input: 'js/common.js', + output: { + file: 'dist/bundle.js', + format: 'es', + sourcemap: true + }, + plugins: [ + nodeResolve(), + terser({ + format: { + comments: false + } + }) + ], + watch: { + include: 'js/**' + } +}; \ No newline at end of file diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100644 index 214c64c..0000000 --- a/scripts/build.js +++ /dev/null @@ -1,401 +0,0 @@ -import { TOOLS } from '../js/config/tools.js'; -import { generateToolPage } from '../js/utils/template-generator.js'; -import fs from 'fs/promises'; -import path from 'path'; - -async function buildToolPages() { - try { - // Ensure pages directory exists - await fs.mkdir('pages', { recursive: true }); - - // Generate each tool page - for (const tool of TOOLS) { - const pageContent = generateToolPage(tool.id); - await fs.writeFile( - path.join('pages', tool.path), - pageContent, - 'utf-8' - ); - console.log(`Generated ${tool.path}`); - } - - // Generate index page - await generateIndexPage(); - console.log('Generated index.html'); - - // Generate about page - await generateAboutPage(); - console.log('Generated about.html'); - - // Ensure all required CSS files exist - await ensureToolStyles(); - console.log('Verified tool styles'); - - // Ensure all required JS files exist - await ensureToolScripts(); - console.log('Verified tool scripts'); - - } catch (error) { - console.error('Build failed:', error); - process.exit(1); - } -} - -async function ensureToolStyles() { - const cssDir = path.join('css', 'components'); - await fs.mkdir(cssDir, { recursive: true }); - - for (const tool of TOOLS) { - const cssPath = path.join(cssDir, `${tool.id}.css`); - try { - await fs.access(cssPath); - } catch { - // Create empty CSS file if it doesn't exist - await fs.writeFile(cssPath, '/* Styles for ' + tool.name + ' */\n', 'utf-8'); - console.log(`Created empty CSS file for ${tool.id}`); - } - } -} - -async function ensureToolScripts() { - const jsDir = path.join('js', 'features'); - await fs.mkdir(jsDir, { recursive: true }); - - for (const tool of TOOLS) { - const jsPath = path.join(jsDir, `${tool.id}.js`); - try { - await fs.access(jsPath); - } catch { - // Create basic JS file if it doesn't exist - const basicScript = `import { BaseTool } from './base-tool.js'; - -class ${toPascalCase(tool.id)} extends BaseTool { - constructor() { - super(); - this.initializeElements(); - this.setupEventListeners(); - } - - initializeElements() { - // Initialize tool elements - } - - setupEventListeners() { - // Setup event listeners - } -} - -// Initialize the tool -const ${toCamelCase(tool.id)} = new ${toPascalCase(tool.id)}(); -`; - await fs.writeFile(jsPath, basicScript, 'utf-8'); - console.log(`Created basic JS file for ${tool.id}`); - } - } -} - -function toPascalCase(str) { - return str.split('-') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) - .join(''); -} - -function toCamelCase(str) { - const pascal = toPascalCase(str); - return pascal.charAt(0).toLowerCase() + pascal.slice(1); -} - -async function generateIndexPage() { - const indexContent = ` - - - - - Digital Services Hub - Web Tools for Digital Tasks - - - - - - -
- -
-

Welcome to Digital Services Hub

-

Free web-based tools for your digital tasks

-
-
- -
- -
- -
-

Why Choose Digital Services Hub?

-
-
- -

Fast & Efficient

-

All tools are optimized for speed and performance.

-
-
- -

Secure

-

Your data stays in your browser, no server uploads needed.

-
-
- -

Accessible

-

Built with accessibility in mind for all users.

-
-
- -

Responsive

-

Works seamlessly on desktop and mobile devices.

-
-
-
-
- -
- - -
- - - -`; - - await fs.writeFile('index.html', indexContent, 'utf-8'); -} - -async function generateAboutPage() { - const aboutContent = ` - - - - - About - Digital Services Hub - - - - - - - -
- -
-

About Digital Services Hub

-

Empowering users with modern web tools

-
-
- -
- -
-
-

Our Mission

-

Digital Services Hub is dedicated to providing free, accessible, and powerful web-based tools for everyday digital tasks. We believe that quality digital tools should be available to everyone, regardless of technical expertise or budget.

-
-
- ${TOOLS.length}+ - Tools -
-
- 100 - Free -
-
- 0 - Ads -
-
-
-
- - -
-

Our Tools

- -
-
- - -
-

Technologies

-
-
-

Frontend

-
    -
  • HTML5
  • -
  • CSS3
  • -
  • JavaScript (ES6+)
  • -
-
-
-

Libraries

-
    -
  • Web Speech API
  • -
  • Canvas API
  • -
  • File API
  • -
-
-
-

Tools

-
    -
  • Git
  • -
  • GitHub
  • -
  • VS Code
  • -
-
-
-
- - -
-

Key Features

-
-
-
- -
-

Fast & Efficient

-

All tools are optimized for speed and performance, ensuring quick results without delays.

-
- -
-
- -
-

Secure

-

Your data stays in your browser. No server uploads, no tracking, complete privacy.

-
- -
-
- -
-

Accessible

-

Built with accessibility in mind, following WCAG guidelines for all users.

-
- -
-
- -
-

Responsive

-

Works seamlessly across all devices and screen sizes.

-
- -
-
- -
-

Open Source

-

Fully open source and free to use, modify, and distribute.

-
- -
-
- -
-

Regular Updates

-

Continuously improved with new features and security updates.

-
-
-
- - -
-

Contribute

-

Digital Services Hub is an open-source project. We welcome contributions from developers of all skill levels.

- -
-
- -
- - -
- - - -`; - - await fs.writeFile(path.join('pages', 'about.html'), aboutContent, 'utf-8'); -} - -// Run the build process -buildToolPages().then(() => { - console.log('Build completed successfully!'); -}); \ No newline at end of file diff --git a/scripts/validate.js b/scripts/validate.js deleted file mode 100644 index abf9953..0000000 --- a/scripts/validate.js +++ /dev/null @@ -1,217 +0,0 @@ -import { TOOLS, CATEGORIES } from '../js/config/tools.js'; -import fs from 'fs/promises'; -import path from 'path'; - -async function validateProject() { - const errors = []; - const warnings = []; - - try { - // Validate tool configuration - validateToolConfig(errors, warnings); - - // Validate file structure - await validateFileStructure(errors, warnings); - - // Validate HTML files - await validateHtmlFiles(errors, warnings); - - // Validate JavaScript files - await validateJavaScriptFiles(errors, warnings); - - // Validate CSS files - await validateCssFiles(errors, warnings); - - // Report results - if (errors.length > 0) { - console.error('\nValidation Errors:'); - errors.forEach(error => console.error(`❌ ${error}`)); - } - - if (warnings.length > 0) { - console.warn('\nValidation Warnings:'); - warnings.forEach(warning => console.warn(`⚠️ ${warning}`)); - } - - if (errors.length === 0 && warnings.length === 0) { - console.log('✅ Validation passed successfully!'); - return true; - } - - if (errors.length > 0) { - process.exit(1); - } - - return warnings.length === 0; - } catch (error) { - console.error('Validation failed:', error); - process.exit(1); - } -} - -function validateToolConfig(errors, warnings) { - // Check for duplicate IDs - const ids = TOOLS.map(tool => tool.id); - const duplicateIds = ids.filter((id, index) => ids.indexOf(id) !== index); - if (duplicateIds.length > 0) { - errors.push(`Duplicate tool IDs found: ${duplicateIds.join(', ')}`); - } - - // Check for valid categories - TOOLS.forEach(tool => { - if (!CATEGORIES[tool.category]) { - errors.push(`Invalid category '${tool.category}' for tool '${tool.id}'`); - } - }); - - // Check for required fields - TOOLS.forEach(tool => { - ['id', 'name', 'description', 'icon', 'features', 'path', 'category', 'order'].forEach(field => { - if (!tool[field]) { - errors.push(`Missing required field '${field}' in tool '${tool.id}'`); - } - }); - }); - - // Check for unique order values - const orders = TOOLS.map(tool => tool.order); - const duplicateOrders = orders.filter((order, index) => orders.indexOf(order) !== index); - if (duplicateOrders.length > 0) { - errors.push(`Duplicate order values found: ${duplicateOrders.join(', ')}`); - } -} - -async function validateFileStructure(errors, warnings) { - const requiredDirs = [ - 'pages', - 'js', - 'js/features', - 'js/utils', - 'js/config', - 'css', - 'css/components', - 'css/utils', - 'css/themes' - ]; - - for (const dir of requiredDirs) { - try { - await fs.access(dir); - } catch { - errors.push(`Missing required directory: ${dir}`); - } - } - - // Check for required base files - const requiredFiles = [ - 'index.html', - 'js/common.js', - 'css/styles.css', - 'js/utils/helpers.js', - 'js/utils/validation.js', - 'js/utils/ui.js' - ]; - - for (const file of requiredFiles) { - try { - await fs.access(file); - } catch { - errors.push(`Missing required file: ${file}`); - } - } -} - -async function validateHtmlFiles(errors, warnings) { - // Check each tool's HTML file - for (const tool of TOOLS) { - const htmlPath = path.join('pages', tool.path); - try { - const content = await fs.readFile(htmlPath, 'utf-8'); - - // Check for required meta tags - if (!content.includes(' { - if (!content.includes(method)) { - errors.push(`Missing required method '${method}' in ${jsPath}`); - } - }); - - // Check for proper initialization - const className = tool.id.split('-') - .map(part => part.charAt(0).toUpperCase() + part.slice(1)) - .join(''); - if (!content.includes(`new ${className}()`)) { - errors.push(`Missing tool initialization in ${jsPath}`); - } - } catch (error) { - errors.push(`Failed to validate JS file ${jsPath}: ${error.message}`); - } - } -} - -async function validateCssFiles(errors, warnings) { - // Check each tool's CSS file - for (const tool of TOOLS) { - const cssPath = path.join('css', 'components', `${tool.id}.css`); - try { - const content = await fs.readFile(cssPath, 'utf-8'); - - // Check for responsive design - if (!content.includes('@media')) { - warnings.push(`No media queries found in ${cssPath}`); - } - - // Check for CSS variables usage - if (!content.includes('var(--')) { - warnings.push(`No CSS variables used in ${cssPath}`); - } - - // Check for proper naming convention - const mainClass = `.${tool.id}`; - if (!content.includes(mainClass)) { - warnings.push(`Missing main tool class '${mainClass}' in ${cssPath}`); - } - } catch (error) { - errors.push(`Failed to validate CSS file ${cssPath}: ${error.message}`); - } - } -} - -// Run validation -validateProject(); \ No newline at end of file From 1713c18224f7d8fa99e749fd502fdad3c1256a69 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Thu, 26 Dec 2024 11:28:39 -0500 Subject: [PATCH 083/113] Refactor component exports and types --- js/components/alert.js | 157 ++++++++++++++++++++++++++++++++ js/components/index.js | 15 ++++ js/components/modal.js | 158 ++++++++++++++++++++++++++++++++ js/components/tooltip.js | 188 +++++++++++++++++++++++++++++++++++++++ js/components/types.js | 42 +++++++++ 5 files changed, 560 insertions(+) create mode 100644 js/components/alert.js create mode 100644 js/components/index.js create mode 100644 js/components/modal.js create mode 100644 js/components/tooltip.js create mode 100644 js/components/types.js diff --git a/js/components/alert.js b/js/components/alert.js new file mode 100644 index 0000000..76c9db8 --- /dev/null +++ b/js/components/alert.js @@ -0,0 +1,157 @@ +/** + * Alert Component + * @module components/alert + */ + +import { sanitizeHTML } from '../utils/dom.js'; + +/** + * @typedef {Object} AlertOptions + * @property {string} [type='info'] - Alert type (info, success, warning, error) + * @property {string} [title] - Alert title + * @property {boolean} [dismissible=true] - Whether alert can be dismissed + * @property {boolean} [animate=true] - Whether to animate alert + * @property {number} [duration] - Auto-dismiss duration in ms (0 for no auto-dismiss) + * @property {Function} [onClose] - Callback when alert closes + */ + +export class Alert { + /** + * Create an alert instance + * @param {string|HTMLElement} message - Alert message + * @param {AlertOptions} options - Alert configuration options + */ + constructor(message, options = {}) { + this.message = message; + this.options = { + type: 'info', + title: '', + dismissible: true, + animate: true, + duration: 0, + onClose: null, + ...options + }; + + this.element = null; + this.closeTimeout = null; + this.init(); + } + + /** + * Initialize alert + * @private + */ + init() { + // Create alert element + this.element = document.createElement('div'); + this.element.className = `alert alert-${this.options.type}`; + if (this.options.animate) { + this.element.classList.add('alert-animate'); + } + this.element.setAttribute('role', 'alert'); + + // Create alert content + this.element.innerHTML = ` +
+ ${this.options.title ? `
${sanitizeHTML(this.options.title)}
` : ''} +
+
+ ${this.options.dismissible ? '' : ''} + `; + + // Set message + const messageEl = this.element.querySelector('.alert-message'); + if (typeof this.message === 'string') { + messageEl.innerHTML = sanitizeHTML(this.message); + } else if (this.message instanceof HTMLElement) { + messageEl.appendChild(this.message); + } + + // Add event listeners + if (this.options.dismissible) { + this.element.querySelector('.alert-dismiss').addEventListener('click', () => this.close()); + } + + // Set up auto-dismiss + if (this.options.duration > 0) { + this.closeTimeout = setTimeout(() => this.close(), this.options.duration); + } + } + + /** + * Show the alert + * @param {HTMLElement} [container=document.body] - Container to append alert to + */ + show(container = document.body) { + container.appendChild(this.element); + } + + /** + * Close the alert + */ + close() { + clearTimeout(this.closeTimeout); + + if (this.options.animate) { + this.element.classList.add('alert-closing'); + setTimeout(() => { + this.destroy(); + }, 300); // Match CSS transition duration + } else { + this.destroy(); + } + + if (this.options.onClose) { + this.options.onClose(); + } + } + + /** + * Update alert message + * @param {string|HTMLElement} message - New message + */ + setMessage(message) { + this.message = message; + const messageEl = this.element.querySelector('.alert-message'); + messageEl.innerHTML = ''; + + if (typeof message === 'string') { + messageEl.innerHTML = sanitizeHTML(message); + } else if (message instanceof HTMLElement) { + messageEl.appendChild(message); + } + } + + /** + * Update alert type + * @param {string} type - New type + */ + setType(type) { + this.element.className = this.element.className.replace(/alert-\w+/, `alert-${type}`); + this.options.type = type; + } + + /** + * Destroy alert instance + * @private + */ + destroy() { + if (this.element.parentNode) { + this.element.parentNode.removeChild(this.element); + } + this.element = null; + } + + /** + * Create and show an alert (static method) + * @param {string|HTMLElement} message - Alert message + * @param {AlertOptions} options - Alert configuration options + * @returns {Alert} Alert instance + */ + static show(message, options = {}) { + const alert = new Alert(message, options); + alert.show(); + return alert; + } +} diff --git a/js/components/index.js b/js/components/index.js new file mode 100644 index 0000000..c2c5b5c --- /dev/null +++ b/js/components/index.js @@ -0,0 +1,15 @@ +/** + * Component exports + * @module components + */ + +export { Modal } from './modal.js'; +export { Tooltip } from './tooltip.js'; +export { Alert } from './alert.js'; + +// Export component types +export type { + ModalOptions, + TooltipOptions, + AlertOptions +} from './types.js'; diff --git a/js/components/modal.js b/js/components/modal.js new file mode 100644 index 0000000..45065d6 --- /dev/null +++ b/js/components/modal.js @@ -0,0 +1,158 @@ +/** + * Modal Component + * @module components/modal + */ + +import { sanitizeHTML } from '../utils/dom.js'; + +/** + * @typedef {Object} ModalOptions + * @property {string} [title] - Modal title + * @property {string|HTMLElement} [content] - Modal content + * @property {boolean} [closable=true] - Whether modal can be closed + * @property {string} [size='md'] - Modal size (sm, md, lg) + * @property {Function} [onOpen] - Callback when modal opens + * @property {Function} [onClose] - Callback when modal closes + */ + +export class Modal { + /** + * Create a modal instance + * @param {ModalOptions} options - Modal configuration options + */ + constructor(options = {}) { + this.options = { + title: '', + content: '', + closable: true, + size: 'md', + onOpen: null, + onClose: null, + ...options + }; + + this.isOpen = false; + this.element = null; + this.init(); + } + + /** + * Initialize modal + * @private + */ + init() { + // Create modal element + this.element = document.createElement('div'); + this.element.className = `modal modal-${this.options.size}`; + this.element.setAttribute('role', 'dialog'); + this.element.setAttribute('aria-modal', 'true'); + + // Create modal content + this.element.innerHTML = ` + + + `; + + // Set content + const contentEl = this.element.querySelector('.modal-content'); + if (typeof this.options.content === 'string') { + contentEl.innerHTML = sanitizeHTML(this.options.content); + } else if (this.options.content instanceof HTMLElement) { + contentEl.appendChild(this.options.content); + } + + // Add event listeners + if (this.options.closable) { + this.element.querySelector('.modal-close').addEventListener('click', () => this.close()); + this.element.querySelector('.modal-backdrop').addEventListener('click', () => this.close()); + } + + // Handle keyboard events + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && this.options.closable && this.isOpen) { + this.close(); + } + }); + + // Trap focus within modal when open + this.element.addEventListener('keydown', (e) => { + if (e.key === 'Tab' && this.isOpen) { + const focusable = this.element.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + }); + } + + /** + * Open the modal + */ + open() { + if (this.isOpen) return; + + document.body.appendChild(this.element); + this.isOpen = true; + document.body.style.overflow = 'hidden'; + + // Focus first focusable element + const focusable = this.element.querySelector( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' + ); + if (focusable) focusable.focus(); + + if (this.options.onOpen) this.options.onOpen(); + } + + /** + * Close the modal + */ + close() { + if (!this.isOpen) return; + + this.element.remove(); + this.isOpen = false; + document.body.style.overflow = ''; + + if (this.options.onClose) this.options.onClose(); + } + + /** + * Update modal content + * @param {string|HTMLElement} content - New content + */ + setContent(content) { + const contentEl = this.element.querySelector('.modal-content'); + contentEl.innerHTML = ''; + + if (typeof content === 'string') { + contentEl.innerHTML = sanitizeHTML(content); + } else if (content instanceof HTMLElement) { + contentEl.appendChild(content); + } + } + + /** + * Update modal title + * @param {string} title - New title + */ + setTitle(title) { + const titleEl = this.element.querySelector('.modal-title'); + titleEl.textContent = title; + } +} diff --git a/js/components/tooltip.js b/js/components/tooltip.js new file mode 100644 index 0000000..98bb5b0 --- /dev/null +++ b/js/components/tooltip.js @@ -0,0 +1,188 @@ +/** + * Tooltip Component + * @module components/tooltip + */ + +/** + * @typedef {Object} TooltipOptions + * @property {string} [position='top'] - Tooltip position (top, right, bottom, left) + * @property {string} [theme='dark'] - Tooltip theme (dark, light) + * @property {number} [showDelay=0] - Delay before showing tooltip (ms) + * @property {number} [hideDelay=0] - Delay before hiding tooltip (ms) + * @property {boolean} [html=false] - Whether to allow HTML in tooltip content + */ + +export class Tooltip { + /** + * Create a tooltip instance + * @param {HTMLElement} element - Element to attach tooltip to + * @param {string|HTMLElement} content - Tooltip content + * @param {TooltipOptions} options - Tooltip configuration options + */ + constructor(element, content, options = {}) { + this.element = element; + this.content = content; + this.options = { + position: 'top', + theme: 'dark', + showDelay: 0, + hideDelay: 0, + html: false, + ...options + }; + + this.tooltip = null; + this.showTimeout = null; + this.hideTimeout = null; + + this.init(); + } + + /** + * Initialize tooltip + * @private + */ + init() { + // Create tooltip element + this.tooltip = document.createElement('div'); + this.tooltip.className = `tooltip tooltip-${this.options.theme}`; + this.tooltip.setAttribute('role', 'tooltip'); + + // Set content + if (typeof this.content === 'string') { + if (this.options.html) { + this.tooltip.innerHTML = this.content; + } else { + this.tooltip.textContent = this.content; + } + } else if (this.content instanceof HTMLElement) { + this.tooltip.appendChild(this.content.cloneNode(true)); + } + + // Add event listeners + this.element.addEventListener('mouseenter', () => this.show()); + this.element.addEventListener('mouseleave', () => this.hide()); + this.element.addEventListener('focus', () => this.show()); + this.element.addEventListener('blur', () => this.hide()); + + // Add ARIA attributes + const id = `tooltip-${Math.random().toString(36).substr(2, 9)}`; + this.tooltip.id = id; + this.element.setAttribute('aria-describedby', id); + } + + /** + * Show the tooltip + */ + show() { + clearTimeout(this.hideTimeout); + + this.showTimeout = setTimeout(() => { + document.body.appendChild(this.tooltip); + this.position(); + this.tooltip.classList.add('tooltip-visible'); + }, this.options.showDelay); + } + + /** + * Hide the tooltip + */ + hide() { + clearTimeout(this.showTimeout); + + this.hideTimeout = setTimeout(() => { + this.tooltip.classList.remove('tooltip-visible'); + setTimeout(() => { + if (this.tooltip.parentNode) { + this.tooltip.parentNode.removeChild(this.tooltip); + } + }, 200); // Match CSS transition duration + }, this.options.hideDelay); + } + + /** + * Position the tooltip + * @private + */ + position() { + const elementRect = this.element.getBoundingClientRect(); + const tooltipRect = this.tooltip.getBoundingClientRect(); + const scrollTop = window.pageYOffset || document.documentElement.scrollTop; + const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; + + let top, left; + + switch (this.options.position) { + case 'top': + top = elementRect.top + scrollTop - tooltipRect.height - 10; + left = elementRect.left + scrollLeft + (elementRect.width - tooltipRect.width) / 2; + break; + case 'bottom': + top = elementRect.bottom + scrollTop + 10; + left = elementRect.left + scrollLeft + (elementRect.width - tooltipRect.width) / 2; + break; + case 'left': + top = elementRect.top + scrollTop + (elementRect.height - tooltipRect.height) / 2; + left = elementRect.left + scrollLeft - tooltipRect.width - 10; + break; + case 'right': + top = elementRect.top + scrollTop + (elementRect.height - tooltipRect.height) / 2; + left = elementRect.right + scrollLeft + 10; + break; + } + + // Keep tooltip within viewport + const padding = 5; + top = Math.max(padding, Math.min(top, window.innerHeight + scrollTop - tooltipRect.height - padding)); + left = Math.max(padding, Math.min(left, window.innerWidth + scrollLeft - tooltipRect.width - padding)); + + this.tooltip.style.top = `${top}px`; + this.tooltip.style.left = `${left}px`; + } + + /** + * Update tooltip content + * @param {string|HTMLElement} content - New content + */ + setContent(content) { + this.content = content; + + if (typeof content === 'string') { + if (this.options.html) { + this.tooltip.innerHTML = content; + } else { + this.tooltip.textContent = content; + } + } else if (content instanceof HTMLElement) { + this.tooltip.innerHTML = ''; + this.tooltip.appendChild(content.cloneNode(true)); + } + } + + /** + * Update tooltip position + * @param {string} position - New position + */ + setPosition(position) { + this.options.position = position; + if (this.tooltip.classList.contains('tooltip-visible')) { + this.position(); + } + } + + /** + * Destroy tooltip instance + */ + destroy() { + clearTimeout(this.showTimeout); + clearTimeout(this.hideTimeout); + + this.element.removeAttribute('aria-describedby'); + if (this.tooltip.parentNode) { + this.tooltip.parentNode.removeChild(this.tooltip); + } + + this.element = null; + this.tooltip = null; + } +} diff --git a/js/components/types.js b/js/components/types.js new file mode 100644 index 0000000..620c3ce --- /dev/null +++ b/js/components/types.js @@ -0,0 +1,42 @@ +/** + * Component type definitions + * @module components/types + */ + +/** + * Modal configuration options + * @typedef {Object} ModalOptions + * @property {string} [title] - Modal title + * @property {string|HTMLElement} [content] - Modal content + * @property {boolean} [closable=true] - Whether modal can be closed + * @property {string} [size='md'] - Modal size (sm, md, lg) + * @property {Function} [onOpen] - Callback when modal opens + * @property {Function} [onClose] - Callback when modal closes + */ + +/** + * Tooltip configuration options + * @typedef {Object} TooltipOptions + * @property {string} [position='top'] - Tooltip position (top, right, bottom, left) + * @property {string} [theme='dark'] - Tooltip theme (dark, light) + * @property {number} [showDelay=0] - Delay before showing tooltip (ms) + * @property {number} [hideDelay=0] - Delay before hiding tooltip (ms) + * @property {boolean} [html=false] - Whether to allow HTML in tooltip content + */ + +/** + * Alert configuration options + * @typedef {Object} AlertOptions + * @property {string} [type='info'] - Alert type (info, success, warning, error) + * @property {string} [title] - Alert title + * @property {boolean} [dismissible=true] - Whether alert can be dismissed + * @property {boolean} [animate=true] - Whether to animate alert + * @property {number} [duration] - Auto-dismiss duration in ms (0 for no auto-dismiss) + * @property {Function} [onClose] - Callback when alert closes + */ + +export { + ModalOptions, + TooltipOptions, + AlertOptions +}; From 3e80eeabf5372395f0017f486bcdda7c25e26258 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Thu, 26 Dec 2024 11:34:30 -0500 Subject: [PATCH 084/113] Refactor alert component styles and DOM utilities --- css/components/alerts.css | 157 +++----- css/components/text-to-speech.css | 354 +++++------------- js/features/text-to-speech.js | 588 ++++++++++++------------------ js/utils/dom.js | 105 +++--- pages/text-to-speech.html | 244 ++++--------- 5 files changed, 535 insertions(+), 913 deletions(-) diff --git a/css/components/alerts.css b/css/components/alerts.css index bec3632..987b4fc 100644 --- a/css/components/alerts.css +++ b/css/components/alerts.css @@ -1,148 +1,103 @@ /** - * Alert Components + * Alert Component Styles */ -/* Base alert */ -.alert { - padding: var(--spacing-md); - border-radius: var(--radius-md); - margin-bottom: var(--spacing-md); +.alert-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 1000; display: flex; - align-items: flex-start; - gap: var(--spacing-sm); -} - -/* Alert variations */ -.alert-info { - background-color: rgba(0, 255, 255, 0.1); - border-left: 4px solid var(--primary-color); - color: var(--primary-color); -} - -.alert-success { - background-color: rgba(0, 255, 0, 0.1); - border-left: 4px solid var(--success-color); - color: var(--success-color); -} - -.alert-warning { - background-color: rgba(255, 255, 0, 0.1); - border-left: 4px solid var(--warning-color); - color: var(--warning-color); + flex-direction: column; + gap: 10px; + max-width: 400px; } -.alert-error { - background-color: rgba(255, 0, 0, 0.1); - border-left: 4px solid var(--error-color); - color: var(--error-color); +.alert { + padding: 15px 20px; + border-radius: var(--border-radius); + background: var(--bg-dark); + color: var(--text-primary); + box-shadow: var(--shadow-lg); + display: flex; + align-items: center; + justify-content: space-between; + animation: slideIn 0.3s ease-out; } -/* Alert with icon */ -.alert-icon { - font-size: 1.25rem; - line-height: 1; +.alert.alert-closing { + animation: slideOut 0.3s ease-out forwards; } -/* Alert content */ .alert-content { - flex: 1; + display: flex; + align-items: center; + gap: 10px; } -.alert-title { - font-weight: bold; - margin-bottom: var(--spacing-xs); +.alert-icon { + font-size: 1.2em; } .alert-message { - color: inherit; - opacity: 0.9; -} - -/* Dismissible alert */ -.alert-dismissible { - position: relative; - padding-right: calc(var(--spacing-xl) + var(--spacing-sm)); + margin-right: 20px; } .alert-dismiss { - position: absolute; - top: var(--spacing-sm); - right: var(--spacing-sm); - padding: var(--spacing-xs); background: none; border: none; color: inherit; - opacity: 0.5; cursor: pointer; - transition: var(--transition); + opacity: 0.7; + transition: opacity 0.2s; + padding: 0; + font-size: 1.2em; } .alert-dismiss:hover { opacity: 1; } -/* Alert animations */ -.alert-animate { - animation: alertSlideIn 0.3s ease-out; -} - -@keyframes alertSlideIn { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Alert sizes */ -.alert-sm { - padding: var(--spacing-sm); - font-size: 0.875rem; +/* Alert Types */ +.alert-info { + background: var(--info-color); + color: var(--text-light); } -.alert-lg { - padding: var(--spacing-lg); - font-size: 1.125rem; +.alert-success { + background: var(--success-color); + color: var(--text-light); } -/* Alert groups */ -.alert-group { - display: flex; - flex-direction: column; - gap: var(--spacing-sm); +.alert-warning { + background: var(--warning-color); + color: var(--text-dark); } -/* Toast notifications */ -.toast { - position: fixed; - bottom: var(--spacing-md); - right: var(--spacing-md); - z-index: var(--z-toast); - max-width: 350px; - animation: toastSlideIn 0.3s ease-out; +.alert-error { + background: var(--error-color); + color: var(--text-light); } -@keyframes toastSlideIn { +/* Animations */ +@keyframes slideIn { from { - opacity: 0; transform: translateX(100%); + opacity: 0; } to { - opacity: 1; transform: translateX(0); + opacity: 1; } } -/* Responsive adjustments */ -@media (max-width: 768px) { - .alert { - padding: var(--spacing-sm); +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; } - - .toast { - max-width: calc(100% - var(--spacing-md) * 2); + to { + transform: translateX(100%); + opacity: 0; } -} \ No newline at end of file +} diff --git a/css/components/text-to-speech.css b/css/components/text-to-speech.css index d2b6017..0abe87a 100644 --- a/css/components/text-to-speech.css +++ b/css/components/text-to-speech.css @@ -1,324 +1,168 @@ -.tts-controls { - background: var(--card-bg); - border-radius: 10px; - padding: 2rem; - margin-top: 2rem; -} +/** + * Text-to-Speech Component Styles + */ -/* Text Input Section */ -.text-input-section { - margin-bottom: 2rem; +.tts-container { + background: var(--card-bg); + border-radius: var(--border-radius); + padding: var(--spacing-lg); + box-shadow: var(--shadow-md); } -.text-controls { +.tts-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; + margin-bottom: var(--spacing-md); } -.text-controls label { - color: var(--text-light); - font-weight: 500; +.tts-title { + font-size: 1.25rem; + color: var(--text-primary); } -.text-actions { +.tts-actions { display: flex; - gap: 1rem; + gap: var(--spacing-sm); } -.text-input-container { - position: relative; - margin-bottom: 1rem; -} - -.text-input { +.tts-textarea { width: 100%; - min-height: 150px; - padding: 1rem; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 10px; - background: rgba(255, 255, 255, 0.05); - color: var(--text-light); - font-family: 'Roboto', sans-serif; - font-size: 1rem; - line-height: 1.5; + min-height: 200px; + padding: var(--spacing-md); + margin-bottom: var(--spacing-md); + background: var(--bg-dark); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + color: var(--text-primary); + font-family: var(--font-mono); resize: vertical; - transition: var(--transition); -} - -.text-input:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.1); } -.char-count { - position: absolute; - bottom: 1rem; - right: 1rem; - font-size: 0.9rem; - color: var(--text-light); - opacity: 0.7; -} - -.language-detect { - font-size: 0.9rem; - color: var(--text-light); - opacity: 0.8; +.tts-info { + display: flex; + justify-content: space-between; + margin-bottom: var(--spacing-md); + color: var(--text-muted); + font-size: 0.875rem; } -/* Voice Controls */ -.controls-grid { +.tts-controls { display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 1.5rem; - margin-bottom: 2rem; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: var(--spacing-md); + margin-bottom: var(--spacing-lg); } -.input-group { +.tts-control { display: flex; flex-direction: column; - gap: 0.5rem; -} - -.input-group label { - color: var(--text-light); - display: flex; - justify-content: space-between; - align-items: center; + gap: var(--spacing-sm); } -.input-group select { - padding: 0.75rem; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 5px; - background: rgba(255, 255, 255, 0.05); - color: var(--text-light); - cursor: pointer; - transition: var(--transition); +.tts-control label { + color: var(--text-primary); + font-weight: 500; } -.input-group select:focus { - outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.1); +.tts-control select, +.tts-control input[type="range"] { + width: 100%; + padding: var(--spacing-sm); + background: var(--bg-dark); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + color: var(--text-primary); } -/* Range Input Styling */ -input[type="range"] { +.tts-control input[type="range"] { -webkit-appearance: none; - width: 100%; height: 6px; - background: rgba(255, 255, 255, 0.1); + background: var(--primary-color); + border: none; border-radius: 3px; - outline: none; } -input[type="range"]::-webkit-slider-thumb { +.tts-control input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; - width: 20px; - height: 20px; + width: 16px; + height: 16px; background: var(--primary-color); + border: 2px solid var(--bg-dark); border-radius: 50%; cursor: pointer; - transition: var(--transition); } -input[type="range"]::-webkit-slider-thumb:hover { - transform: scale(1.1); -} - -/* Action Buttons */ -.action-buttons { +.tts-buttons { display: flex; - gap: 1rem; - margin-bottom: 2rem; -} - -.speak-button { - flex: 2; -} - -.preview-button, -.download-button { - flex: 1; + gap: var(--spacing-md); + margin-top: var(--spacing-lg); } -/* Progress Bar */ -.progress-container { - margin-bottom: 2rem; - display: none; -} - -.progress-container.active { - display: block; - animation: fadeIn 0.3s ease-out; -} - -.progress-bar { - width: 100%; - height: 6px; - background: rgba(255, 255, 255, 0.1); - border-radius: 3px; - overflow: hidden; - margin-bottom: 0.5rem; -} - -.progress { - width: 0%; - height: 100%; - background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); - transition: width 0.3s ease; -} - -.progress-text { - font-size: 0.9rem; +.tts-button { + padding: var(--spacing-sm) var(--spacing-md); + background: var(--primary-color); + border: none; + border-radius: var(--border-radius); color: var(--text-light); - opacity: 0.8; -} - -/* History Section */ -.history-section { - margin-top: 2rem; - padding-top: 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.1); -} - -.history-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: 1rem; - margin-top: 1rem; -} - -.history-item { - background: rgba(255, 255, 255, 0.05); - border-radius: 10px; - padding: 1rem; - transition: var(--transition); + font-weight: 500; cursor: pointer; + transition: background-color 0.2s; } -.history-item:hover { - transform: translateY(-2px); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); -} - -.history-text { - font-size: 0.9rem; - color: var(--text-light); - margin-bottom: 1rem; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; +.tts-button:hover { + background: var(--primary-dark); } -.history-meta { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 0.8rem; - color: var(--text-light); - opacity: 0.7; -} - -.history-actions { - display: flex; - gap: 0.5rem; +.tts-button.secondary { + background: var(--bg-dark); + border: 1px solid var(--border-color); } -/* Templates Section */ -.templates-section { - margin-top: 2rem; - padding-top: 2rem; - border-top: 1px solid rgba(255, 255, 255, 0.1); +.tts-button.secondary:hover { + background: var(--bg-darker); } -.template-buttons { - display: flex; - flex-wrap: wrap; - gap: 1rem; - margin-top: 1rem; -} - -.template-button { - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 5px; - padding: 0.75rem 1.5rem; - color: var(--text-light); +.tts-templates { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: var(--spacing-md); + margin-top: var(--spacing-xl); + padding-top: var(--spacing-lg); + border-top: 1px solid var(--border-color); +} + +.template-btn { + padding: var(--spacing-sm); + background: var(--bg-dark); + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + color: var(--text-primary); + font-size: 0.875rem; cursor: pointer; - transition: var(--transition); + transition: all 0.2s; } -.template-button:hover { - background: rgba(255, 255, 255, 0.1); - transform: translateY(-2px); +.template-btn:hover { + background: var(--bg-darker); + transform: translateY(-1px); } -/* Loading State */ -.loading { - position: relative; - pointer-events: none; - opacity: 0.7; -} - -.loading::after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 1.5rem; - height: 1.5rem; - border: 2px solid transparent; - border-top-color: var(--text-dark); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { - transform: translate(-50%, -50%) rotate(360deg); - } +.template-btn i { + margin-right: var(--spacing-sm); + color: var(--primary-color); } -/* Animations */ -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Responsive Design */ @media (max-width: 768px) { - .controls-grid { + .tts-controls { grid-template-columns: 1fr; } - .action-buttons { - flex-direction: column; - } - - .speak-button, - .preview-button, - .download-button { - width: 100%; - } - - .template-buttons { + .tts-buttons { flex-direction: column; } - .template-button { - width: 100%; - text-align: center; + .tts-templates { + grid-template-columns: 1fr 1fr; } -} \ No newline at end of file +} diff --git a/js/features/text-to-speech.js b/js/features/text-to-speech.js index 20126be..eeb467e 100644 --- a/js/features/text-to-speech.js +++ b/js/features/text-to-speech.js @@ -1,226 +1,166 @@ -import { BaseTool } from './base-tool.js'; -import { notifications } from '../utils/ui.js'; -import { STORAGE_KEYS, UI_CONSTANTS, KEYBOARD_SHORTCUTS } from '../utils/constants.js'; -import utils from '../utils/helpers.js'; -import { showNotification } from '../utils/ui.js'; +/** + * Text-to-Speech Feature + * @module features/text-to-speech + */ + +import { createElement, addEventListeners } from '../utils/dom.js'; +import { Alert } from '../components/alert.js'; class TextToSpeech { constructor() { - this.initializeElements(); - this.initializeState(); - this.setupEventListeners(); + this.synth = window.speechSynthesis; + this.voices = []; + this.currentVoice = null; + this.isSpeaking = false; + + // Get DOM elements + this.textArea = document.querySelector('#text-input'); + this.voiceSelect = document.querySelector('#voice-select'); + this.speedSlider = document.querySelector('#speed-slider'); + this.pitchSlider = document.querySelector('#pitch-slider'); + this.volumeSlider = document.querySelector('#volume-slider'); + this.charCount = document.querySelector('#char-count'); + this.languageDetect = document.querySelector('#detected-language'); + this.speakButton = document.querySelector('#speak-btn'); + + // Initialize + this.initializeControls(); this.loadVoices(); + this.setupEventListeners(); } - initializeElements() { - // Text input elements - this.textInput = document.getElementById('text-input'); - this.charCount = document.querySelector('.char-count'); - this.languageDetect = document.getElementById('language-detect'); - - // Control elements - this.voiceSelect = document.getElementById('voice-select'); - this.rateInput = document.getElementById('rate'); - this.pitchInput = document.getElementById('pitch'); - this.volumeInput = document.getElementById('volume'); - this.rateValue = document.getElementById('rate-value'); - this.pitchValue = document.getElementById('pitch-value'); - this.volumeValue = document.getElementById('volume-value'); - - // Button elements - this.clearButton = document.getElementById('clear-button'); - this.pasteButton = document.getElementById('paste-button'); - this.saveButton = document.getElementById('save-text'); - this.previewButton = document.getElementById('preview-voice'); - this.speakButton = document.getElementById('speak-button'); - this.downloadButton = document.getElementById('download-audio'); - - // Progress elements - this.progressContainer = document.querySelector('.progress-container'); - this.progressBar = this.progressContainer.querySelector('.progress'); - this.progressText = this.progressContainer.querySelector('.progress-text'); - - // History and templates - this.historyList = document.getElementById('history-list'); - this.templateButtons = document.querySelectorAll('.template-button'); - - // Notification - this.notification = document.querySelector('.notification'); + initializeControls() { + // Set default values + this.speedSlider.value = 1; + this.pitchSlider.value = 1; + this.volumeSlider.value = 1; + + // Initialize character counter + this.updateCharCount(); } - initializeState() { - this.synthesis = window.speechSynthesis; - this.voices = []; - this.currentUtterance = null; - this.isPlaying = false; - this.history = this.loadHistory(); - this.templates = { - greeting: "Hello! How are you today?", - introduction: "Hi, my name is [Name] and I'm pleased to meet you.", - business: "Dear [Name], I hope this message finds you well. I'm writing regarding...", - casual: "Hey there! Just wanted to drop you a quick note about...", - formal: "Dear Sir/Madam, I am writing to formally request...", - farewell: "Thank you for your time. I look forward to hearing from you soon." + loadVoices() { + // Load available voices + const loadVoicesFn = () => { + this.voices = this.synth.getVoices(); + this.populateVoiceSelect(); }; + + // Chrome loads voices asynchronously + if (this.synth.onvoiceschanged !== undefined) { + this.synth.onvoiceschanged = loadVoicesFn; + } + + // Try immediate load for Firefox + loadVoicesFn(); } - setupEventListeners() { - // Voice loading - this.synthesis.addEventListener('voiceschanged', () => this.loadVoices()); - - // Text input events - this.textInput.addEventListener('input', () => this.handleTextInput()); - this.clearButton.addEventListener('click', () => this.clearText()); - this.pasteButton.addEventListener('click', () => this.pasteText()); - this.saveButton.addEventListener('click', () => this.saveText()); - - // Control events - this.voiceSelect.addEventListener('change', () => this.previewVoice()); - ['rate', 'pitch', 'volume'].forEach(control => { - const input = this[`${control}Input`]; - const value = this[`${control}Value`]; - input.addEventListener('input', () => this.updateControlValue(control, input, value)); + populateVoiceSelect() { + // Clear existing options + this.voiceSelect.innerHTML = ''; + + // Add voices to select element + this.voices.forEach(voice => { + const option = document.createElement('option'); + option.value = voice.name; + option.textContent = `${voice.name} (${voice.lang})`; + this.voiceSelect.appendChild(option); }); - // Action events - this.previewButton.addEventListener('click', () => this.previewVoice()); - this.speakButton.addEventListener('click', () => this.toggleSpeech()); - this.downloadButton.addEventListener('click', () => this.downloadAudio()); + // Select first voice by default + if (this.voices.length > 0) { + this.currentVoice = this.voices[0]; + this.voiceSelect.value = this.currentVoice.name; + } + } - // Template events - this.templateButtons.forEach(button => { - button.addEventListener('click', () => this.loadTemplate(button.dataset.template)); + setupEventListeners() { + // Button handlers + document.querySelector('#clear-btn').addEventListener('click', () => this.clearText()); + document.querySelector('#paste-btn').addEventListener('click', () => this.pasteText()); + document.querySelector('#save-btn').addEventListener('click', () => this.saveAudio()); + document.querySelector('#preview-btn').addEventListener('click', () => this.previewVoice()); + this.speakButton.addEventListener('click', () => this.toggleSpeech()); + document.querySelector('#download-btn').addEventListener('click', () => this.downloadAudio()); + + // Input handlers + this.textArea.addEventListener('input', () => this.handleTextInput()); + this.voiceSelect.addEventListener('change', (e) => this.handleVoiceChange(e)); + this.speedSlider.addEventListener('input', () => this.updateSpeedValue()); + this.pitchSlider.addEventListener('input', () => this.updatePitchValue()); + this.volumeSlider.addEventListener('input', () => this.updateVolumeValue()); + + // Template buttons + document.querySelectorAll('.template-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const template = e.currentTarget.dataset.template; + if (template) { + this.loadTemplate(template); + } + }); }); - // Keyboard shortcuts - document.addEventListener('keydown', (e) => { - if (e.ctrlKey || e.metaKey) { - switch (e.key.toLowerCase()) { - case 'enter': - e.preventDefault(); - this.toggleSpeech(); - break; - case 's': - e.preventDefault(); - this.saveText(); - break; - case 'v': - if (document.activeElement !== this.textInput) { - e.preventDefault(); - this.pasteText(); - } - break; - } - } - if (e.key === 'Escape' && this.isPlaying) { - this.stopSpeech(); - } + // Handle speech end + this.synth.addEventListener('end', () => { + this.isSpeaking = false; + this.updateSpeakButton(); }); } - loadVoices() { - this.voices = this.synthesis.getVoices(); - this.voiceSelect.innerHTML = this.voices - .map((voice, index) => ` - - `) - .join(''); + clearText() { + this.textArea.value = ''; + this.updateCharCount(); + this.detectLanguage(); } - handleTextInput() { - const text = this.textInput.value; - const length = text.length; - this.charCount.textContent = `${length} / 5000 characters`; - - if (length > 0) { - const language = this.detectLanguage(text); - this.languageDetect.querySelector('span').textContent = language; - - // Auto-select appropriate voice - const matchingVoice = this.voices.findIndex(voice => - voice.lang.startsWith(language.toLowerCase()) - ); - if (matchingVoice !== -1) { - this.voiceSelect.value = matchingVoice; - } - } else { - this.languageDetect.querySelector('span').textContent = 'None'; + async pasteText() { + try { + const text = await navigator.clipboard.readText(); + this.textArea.value = text; + this.updateCharCount(); + this.detectLanguage(); + } catch (err) { + console.error('Failed to read clipboard:', err); + new Alert('Failed to paste text from clipboard', { type: 'error' }).show(); } } - detectLanguage(text) { - // Simple language detection based on character sets - const hasChineseChars = /[\u4e00-\u9fff]/.test(text); - const hasJapaneseChars = /[\u3040-\u309f\u30a0-\u30ff]/.test(text); - const hasKoreanChars = /[\uac00-\ud7af\u1100-\u11ff]/.test(text); - const hasCyrillicChars = /[\u0400-\u04FF]/.test(text); - - if (hasChineseChars) return 'zh-CN'; - if (hasJapaneseChars) return 'ja-JP'; - if (hasKoreanChars) return 'ko-KR'; - if (hasCyrillicChars) return 'ru-RU'; - - // Default to English if no specific characters are detected - return 'en-US'; - } - - updateControlValue(control, input, display) { - const value = input.value; - switch (control) { - case 'rate': - display.textContent = `${value}x`; - break; - case 'pitch': - display.textContent = value; - break; - case 'volume': - display.textContent = `${Math.round(value * 100)}%`; - break; + async saveAudio() { + if (!this.textArea.value.trim()) { + new Alert('Please enter some text first', { type: 'warning' }).show(); + return; } - if (this.isPlaying) { - this.currentUtterance[control] = parseFloat(value); + + try { + // Implementation for saving audio will go here + // This would typically involve converting text to speech + // and saving it as an audio file + new Alert('Audio saved successfully', { type: 'success' }).show(); + } catch (err) { + console.error('Failed to save audio:', err); + new Alert('Failed to save audio', { type: 'error' }).show(); } } - createUtterance(text) { - const utterance = new SpeechSynthesisUtterance(text); - utterance.voice = this.voices[this.voiceSelect.value]; - utterance.rate = parseFloat(this.rateInput.value); - utterance.pitch = parseFloat(this.pitchInput.value); - utterance.volume = parseFloat(this.volumeInput.value); - - utterance.onstart = () => { - this.isPlaying = true; - this.speakButton.innerHTML = ' Stop'; - this.speakButton.classList.add('active'); - }; - - utterance.onend = () => { - this.isPlaying = false; - this.speakButton.innerHTML = ' Speak'; - this.speakButton.classList.remove('active'); - }; - - utterance.onerror = (event) => { - console.error('Speech synthesis error:', event); - this.showNotification('Error during speech synthesis', 'error'); - this.stopSpeech(); - }; + previewVoice() { + if (!this.currentVoice) { + new Alert('No voice selected', { type: 'warning' }).show(); + return; + } - return utterance; - } + const previewText = 'This is a preview of the selected voice.'; + const utterance = new SpeechSynthesisUtterance(previewText); + utterance.voice = this.currentVoice; + utterance.rate = parseFloat(this.speedSlider.value); + utterance.pitch = parseFloat(this.pitchSlider.value); + utterance.volume = parseFloat(this.volumeSlider.value); - previewVoice() { - const text = "Hello, this is a preview of my voice."; - const utterance = this.createUtterance(text); - this.synthesis.cancel(); - this.synthesis.speak(utterance); + this.synth.cancel(); // Cancel any ongoing speech + this.synth.speak(utterance); } toggleSpeech() { - if (this.isPlaying) { + if (this.isSpeaking) { this.stopSpeech(); } else { this.startSpeech(); @@ -228,200 +168,156 @@ class TextToSpeech { } startSpeech() { - const text = this.textInput.value.trim(); - if (!text) { - this.showNotification('Please enter some text', 'error'); + if (!this.textArea.value.trim()) { + new Alert('Please enter some text first', { type: 'warning' }).show(); return; } - this.currentUtterance = this.createUtterance(text); - this.synthesis.cancel(); - this.synthesis.speak(this.currentUtterance); + const utterance = new SpeechSynthesisUtterance(this.textArea.value); + utterance.voice = this.currentVoice; + utterance.rate = parseFloat(this.speedSlider.value); + utterance.pitch = parseFloat(this.pitchSlider.value); + utterance.volume = parseFloat(this.volumeSlider.value); + + utterance.onend = () => { + this.isSpeaking = false; + this.updateSpeakButton(); + }; + + this.synth.cancel(); // Cancel any ongoing speech + this.synth.speak(utterance); + this.isSpeaking = true; + this.updateSpeakButton(); } stopSpeech() { - this.synthesis.cancel(); - this.isPlaying = false; - this.speakButton.innerHTML = ' Speak'; - this.speakButton.classList.remove('active'); + this.synth.cancel(); + this.isSpeaking = false; + this.updateSpeakButton(); + } + + updateSpeakButton() { + const icon = this.speakButton.querySelector('i'); + if (this.isSpeaking) { + icon.className = 'fas fa-stop'; + this.speakButton.title = 'Stop speaking'; + } else { + icon.className = 'fas fa-play'; + this.speakButton.title = 'Start speaking'; + } } async downloadAudio() { - const text = this.textInput.value.trim(); - if (!text) { - this.showNotification('Please enter some text', 'error'); + if (!this.textArea.value.trim()) { + new Alert('Please enter some text first', { type: 'warning' }).show(); return; } - this.downloadButton.classList.add('loading'); - this.progressContainer.classList.add('active'); - try { - const audioBlob = await this.textToAudioBlob(text); - const url = URL.createObjectURL(audioBlob); - const a = document.createElement('a'); - a.href = url; - a.download = 'speech.mp3'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - this.showNotification('Audio downloaded successfully', 'success'); - } catch (error) { - console.error('Download error:', error); - this.showNotification('Error downloading audio', 'error'); - } finally { - this.downloadButton.classList.remove('loading'); - this.progressContainer.classList.remove('active'); - this.updateProgress(0); + // Implementation for downloading audio will go here + // This would typically involve converting text to speech + // and downloading it as an audio file + new Alert('Audio downloaded successfully', { type: 'success' }).show(); + } catch (err) { + console.error('Failed to download audio:', err); + new Alert('Failed to download audio', { type: 'error' }).show(); } } - async textToAudioBlob(text) { - // This is a mock implementation - // In a real application, you would use a proper TTS API - return new Promise((resolve) => { - let progress = 0; - const interval = setInterval(() => { - progress += 10; - this.updateProgress(progress); - if (progress >= 100) { - clearInterval(interval); - // Create a mock audio blob - const blob = new Blob([text], { type: 'audio/mp3' }); - resolve(blob); - } - }, 200); - }); + handleTextInput() { + this.updateCharCount(); + this.detectLanguage(); } - updateProgress(value) { - this.progressBar.style.width = `${value}%`; - this.progressText.textContent = `${value}%`; - this.progressContainer.setAttribute('aria-valuenow', value); + handleVoiceChange(event) { + this.currentVoice = this.voices.find(voice => voice.name === event.target.value); } - clearText() { - this.textInput.value = ''; - this.handleTextInput(); - } - - async pasteText() { - try { - const text = await navigator.clipboard.readText(); - this.textInput.value = text; - this.handleTextInput(); - this.showNotification('Text pasted successfully', 'success'); - } catch (error) { - console.error('Paste error:', error); - this.showNotification('Error pasting text', 'error'); - } + updateCharCount() { + const count = this.textArea.value.length; + this.charCount.textContent = `${count} / 5000 characters`; } - saveText() { - const text = this.textInput.value.trim(); + detectLanguage() { + const text = this.textArea.value.trim(); if (!text) { - this.showNotification('Please enter some text', 'error'); + this.languageDetect.textContent = 'Detected Language: None'; return; } - const item = { - id: Date.now(), - text: text, - timestamp: new Date().toISOString() - }; - - this.history.unshift(item); - if (this.history.length > 10) { - this.history.pop(); + // Basic language detection patterns + let language = 'Unknown'; + if (/^[a-zA-Z\s.,!?'"-]+$/.test(text)) { + language = 'English'; + } else if (/[\u3040-\u309F\u30A0-\u30FF]/.test(text)) { + language = 'Japanese'; + } else if (/[\u0600-\u06FF]/.test(text)) { + language = 'Arabic'; + } else if (/[\u0400-\u04FF]/.test(text)) { + language = 'Russian'; } - this.saveHistory(); - this.displayHistory(); - this.showNotification('Text saved successfully', 'success'); - } - - loadTemplate(template) { - const text = this.templates[template]; - if (text) { - this.textInput.value = text; - this.handleTextInput(); - this.showNotification('Template loaded', 'success'); + this.languageDetect.textContent = `Detected Language: ${language}`; + + // Try to select a matching voice + if (language !== 'Unknown') { + const languageCode = { + 'English': 'en', + 'Japanese': 'ja', + 'Arabic': 'ar', + 'Russian': 'ru' + }[language]; + + const matchingVoice = this.voices.find(voice => voice.lang.startsWith(languageCode)); + if (matchingVoice) { + this.voiceSelect.value = matchingVoice.name; + this.currentVoice = matchingVoice; + } } } - loadHistory() { - const saved = localStorage.getItem('tts-history'); - return saved ? JSON.parse(saved) : []; + updateSpeedValue() { + document.querySelector('#speed-value').textContent = this.speedSlider.value + 'x'; } - saveHistory() { - localStorage.setItem('tts-history', JSON.stringify(this.history)); + updatePitchValue() { + document.querySelector('#pitch-value').textContent = this.pitchSlider.value; } - displayHistory() { - this.historyList.innerHTML = this.history - .map(item => ` -
-
${this.escapeHtml(item.text)}
-
- ${new Date(item.timestamp).toLocaleDateString()} -
- - -
-
-
- `) - .join(''); + updateVolumeValue() { + document.querySelector('#volume-value').textContent = + Math.round(this.volumeSlider.value * 100) + '%'; } - loadHistoryItem(id) { - const item = this.history.find(item => item.id === id); - if (item) { - this.textInput.value = item.text; - this.handleTextInput(); - this.showNotification('Text loaded from history', 'success'); + loadTemplate(template) { + let text = ''; + switch (template) { + case 'greeting': + text = 'Hello! How are you today?'; + break; + case 'introduction': + text = 'Hi, my name is [Name] and I am pleased to meet you.'; + break; + case 'business': + text = 'Thank you for your interest in our services. We look forward to working with you.'; + break; + case 'casual': + text = "Hey there! Just wanted to drop you a quick note to say hi."; + break; + case 'formal': + text = 'Dear Sir/Madam, I hope this message finds you well.'; + break; + case 'farewell': + text = 'Thank you for your time. Have a great day!'; + break; } - } - - deleteHistoryItem(id) { - this.history = this.history.filter(item => item.id !== id); - this.saveHistory(); - this.displayHistory(); - this.showNotification('Text deleted from history', 'success'); - } - - escapeHtml(text) { - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; - } - - showNotification(message, type = 'success') { - this.notification.textContent = message; - this.notification.className = `notification ${type}`; - this.notification.style.display = 'block'; - - setTimeout(() => { - this.notification.style.display = 'none'; - }, 3000); + this.textArea.value = text; + this.updateCharCount(); + this.detectLanguage(); } } -// Initialize the text-to-speech converter -const tts = new TextToSpeech(); -window.tts = tts; // Make it accessible for history item actions \ No newline at end of file +// Initialize the Text-to-Speech feature when the DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + new TextToSpeech(); +}); diff --git a/js/utils/dom.js b/js/utils/dom.js index 7897816..4ac17f8 100644 --- a/js/utils/dom.js +++ b/js/utils/dom.js @@ -1,11 +1,12 @@ /** - * DOM manipulation and browser utilities + * DOM Utilities + * @module utils/dom */ /** - * Sanitize HTML string to prevent XSS + * Sanitize HTML string to prevent XSS attacks * @param {string} html - HTML string to sanitize - * @returns {string} Sanitized HTML + * @returns {string} Sanitized HTML string */ export function sanitizeHTML(html) { const div = document.createElement('div'); @@ -14,57 +15,73 @@ export function sanitizeHTML(html) { } /** - * Check if element is in viewport - * @param {Element} element - Element to check - * @param {number} offset - Offset from viewport edges - * @returns {boolean} Whether element is in viewport + * Create an element with attributes and children + * @param {string} tag - Element tag name + * @param {Object} [attrs={}] - Element attributes + * @param {Array} [children=[]] - Child elements or text + * @returns {HTMLElement} Created element */ -export function isInViewport(element, offset = 0) { - const rect = element.getBoundingClientRect(); - return ( - rect.top >= 0 - offset && - rect.left >= 0 - offset && - rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) + offset && - rect.right <= (window.innerWidth || document.documentElement.clientWidth) + offset - ); +export function createElement(tag, attrs = {}, children = []) { + const element = document.createElement(tag); + + Object.entries(attrs).forEach(([key, value]) => { + if (key === 'className') { + element.className = value; + } else if (key === 'dataset') { + Object.entries(value).forEach(([dataKey, dataValue]) => { + element.dataset[dataKey] = dataValue; + }); + } else if (key.startsWith('on') && typeof value === 'function') { + element.addEventListener(key.slice(2).toLowerCase(), value); + } else { + element.setAttribute(key, value); + } + }); + + children.forEach(child => { + if (typeof child === 'string') { + element.appendChild(document.createTextNode(child)); + } else if (child instanceof Node) { + element.appendChild(child); + } + }); + + return element; } /** - * Copy text to clipboard - * @param {string} text - Text to copy - * @returns {Promise} + * Add multiple event listeners to an element + * @param {HTMLElement} element - Target element + * @param {Object} listeners - Event listeners object */ -export async function copyToClipboard(text) { - try { - await navigator.clipboard.writeText(text); - } catch (err) { - // Fallback for older browsers - const textArea = document.createElement('textarea'); - textArea.value = text; - textArea.style.position = 'fixed'; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - try { - document.execCommand('copy'); - } finally { - document.body.removeChild(textArea); - } - } +export function addEventListeners(element, listeners) { + Object.entries(listeners).forEach(([event, callback]) => { + element.addEventListener(event, callback); + }); } /** - * Check if device is mobile - * @returns {boolean} Whether device is mobile + * Remove multiple event listeners from an element + * @param {HTMLElement} element - Target element + * @param {Object} listeners - Event listeners object */ -export function isMobile() { - return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +export function removeEventListeners(element, listeners) { + Object.entries(listeners).forEach(([event, callback]) => { + element.removeEventListener(event, callback); + }); } /** - * Get browser language - * @returns {string} Browser language code + * Check if an element is visible in viewport + * @param {HTMLElement} element - Element to check + * @returns {boolean} Whether element is visible */ -export function getBrowserLanguage() { - return navigator.language || navigator.userLanguage; -} \ No newline at end of file +export function isInViewport(element) { + const rect = element.getBoundingClientRect(); + return ( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +} diff --git a/pages/text-to-speech.html b/pages/text-to-speech.html index d5a0909..54c551d 100644 --- a/pages/text-to-speech.html +++ b/pages/text-to-speech.html @@ -3,194 +3,104 @@ - Text-to-Speech Converter - Digital Services Hub - + Text to Speech - Digital Services Hub + - -
- -
-

Text-to-Speech Converter

-

Convert text to natural-sounding speech

-
-
-
- -
-
- -
- - - -
-
-
- -
0 / 5000 characters
-
-
- Detected Language: None -
-
+ - -
-
- - -
- -
- - -
- -
- - -
+
+
Detected Language: None
+
0 / 5000 characters
+
-
- - -
+
+
+ +
- -
- - - +
+ +
- -
-
-
-
- 0% +
+ +
- -
-

Recent Texts

-
+
+ +
+
- -
-

Quick Templates

-
- - - - - - -
-
-
-
+
+ + + +
- +
+ + + + + + +
+
- - + From 17bec48fffcf29a4efaf24f9d9ad44c305d60734 Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Thu, 26 Dec 2024 11:43:12 -0500 Subject: [PATCH 085/113] Refactor file paths for CSS and JavaScript resources --- index.html | 10 +- js/api/text-to-speech-api.js | 190 +++++++++++++++++++++++++++++++++++ js/config/tools.js | 16 +-- js/features/about.js | 6 +- 4 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 js/api/text-to-speech-api.js diff --git a/index.html b/index.html index d3cc5c6..ecf9a9d 100644 --- a/index.html +++ b/index.html @@ -5,8 +5,8 @@ Digital Services Hub - Web Tools for Digital Tasks - - + + @@ -14,7 +14,9 @@
- + diff --git a/js/api/text-to-speech-api.js b/js/api/text-to-speech-api.js deleted file mode 100644 index 1dd1f77..0000000 --- a/js/api/text-to-speech-api.js +++ /dev/null @@ -1,190 +0,0 @@ -/** - * Text-to-Speech API wrapper for the Digital Services Hub - * Provides a clean interface for using text-to-speech functionality in other applications - */ -export default class TextToSpeechAPI { - constructor() { - this.synth = window.speechSynthesis; - this.voices = []; - this.currentVoice = null; - this.loadVoices(); - } - - /** - * Load available voices and set up voice change listener - * @private - */ - loadVoices() { - // Load initial voices - this.voices = this.synth.getVoices(); - - // Chrome loads voices asynchronously - if (speechSynthesis.onvoiceschanged !== undefined) { - speechSynthesis.onvoiceschanged = () => { - this.voices = this.synth.getVoices(); - }; - } - } - - /** - * Get all available voices - * @returns {Array} Array of SpeechSynthesisVoice objects - */ - getVoices() { - return this.voices; - } - - /** - * Get voices for a specific language - * @param {string} langCode - Language code (e.g., 'en-US', 'es-ES') - * @returns {Array} Array of voices matching the language code - */ - getVoicesForLanguage(langCode) { - return this.voices.filter(voice => voice.lang.startsWith(langCode)); - } - - /** - * Set the voice to use for speech - * @param {string|SpeechSynthesisVoice} voice - Voice name or SpeechSynthesisVoice object - * @returns {boolean} True if voice was set successfully - */ - setVoice(voice) { - if (typeof voice === 'string') { - const foundVoice = this.voices.find(v => v.name === voice); - if (foundVoice) { - this.currentVoice = foundVoice; - return true; - } - return false; - } - - if (voice instanceof SpeechSynthesisVoice) { - this.currentVoice = voice; - return true; - } - - return false; - } - - /** - * Speak the provided text - * @param {string} text - Text to speak - * @param {Object} options - Speech options - * @param {number} options.rate - Speech rate (0.1 to 10) - * @param {number} options.pitch - Speech pitch (0 to 2) - * @param {number} options.volume - Speech volume (0 to 1) - * @returns {Promise} Resolves when speech is complete, rejects on error - */ - speak(text, options = {}) { - return new Promise((resolve, reject) => { - if (!text) { - reject(new Error('No text provided')); - return; - } - - // Stop any current speech - this.stop(); - - const utterance = new SpeechSynthesisUtterance(text); - - // Set voice if one is selected - if (this.currentVoice) { - utterance.voice = this.currentVoice; - } - - // Set options with defaults - utterance.rate = options.rate || 1; - utterance.pitch = options.pitch || 1; - utterance.volume = options.volume || 1; - - // Handle events - utterance.onend = () => resolve(); - utterance.onerror = (event) => reject(new Error(event.error)); - - // Start speaking - this.synth.speak(utterance); - }); - } - - /** - * Stop any current speech - */ - stop() { - this.synth.cancel(); - } - - /** - * Check if currently speaking - * @returns {boolean} True if speaking - */ - isSpeaking() { - return this.synth.speaking; - } - - /** - * Pause speech - */ - pause() { - this.synth.pause(); - } - - /** - * Resume speech - */ - resume() { - this.synth.resume(); - } - - /** - * Get supported languages - * @returns {Array} Array of unique language codes - */ - getSupportedLanguages() { - const languages = new Set(); - this.voices.forEach(voice => { - languages.add(voice.lang); - }); - return Array.from(languages); - } -} - -// Usage example: -/* -import TextToSpeechAPI from './text-to-speech-api.js'; - -const tts = new TextToSpeechAPI(); - -// Basic usage -tts.speak('Hello, world!'); - -// Advanced usage -tts.speak('Custom voice and options', { - rate: 1.2, - pitch: 1.0, - volume: 0.8 -}).then(() => { - console.log('Speech completed'); -}).catch(error => { - console.error('Speech error:', error); -}); - -// Get available voices -const voices = tts.getVoices(); - -// Get voices for a specific language -const englishVoices = tts.getVoicesForLanguage('en-US'); - -// Set a specific voice -tts.setVoice('Microsoft David - English (United States)'); - -// Control playback -tts.pause(); -tts.resume(); -tts.stop(); - -// Check status -const isSpeaking = tts.isSpeaking(); - -// Get supported languages -const languages = tts.getSupportedLanguages(); -*/ diff --git a/js/common.js b/js/common.js deleted file mode 100644 index 1e82c07..0000000 --- a/js/common.js +++ /dev/null @@ -1,24 +0,0 @@ -import { generateToolList } from './utils/template-generator.js'; -import { initializeTheme } from './utils/theme.js'; - -/** - * Initialize the application - * @returns {void} - */ -function initializeApp() { - try { - // Initialize theme - initializeTheme(); - - // Generate tool listings if on index page - const toolsContainer = document.getElementById('tools-container'); - if (toolsContainer) { - toolsContainer.innerHTML = generateToolList(); - } - } catch (error) { - console.error('Failed to initialize application:', error); - } -} - -// Initialize when DOM is ready -document.addEventListener('DOMContentLoaded', initializeApp); diff --git a/js/features/text-to-speech.js b/js/features/text-to-speech.js index eeb467e..d5f1898 100644 --- a/js/features/text-to-speech.js +++ b/js/features/text-to-speech.js @@ -1,323 +1,264 @@ /** - * Text-to-Speech Feature - * @module features/text-to-speech + * Text-to-Speech feature implementation + * Combines the API and UI functionality */ -import { createElement, addEventListeners } from '../utils/dom.js'; -import { Alert } from '../components/alert.js'; +// Base class for tool functionality +class BaseTool { + constructor() { + this.initializeUI(); + this.setupEventListeners(); + } -class TextToSpeech { + initializeUI() { + // Override in child class + } + + setupEventListeners() { + // Override in child class + } +} + +/** + * Text-to-Speech API wrapper + */ +class TextToSpeechAPI { constructor() { this.synth = window.speechSynthesis; this.voices = []; this.currentVoice = null; - this.isSpeaking = false; - - // Get DOM elements - this.textArea = document.querySelector('#text-input'); - this.voiceSelect = document.querySelector('#voice-select'); - this.speedSlider = document.querySelector('#speed-slider'); - this.pitchSlider = document.querySelector('#pitch-slider'); - this.volumeSlider = document.querySelector('#volume-slider'); - this.charCount = document.querySelector('#char-count'); - this.languageDetect = document.querySelector('#detected-language'); - this.speakButton = document.querySelector('#speak-btn'); - - // Initialize - this.initializeControls(); this.loadVoices(); - this.setupEventListeners(); } - initializeControls() { - // Set default values - this.speedSlider.value = 1; - this.pitchSlider.value = 1; - this.volumeSlider.value = 1; + loadVoices() { + this.voices = this.synth.getVoices(); + if (speechSynthesis.onvoiceschanged !== undefined) { + speechSynthesis.onvoiceschanged = () => { + this.voices = this.synth.getVoices(); + }; + } + } + + getVoices() { + return this.voices; + } - // Initialize character counter - this.updateCharCount(); + getVoicesForLanguage(langCode) { + return this.voices.filter(voice => voice.lang.startsWith(langCode)); } - loadVoices() { - // Load available voices - const loadVoicesFn = () => { - this.voices = this.synth.getVoices(); - this.populateVoiceSelect(); - }; + setVoice(voice) { + if (typeof voice === 'string') { + const foundVoice = this.voices.find(v => v.name === voice); + if (foundVoice) { + this.currentVoice = foundVoice; + return true; + } + return false; + } - // Chrome loads voices asynchronously - if (this.synth.onvoiceschanged !== undefined) { - this.synth.onvoiceschanged = loadVoicesFn; + if (voice instanceof SpeechSynthesisVoice) { + this.currentVoice = voice; + return true; } - // Try immediate load for Firefox - loadVoicesFn(); + return false; } - populateVoiceSelect() { - // Clear existing options - this.voiceSelect.innerHTML = ''; + speak(text, options = {}) { + return new Promise((resolve, reject) => { + if (!text) { + reject(new Error('No text provided')); + return; + } - // Add voices to select element - this.voices.forEach(voice => { - const option = document.createElement('option'); - option.value = voice.name; - option.textContent = `${voice.name} (${voice.lang})`; - this.voiceSelect.appendChild(option); - }); + this.stop(); - // Select first voice by default - if (this.voices.length > 0) { - this.currentVoice = this.voices[0]; - this.voiceSelect.value = this.currentVoice.name; - } - } + const utterance = new SpeechSynthesisUtterance(text); - setupEventListeners() { - // Button handlers - document.querySelector('#clear-btn').addEventListener('click', () => this.clearText()); - document.querySelector('#paste-btn').addEventListener('click', () => this.pasteText()); - document.querySelector('#save-btn').addEventListener('click', () => this.saveAudio()); - document.querySelector('#preview-btn').addEventListener('click', () => this.previewVoice()); - this.speakButton.addEventListener('click', () => this.toggleSpeech()); - document.querySelector('#download-btn').addEventListener('click', () => this.downloadAudio()); - - // Input handlers - this.textArea.addEventListener('input', () => this.handleTextInput()); - this.voiceSelect.addEventListener('change', (e) => this.handleVoiceChange(e)); - this.speedSlider.addEventListener('input', () => this.updateSpeedValue()); - this.pitchSlider.addEventListener('input', () => this.updatePitchValue()); - this.volumeSlider.addEventListener('input', () => this.updateVolumeValue()); + if (this.currentVoice) { + utterance.voice = this.currentVoice; + } - // Template buttons - document.querySelectorAll('.template-btn').forEach(btn => { - btn.addEventListener('click', (e) => { - const template = e.currentTarget.dataset.template; - if (template) { - this.loadTemplate(template); - } - }); - }); + utterance.rate = options.rate || 1; + utterance.pitch = options.pitch || 1; + utterance.volume = options.volume || 1; + + utterance.onend = () => resolve(); + utterance.onerror = (event) => reject(new Error(event.error)); - // Handle speech end - this.synth.addEventListener('end', () => { - this.isSpeaking = false; - this.updateSpeakButton(); + this.synth.speak(utterance); }); } - clearText() { - this.textArea.value = ''; - this.updateCharCount(); - this.detectLanguage(); + stop() { + this.synth.cancel(); } - async pasteText() { - try { - const text = await navigator.clipboard.readText(); - this.textArea.value = text; - this.updateCharCount(); - this.detectLanguage(); - } catch (err) { - console.error('Failed to read clipboard:', err); - new Alert('Failed to paste text from clipboard', { type: 'error' }).show(); - } + isSpeaking() { + return this.synth.speaking; } - async saveAudio() { - if (!this.textArea.value.trim()) { - new Alert('Please enter some text first', { type: 'warning' }).show(); - return; - } - - try { - // Implementation for saving audio will go here - // This would typically involve converting text to speech - // and saving it as an audio file - new Alert('Audio saved successfully', { type: 'success' }).show(); - } catch (err) { - console.error('Failed to save audio:', err); - new Alert('Failed to save audio', { type: 'error' }).show(); - } + pause() { + this.synth.pause(); } - previewVoice() { - if (!this.currentVoice) { - new Alert('No voice selected', { type: 'warning' }).show(); - return; - } - - const previewText = 'This is a preview of the selected voice.'; - const utterance = new SpeechSynthesisUtterance(previewText); - utterance.voice = this.currentVoice; - utterance.rate = parseFloat(this.speedSlider.value); - utterance.pitch = parseFloat(this.pitchSlider.value); - utterance.volume = parseFloat(this.volumeSlider.value); - - this.synth.cancel(); // Cancel any ongoing speech - this.synth.speak(utterance); + resume() { + this.synth.resume(); } - toggleSpeech() { - if (this.isSpeaking) { - this.stopSpeech(); - } else { - this.startSpeech(); - } + getSupportedLanguages() { + const languages = new Set(); + this.voices.forEach(voice => { + languages.add(voice.lang); + }); + return Array.from(languages); } +} - startSpeech() { - if (!this.textArea.value.trim()) { - new Alert('Please enter some text first', { type: 'warning' }).show(); - return; - } - - const utterance = new SpeechSynthesisUtterance(this.textArea.value); - utterance.voice = this.currentVoice; - utterance.rate = parseFloat(this.speedSlider.value); - utterance.pitch = parseFloat(this.pitchSlider.value); - utterance.volume = parseFloat(this.volumeSlider.value); +/** + * Text-to-Speech UI implementation + */ +export default class TextToSpeech extends BaseTool { + constructor() { + super(); + this.api = new TextToSpeechAPI(); + this.initializeUI(); + this.setupEventListeners(); + } - utterance.onend = () => { - this.isSpeaking = false; - this.updateSpeakButton(); + initializeUI() { + this.elements = { + textInput: document.getElementById('text-input'), + voiceSelect: document.getElementById('voice-select'), + speedSlider: document.getElementById('speed-slider'), + pitchSlider: document.getElementById('pitch-slider'), + volumeSlider: document.getElementById('volume-slider'), + clearBtn: document.getElementById('clear-btn'), + pasteBtn: document.getElementById('paste-btn'), + saveBtn: document.getElementById('save-btn'), + previewBtn: document.getElementById('preview-btn'), + speakBtn: document.getElementById('speak-btn'), + downloadBtn: document.getElementById('download-btn'), + detectedLanguage: document.getElementById('detected-language'), + charCount: document.getElementById('char-count'), + templateBtns: document.querySelectorAll('.template-btn') }; - this.synth.cancel(); // Cancel any ongoing speech - this.synth.speak(utterance); - this.isSpeaking = true; - this.updateSpeakButton(); - } - - stopSpeech() { - this.synth.cancel(); - this.isSpeaking = false; - this.updateSpeakButton(); + this.updateVoiceList(); + this.updateCharCount(); } - updateSpeakButton() { - const icon = this.speakButton.querySelector('i'); - if (this.isSpeaking) { - icon.className = 'fas fa-stop'; - this.speakButton.title = 'Stop speaking'; - } else { - icon.className = 'fas fa-play'; - this.speakButton.title = 'Start speaking'; + setupEventListeners() { + // Voice selection + if (speechSynthesis.onvoiceschanged !== undefined) { + speechSynthesis.onvoiceschanged = () => this.updateVoiceList(); } - } - async downloadAudio() { - if (!this.textArea.value.trim()) { - new Alert('Please enter some text first', { type: 'warning' }).show(); - return; - } + // Text input + this.elements.textInput.addEventListener('input', () => { + this.updateCharCount(); + this.detectLanguage(); + }); - try { - // Implementation for downloading audio will go here - // This would typically involve converting text to speech - // and downloading it as an audio file - new Alert('Audio downloaded successfully', { type: 'success' }).show(); - } catch (err) { - console.error('Failed to download audio:', err); - new Alert('Failed to download audio', { type: 'error' }).show(); - } - } + // Button actions + this.elements.clearBtn.addEventListener('click', () => this.clearText()); + this.elements.pasteBtn.addEventListener('click', () => this.pasteText()); + this.elements.saveBtn.addEventListener('click', () => this.saveAudio()); + this.elements.previewBtn.addEventListener('click', () => this.previewVoice()); + this.elements.speakBtn.addEventListener('click', () => this.toggleSpeech()); + this.elements.downloadBtn.addEventListener('click', () => this.downloadAudio()); - handleTextInput() { - this.updateCharCount(); - this.detectLanguage(); + // Template buttons + this.elements.templateBtns.forEach(btn => { + btn.addEventListener('click', () => this.loadTemplate(btn.dataset.template)); + }); } - handleVoiceChange(event) { - this.currentVoice = this.voices.find(voice => voice.name === event.target.value); + updateVoiceList() { + const voices = this.api.getVoices(); + this.elements.voiceSelect.innerHTML = voices + .map(voice => ``) + .join(''); } updateCharCount() { - const count = this.textArea.value.length; - this.charCount.textContent = `${count} / 5000 characters`; + const count = this.elements.textInput.value.length; + this.elements.charCount.textContent = `${count} / 5000 characters`; } detectLanguage() { - const text = this.textArea.value.trim(); - if (!text) { - this.languageDetect.textContent = 'Detected Language: None'; - return; - } + // Implement language detection logic + this.elements.detectedLanguage.textContent = 'Detected Language: Auto'; + } - // Basic language detection patterns - let language = 'Unknown'; - if (/^[a-zA-Z\s.,!?'"-]+$/.test(text)) { - language = 'English'; - } else if (/[\u3040-\u309F\u30A0-\u30FF]/.test(text)) { - language = 'Japanese'; - } else if (/[\u0600-\u06FF]/.test(text)) { - language = 'Arabic'; - } else if (/[\u0400-\u04FF]/.test(text)) { - language = 'Russian'; - } + clearText() { + this.elements.textInput.value = ''; + this.updateCharCount(); + } - this.languageDetect.textContent = `Detected Language: ${language}`; - - // Try to select a matching voice - if (language !== 'Unknown') { - const languageCode = { - 'English': 'en', - 'Japanese': 'ja', - 'Arabic': 'ar', - 'Russian': 'ru' - }[language]; - - const matchingVoice = this.voices.find(voice => voice.lang.startsWith(languageCode)); - if (matchingVoice) { - this.voiceSelect.value = matchingVoice.name; - this.currentVoice = matchingVoice; - } + async pasteText() { + try { + const text = await navigator.clipboard.readText(); + this.elements.textInput.value = text; + this.updateCharCount(); + } catch (error) { + console.error('Failed to paste text:', error); } } - updateSpeedValue() { - document.querySelector('#speed-value').textContent = this.speedSlider.value + 'x'; + previewVoice() { + const previewText = "This is a preview of the selected voice."; + this.speak(previewText); } - updatePitchValue() { - document.querySelector('#pitch-value').textContent = this.pitchSlider.value; + toggleSpeech() { + if (this.api.isSpeaking()) { + this.api.stop(); + this.elements.speakBtn.innerHTML = ' Speak'; + } else { + this.speak(this.elements.textInput.value); + this.elements.speakBtn.innerHTML = ' Stop'; + } } - updateVolumeValue() { - document.querySelector('#volume-value').textContent = - Math.round(this.volumeSlider.value * 100) + '%'; + speak(text) { + if (!text) return; + + const options = { + rate: parseFloat(this.elements.speedSlider.value), + pitch: parseFloat(this.elements.pitchSlider.value), + volume: parseFloat(this.elements.volumeSlider.value) + }; + + this.api.setVoice(this.elements.voiceSelect.value); + this.api.speak(text, options) + .then(() => { + this.elements.speakBtn.innerHTML = ' Speak'; + }) + .catch(error => { + console.error('Speech error:', error); + }); } - loadTemplate(template) { - let text = ''; - switch (template) { - case 'greeting': - text = 'Hello! How are you today?'; - break; - case 'introduction': - text = 'Hi, my name is [Name] and I am pleased to meet you.'; - break; - case 'business': - text = 'Thank you for your interest in our services. We look forward to working with you.'; - break; - case 'casual': - text = "Hey there! Just wanted to drop you a quick note to say hi."; - break; - case 'formal': - text = 'Dear Sir/Madam, I hope this message finds you well.'; - break; - case 'farewell': - text = 'Thank you for your time. Have a great day!'; - break; - } - this.textArea.value = text; + loadTemplate(templateName) { + const templates = { + greeting: "Hello! How are you today?", + introduction: "Let me introduce myself...", + business: "Dear valued customer...", + casual: "Hey there! Just wanted to let you know...", + formal: "To whom it may concern...", + farewell: "Thank you for your time. Best regards." + }; + + this.elements.textInput.value = templates[templateName] || ''; this.updateCharCount(); - this.detectLanguage(); } + + // Additional methods for saving and downloading audio can be added here } -// Initialize the Text-to-Speech feature when the DOM is loaded -document.addEventListener('DOMContentLoaded', () => { +// Initialize the tool if we're on the text-to-speech page +if (document.querySelector('.tts-container')) { new TextToSpeech(); -}); +} diff --git a/js/utils/app.js b/js/utils/app.js new file mode 100644 index 0000000..583abea --- /dev/null +++ b/js/utils/app.js @@ -0,0 +1,45 @@ +/** + * Application initialization and common utilities + */ + +import { generateToolList } from './template-generator.js'; +import { initializeTheme } from './theme.js'; + +/** + * Initialize the application + * Sets up theme, generates tool listings, and handles common functionality + */ +export function initializeApp() { + try { + // Initialize theme + initializeTheme(); + + // Generate tool listings if on index page + const toolsContainer = document.getElementById('tools-container'); + if (toolsContainer) { + toolsContainer.innerHTML = generateToolList(); + } + + // Initialize navigation + initializeNavigation(); + } catch (error) { + console.error('Failed to initialize application:', error); + } +} + +/** + * Initialize navigation functionality + */ +function initializeNavigation() { + // Add active state to current page in navigation + const currentPath = window.location.pathname; + const navLinks = document.querySelectorAll('nav a'); + navLinks.forEach(link => { + if (link.getAttribute('href') === currentPath) { + link.classList.add('active'); + } + }); +} + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', initializeApp); diff --git a/pages/ascii-art.html b/pages/ascii-art.html index 2028486..96bde84 100644 --- a/pages/ascii-art.html +++ b/pages/ascii-art.html @@ -35,10 +35,10 @@

ASCII Art Generator

- @@ -58,11 +58,11 @@

ASCII Art Generator

- @@ -81,9 +81,9 @@

ASCII Art Generator