1
0
Fork 0
mirror of https://github.com/maybe-finance/maybe.git synced 2025-07-22 22:59:39 +02:00

Feature: Implement Mobile Responsiveness (#2092)

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* format

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* fix conflict

* fix conflict

* chore: run rubocop

* fix test

* update PWA logo

* fix tests

* chore: lint

* fix test

* Refactor: Remove duplicate data attribute in activity partial and add chat form rendering in chats index

---------

Co-authored-by: Josh Pigford <josh@joshpigford.com>
This commit is contained in:
neo773 2025-04-18 18:53:10 +05:30 committed by GitHub
parent 6a21f26d2d
commit 65e1bc6edd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
91 changed files with 1333 additions and 527 deletions

View file

@ -0,0 +1,74 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "fileName", "uploadArea", "uploadText"]
connect() {
if (this.hasInputTarget) {
this.inputTarget.addEventListener("change", this.fileSelected.bind(this))
}
// Find the form element
this.form = this.element.closest("form")
if (this.form) {
this.form.addEventListener("turbo:submit-start", this.formSubmitting.bind(this))
}
}
disconnect() {
if (this.hasInputTarget) {
this.inputTarget.removeEventListener("change", this.fileSelected.bind(this))
}
if (this.form) {
this.form.removeEventListener("turbo:submit-start", this.formSubmitting.bind(this))
}
}
triggerFileInput() {
if (this.hasInputTarget) {
this.inputTarget.click()
}
}
fileSelected() {
if (this.hasInputTarget && this.inputTarget.files.length > 0) {
const fileName = this.inputTarget.files[0].name
if (this.hasFileNameTarget) {
// Find the paragraph element inside the fileName target
const fileNameText = this.fileNameTarget.querySelector('p')
if (fileNameText) {
fileNameText.textContent = fileName
}
this.fileNameTarget.classList.remove("hidden")
}
if (this.hasUploadTextTarget) {
this.uploadTextTarget.classList.add("hidden")
}
}
}
formSubmitting() {
if (this.hasFileNameTarget && this.hasInputTarget && this.inputTarget.files.length > 0) {
const fileNameText = this.fileNameTarget.querySelector('p')
if (fileNameText) {
fileNameText.textContent = `Uploading ${this.inputTarget.files[0].name}...`
}
// Change the icon to a loader
const iconContainer = this.fileNameTarget.querySelector('.lucide-file-text')
if (iconContainer) {
iconContainer.classList.add('animate-pulse')
}
}
if (this.hasUploadAreaTarget) {
this.uploadAreaTarget.classList.add("opacity-70")
}
}
}

View file

@ -0,0 +1,149 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="mobile-cell-interaction"
export default class extends Controller {
static targets = ["field", "highlight", "errorTooltip", "errorIcon"];
static values = { error: String };
touchTimeout = null;
activeTooltip = null;
documentClickHandler = null;
connect() {
this.documentClickHandler = this.handleDocumentClick.bind(this);
document.addEventListener('click', this.documentClickHandler);
}
disconnect() {
if (this.documentClickHandler) {
document.removeEventListener('click', this.documentClickHandler);
}
}
handleDocumentClick(event) {
if (event.target.closest('[data-mobile-cell-interaction-target="errorTooltip"]') ||
event.target.closest('[data-mobile-cell-interaction-target="errorIcon"]')) {
return;
}
this.hideAllErrorTooltips();
}
highlightCell(event) {
const field = event.target;
const highlight = this.findHighlightForField(field);
if (highlight) {
highlight.style.opacity = '1';
}
}
unhighlightCell(event) {
const field = event.target;
const highlight = this.findHighlightForField(field);
if (highlight) {
highlight.style.opacity = '0';
}
this.hideAllErrorTooltips();
}
handleCellTouch(event) {
if (this.touchTimeout) {
clearTimeout(this.touchTimeout);
}
const field = event.target;
const highlight = this.findHighlightForField(field);
if (highlight) {
highlight.style.opacity = '1';
this.touchTimeout = window.setTimeout(() => {
if (document.activeElement !== field) {
highlight.style.opacity = '0';
}
}, 1000);
}
if (this.hasErrorValue && this.errorValue) {
this.showErrorTooltip();
}
}
toggleErrorMessage(event) {
const errorIcon = event.currentTarget;
const cellContainer = errorIcon.closest('div');
const field = cellContainer.querySelector('input');
if (field) {
field.focus();
}
const tooltip = this.errorTooltipTarget;
this.hideAllTooltipsExcept(tooltip);
if (tooltip.classList.contains('hidden')) {
tooltip.classList.remove('hidden');
this.activeTooltip = tooltip;
setTimeout(() => {
if (tooltip === this.activeTooltip) {
tooltip.classList.add('hidden');
this.activeTooltip = null;
}
}, 3000);
} else {
tooltip.classList.add('hidden');
this.activeTooltip = null;
}
event.stopPropagation();
}
showErrorTooltip() {
if (this.hasErrorTooltipTarget) {
const tooltip = this.errorTooltipTarget;
tooltip.classList.remove('hidden');
this.activeTooltip = tooltip;
setTimeout(() => {
if (tooltip === this.activeTooltip) {
tooltip.classList.add('hidden');
this.activeTooltip = null;
}
}, 3000);
}
}
hideAllErrorTooltips() {
document.querySelectorAll('[data-mobile-cell-interaction-target="errorTooltip"]').forEach(tooltip => {
tooltip.classList.add('hidden');
});
this.activeTooltip = null;
}
hideAllTooltipsExcept(tooltipToKeep) {
document.querySelectorAll('[data-mobile-cell-interaction-target="errorTooltip"]').forEach(tooltip => {
if (tooltip !== tooltipToKeep) {
tooltip.classList.add('hidden');
}
});
}
selectCell(event) {
const errorIcon = event.currentTarget;
const cellContainer = errorIcon.closest('div');
const field = cellContainer.querySelector('input');
if (field) {
field.focus();
event.stopPropagation();
}
}
findHighlightForField(field) {
const container = field.closest('div');
return container ? container.querySelector('[data-mobile-cell-interaction-target="highlight"]') : null;
}
}

View file

@ -0,0 +1,63 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="password-validator"
export default class extends Controller {
static targets = ["input", "requirementType", "blockLine"];
connect() {
this.validate();
}
validate() {
const password = this.inputTarget.value;
let requirementsMet = 0;
// Check each requirement and count how many are met
const lengthValid = password.length >= 8;
const caseValid = /[A-Z]/.test(password) && /[a-z]/.test(password);
const numberValid = /\d/.test(password);
const specialValid = /[!@#$%^&*(),.?":{}|<>]/.test(password);
// Update individual requirement text
this.validateRequirementText("length", lengthValid);
this.validateRequirementText("case", caseValid);
this.validateRequirementText("number", numberValid);
this.validateRequirementText("special", specialValid);
// Count total requirements met
if (lengthValid) requirementsMet++;
if (caseValid) requirementsMet++;
if (numberValid) requirementsMet++;
if (specialValid) requirementsMet++;
// Update block lines sequentially
this.updateBlockLines(requirementsMet);
}
validateRequirementText(type, isValid) {
this.requirementTypeTargets.forEach((target) => {
if (target.dataset.requirementType === type) {
if (isValid) {
target.classList.remove("text-secondary");
target.classList.add("text-green-600");
} else {
target.classList.remove("text-green-600");
target.classList.add("text-secondary");
}
}
});
}
updateBlockLines(requirementsMet) {
// Update block lines sequentially based on total requirements met
this.blockLineTargets.forEach((line, index) => {
if (index < requirementsMet) {
line.classList.remove("bg-gray-200");
line.classList.add("bg-green-600");
} else {
line.classList.remove("bg-green-600");
line.classList.add("bg-gray-200");
}
});
}
}

View file

@ -0,0 +1,19 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="password-visibility"
export default class extends Controller {
static targets = ["input", "showIcon", "hideIcon"];
connect() {
this.hideIconTarget.classList.add("hidden");
}
toggle() {
const input = this.inputTarget;
const type = input.type === "password" ? "text" : "password";
input.type = type;
this.showIconTarget.classList.toggle("hidden");
this.hideIconTarget.classList.toggle("hidden");
}
}

View file

@ -0,0 +1,39 @@
/*
https://dev.to/konnorrogers/maintain-scroll-position-in-turbo-without-data-turbo-permanent-2b1i
modified to add support for horizontal scrolling
*/
if (!window.scrollPositions) {
window.scrollPositions = {};
}
function preserveScroll() {
document.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
scrollPositions[element.id] = {
top: element.scrollTop,
left: element.scrollLeft
};
});
}
function restoreScroll(event) {
document.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
if (scrollPositions[element.id]) {
element.scrollTop = scrollPositions[element.id].top;
element.scrollLeft = scrollPositions[element.id].left;
}
});
if (!event.detail.newBody) return;
// event.detail.newBody is the body element to be swapped in.
// https://turbo.hotwired.dev/reference/events
event.detail.newBody.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
if (scrollPositions[element.id]) {
element.scrollTop = scrollPositions[element.id].top;
element.scrollLeft = scrollPositions[element.id].left;
}
});
}
window.addEventListener("turbo:before-cache", preserveScroll);
window.addEventListener("turbo:before-render", restoreScroll);
window.addEventListener("turbo:render", restoreScroll);

View file

@ -8,6 +8,9 @@ export default class extends Controller {
"deleteProfileImage",
"input",
"clearBtn",
"uploadText",
"changeText",
"cameraIcon"
];
clearFileInput() {
@ -17,6 +20,12 @@ export default class extends Controller {
this.attachedImageTarget.classList.add("hidden");
this.previewImageTarget.classList.add("hidden");
this.deleteProfileImageTarget.value = "1";
this.uploadTextTarget.classList.remove("hidden");
this.changeTextTarget.classList.add("hidden");
this.changeTextTarget.setAttribute("aria-hidden", "true");
this.uploadTextTarget.setAttribute("aria-hidden", "false");
this.cameraIconTarget.classList.remove("!hidden");
}
showFileInputPreview(event) {
@ -28,7 +37,11 @@ export default class extends Controller {
this.previewImageTarget.classList.remove("hidden");
this.clearBtnTarget.classList.remove("hidden");
this.deleteProfileImageTarget.value = "0";
this.uploadTextTarget.classList.add("hidden");
this.changeTextTarget.classList.remove("hidden");
this.changeTextTarget.setAttribute("aria-hidden", "false");
this.uploadTextTarget.setAttribute("aria-hidden", "true");
this.cameraIconTarget.classList.add("!hidden");
this.previewImageTarget.querySelector("img").src =
URL.createObjectURL(file);
}

View file

@ -50,7 +50,8 @@ export default class extends Controller {
}
systemPrefersDark() {
return window.matchMedia("(prefers-color-scheme: dark)").matches
return false
// return window.matchMedia("(prefers-color-scheme: dark)").matches
}
handleSystemThemeChange = (event) => {