This commit is contained in:
Chris Veleris 2024-11-13 12:13:38 +02:00
parent 085fb25de9
commit cbc53822f4
20 changed files with 17 additions and 644 deletions

View file

@ -104,7 +104,7 @@ const App: React.FC = () => {
{currentUser ? (
<Layout
currentUser={currentUser}
setCurrentUser={setCurrentUser} // Make sure to pass this down
setCurrentUser={setCurrentUser}
isDarkMode={isDarkMode}
toggleDarkMode={toggleDarkMode}
>

View file

@ -54,7 +54,6 @@ const AreaModal: React.FC<AreaModalProps> = ({ isOpen, onClose, area, onSave })
};
}, [isOpen]);
// Handle Escape key to close modal
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {

View file

@ -53,8 +53,8 @@ const Navbar: React.FC<NavbarProps> = ({
});
if (response.ok) {
setCurrentUser(null); // Update the application state
navigate("/login"); // Redirect to the login page
setCurrentUser(null);
navigate("/login");
} else {
console.error("Failed to log out");
}

View file

@ -1,5 +1,3 @@
// app/frontend/components/Note/NoteModal.tsx
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Note } from '../../entities/Note';
import { useDataContext } from '../../contexts/DataContext';
@ -31,7 +29,6 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
const { showSuccessToast, showErrorToast } = useToast();
// Fetch available tags when modal is opened
useEffect(() => {
if (isOpen) {
fetch('/api/tags')
@ -44,7 +41,6 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
}
}, [isOpen, showErrorToast]);
// Reset form data when modal opens or note changes
useEffect(() => {
if (isOpen) {
setFormData({
@ -58,7 +54,6 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
}
}, [isOpen, note]);
// Handle click outside to close modal
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
@ -77,7 +72,6 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
};
}, [isOpen]);
// Handle Escape key to close modal
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
@ -105,7 +99,6 @@ const NoteModal: React.FC<NoteModalProps> = ({ isOpen, onClose, note, onSave })
const handleTagsChange = useCallback((newTags: string[]) => {
setTags(newTags);
// Map newTags to Tag objects with 'id' if they exist in availableTags
const updatedTags: Tag[] = newTags.map((name) => {
const existingTag = availableTags.find((tag) => tag.name === name);
return existingTag ? { id: existingTag.id, name } : { name };

View file

@ -1,5 +1,3 @@
// app/frontend/components/Project/ProjectDetails.tsx
import React, { useEffect, useState } from "react";
import { useParams, useLocation, useNavigate, Link } from "react-router-dom";
import {
@ -34,7 +32,6 @@ const ProjectDetails: React.FC = () => {
const projectTitle = stateTitle || project?.name || "Project";
const projectIcon = stateIcon;
// State for Collapsible Completed Tasks
const [isCompletedOpen, setIsCompletedOpen] = useState(false);
useEffect(() => {
@ -169,7 +166,6 @@ const ProjectDetails: React.FC = () => {
);
}
// Separate tasks into active and completed
const activeTasks = tasks.filter(task => task.status !== 'done');
const completedTasks = tasks.filter(task => task.status === 'done');
@ -231,7 +227,7 @@ const ProjectDetails: React.FC = () => {
handleTaskCreate({
name: taskName,
status: "not_started",
project_id: project?.id, // Ensure project_id is correctly assigned
project_id: project?.id,
})
}
/>

View file

@ -68,7 +68,6 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
};
}, [isOpen]);
// Handle Escape key to close modal
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {

View file

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { ChevronDownIcon, ArrowDownIcon, ArrowUpIcon, FireIcon } from '@heroicons/react/24/outline'; // Import the icons
import { ChevronDownIcon, ArrowDownIcon, ArrowUpIcon, FireIcon } from '@heroicons/react/24/outline';
import { PriorityType } from '../../entities/Task';
interface PriorityDropdownProps {

View file

@ -1,5 +1,3 @@
// app/frontend/components/Tag/TagInput.tsx
import React, { useState } from 'react';
import TaskTags from '../Task/TaskTags';
import { Tag } from '../../entities/Tag';

View file

@ -55,7 +55,6 @@ const TagModal: React.FC<TagModalProps> = ({
};
}, [isOpen]);
// Handle Escape key to close modal
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
@ -87,14 +86,9 @@ const TagModal: React.FC<TagModalProps> = ({
setIsSubmitting(true);
try {
// Assuming you have createTag and updateTag functions
if (tag) {
// Update existing tag
// await updateTag(formData.id, formData);
showSuccessToast('Tag updated successfully!');
} else {
// Create new tag
// await createTag(formData);
showSuccessToast('Tag created successfully!');
}
onSave(formData);

View file

@ -1,5 +1,3 @@
// app/frontend/components/Task/TaskHeader.tsx
import React from "react";
import TaskPriorityIcon from "./TaskPriorityIcon";
import TaskTags from "./TaskTags";

View file

@ -39,9 +39,9 @@ const TaskModal: React.FC<TaskModalProps> = ({
const [dropdownOpen, setDropdownOpen] = useState(false);
const modalRef = useRef<HTMLDivElement>(null);
const [isClosing, setIsClosing] = useState(false);
const [showConfirmDialog, setShowConfirmDialog] = useState(false); // State to control confirm dialog
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { showSuccessToast, showErrorToast } = useToast(); // Use toast functions
const { showSuccessToast, showErrorToast } = useToast();
useEffect(() => {
setFormData(task);
@ -118,7 +118,7 @@ const TaskModal: React.FC<TaskModalProps> = ({
};
const handleDeleteClick = () => {
setShowConfirmDialog(true); // Show confirmation dialog
setShowConfirmDialog(true);
};
const handleDeleteConfirm = () => {
@ -138,7 +138,6 @@ const TaskModal: React.FC<TaskModalProps> = ({
}, 300);
};
// Handler to remove tag
const handleTagRemove = (tagId: string | number | undefined) => {
if (tagId === undefined) return;
const tagIndex = Number(tagId);
@ -174,7 +173,6 @@ const TaskModal: React.FC<TaskModalProps> = ({
};
}, [isOpen]);
// Handle Escape key to close modal
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {

View file

@ -1,5 +1,3 @@
// app/frontend/components/Tasks.tsx
import React, { useEffect, useState, useRef } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import TaskList from "./Task/TaskList";

View file

@ -1,9 +1,7 @@
// app/frontend/contexts/DataContext.tsx
import React, { createContext, useContext } from 'react';
import useFetchTags from '../hooks/useFetchTags';
import useFetchAreas from '../hooks/useFetchAreas';
import useFetchProjects from '../hooks/useFetchProjects'; // Use the updated hook
import useFetchProjects from '../hooks/useFetchProjects';
import useManageAreas from '../hooks/useManageAreas';
import useManageNotes from '../hooks/useManageNotes';
import useManageProjects from '../hooks/useManageProjects';
@ -37,7 +35,7 @@ interface DataContextProps {
mutateTags: () => void;
mutateAreas: () => void;
mutateNotes: () => void;
mutateProjects: () => void; // Include mutateProjects
mutateProjects: () => void;
}
const DataContext = createContext<DataContextProps | undefined>(undefined);
@ -58,7 +56,7 @@ export const DataProvider: React.FC<{ children: React.ReactNode }> = ({ children
isLoading: isLoadingProjects,
isError: isErrorProjects,
mutate: mutateProjects,
} = useFetchProjects(); // Use the updated hook without options
} = useFetchProjects();
const { createArea, updateArea, deleteArea } = useManageAreas();
const { createProject, updateProject, deleteProject } = useManageProjects();
const { createTag, updateTag, deleteTag } = useManageTags();
@ -111,7 +109,7 @@ export const DataProvider: React.FC<{ children: React.ReactNode }> = ({ children
mutateTags,
mutateAreas,
mutateNotes,
mutateProjects, // Include mutateProjects
mutateProjects,
}}
>
{children}

View file

@ -1,5 +1,3 @@
// app/frontend/hooks/useFetchProjects.ts
import { useState, useEffect } from 'react';
import { Project } from '../entities/Project';

View file

@ -83,7 +83,7 @@ const useManageTasks = () => {
}
};
const mutateTasks = fetchTasks; // Expose fetchTasks as mutateTasks
const mutateTasks = fetchTasks;
return { tasks, isLoading, isError, fetchTasks, mutateTasks, createTask, updateTask, deleteTask };
};

View file

@ -1,6 +1,6 @@
import React from "react";
import { createRoot } from "react-dom/client"; // Import createRoot from react-dom
import { BrowserRouter } from "react-router-dom"; // Import BrowserRouter
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import { ToastProvider } from "./components/Shared/ToastContext";

View file

@ -1,299 +0,0 @@
body {
font-family: 'Poppins', sans-serif;
font-size: 0.86rem;
background: #fafafa;
}
h1 {
font-size: 1.875rem;
}
h2 {
font-size: 1.625rem;
}
h3 {
font-size: 1.375rem;
}
h4 {
font-size: 1.125rem;
}
h5 {
font-size: 0.925rem;
}
h6 {
font-size: 0.825rem;
}
.dropdown-menu,
.btn,
.form-control,
.form-select {
font-size: 0.85rem;
padding: .375rem .75rem;
}
.btn-lg {
font-size: 0.95rem;
padding: .5rem 1rem;
}
.btn-sm {
font-size: 0.75rem;
padding: .25rem .5rem;
}
.task-item:hover {
cursor: pointer;
}
::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: #eee;
opacity: 1;
/* Firefox */
}
:-ms-input-placeholder {
/* Internet Explorer 10-11 */
color: gray;
}
::-ms-input-placeholder {
/* Microsoft Edge */
color: gray;
}
/* sidebar */
.area-item .area-options {
visibility: hidden;
opacity: 0;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.area-item:hover .area-options {
visibility: visible;
opacity: 1;
}
.fixed-sidebar {
position: fixed;
top: 0;
left: 0;
height: 100vh;
overflow-y: auto;
}
/* main */
.main-content {
margin-top: 50px;
margin-left: 250px;
background: transparent;
}
.login-content {
margin-left: 200px;
}
/* sidebar */
.sidebar {
background: #ececec;
}
.nav-link {
color: var(--bs-dark);
}
.nav-link.active-link {
background-color: #343a40;
color: #fff;
}
.nav-link:hover, .nav-link a:hover {
color: #666 !important;
}
.nav-link.active-link:hover, .nav-link.active-link a:hover {
color: #ccc !important;
}
.dark-mode .nav-link {
color: #fff;
}
.dark-mode .nav-link.active-link {
background-color: var(--bs-dark);
}
/* tasks, notes */
.task-item,
.note-item{
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.dark-mode .task-item,
.dark-mode .note-item{
border-bottom: 1px solid #222;
padding-bottom: 8px;
}
.dark-mode .task-list,
.dark-mode .note-list,
.dark-mode .project-list {
background: var(--bs-black) !important;
}
.task-item:hover,
.note-item:hover {
background: #eee !important;
}
.dark-mode .task-item:hover,
.dark-mode .note-item:hover {
background: #292929 !important;
}
/* task form */
.task-name-input::placeholder {
color: #ccc;
opacity: 1;
}
.new-task-input {
background: #f2f2f2;
}
/* note form */
.no-focus-outline textarea:focus,
.no-focus-outline textarea:-moz-focusring {
outline: none !important;
}
/* project card */
a.project-card .card:hover {
border: 1px solid #777 !important;
}
/* dark mode */
.dark-mode {
background-color: #191919;
color: var(--bs-light);
}
.dark-mode .sidebar {
background-color: var(--bs-black);
}
.sidebar .border-top {
border-color: #ddd !important;
}
.dark-mode .sidebar .border-top {
border-color: #333 !important;
}
.dark-mode h1,
.dark-mode h2,
.dark-mode h3,
.dark-mode h4,
.dark-mode h5,
.dark-mode h6 {
color: var(--bs-light) !important;
}
.dark-mode .modal .modal-dialog .modal-content {
background-color: var(--bs-gray-dark);
color: var(--bs-light);
}
.dark-mode .modal-header {
border-color: var(--bs-black);
}
.dark-mode input,
.dark-mode tags,
.dark-mode select,
.dark-mode textarea {
background-color: var(--bs-dark);
color: var(--bs-light);
border-color: var(--bs-black);
}
.dark-mode input:focus,
.dark-mode textarea:focus {
background-color: var(--bs-black);
color: var(--bs-light);
}
.dark-mode tag {
background-color: var(--bs-gray-dark) !important;
color: var(--bs-light) !important;
me
}
.dark-mode .project-card .card {
background-color: var(--bs-gray-dark) !important;
color: var(--bs-light) !important;
}
.dark-mode .progress {
background-color: var(--bs-dark);
}
.panel-card {
background-color: var(--bs-white) !important;
color: var(--bs-dark) !important;
}
.dark-mode .panel-card {
background-color: var(--bs-gray-dark) !important;
color: var(--bs-light) !important;
}
.panel {
background-color: #eee;
}
.dark-mode .panel {
background-color: var(--bs-black);
}
#newNoteForm .card {
color: var(--bs-dark) !important;
}
.dark-mode #newNoteForm .card {
background-color: var(--bs-gray-dark) !important;
color: var(--bs-light) !important;
}
.blank-slate {
background-color: #eee !important;
color: var(--bs-dark) !important;
}
.dark-mode .blank-slate {
background-color: var(--bs-gray-dark) !important;
color: var(--bs-light) !important;
}
.note-form-toggler {
border: 1px solid #ddd !important;
background-color: #f4f4f4 !important;
color: #ccc !important;
}
.dark-mode .note-form-toggler {
border: 1px solid #000 !important;
background-color: var(--bs-dark) !important;
color: var(--bs-light) !important;
}

View file

@ -1,297 +0,0 @@
document.addEventListener("DOMContentLoaded", function () {
attachEventListeners();
initializeTagifyOnNotes();
initializeDarkModeToggle();
setInitialDarkModeState();
});
function attachEventListeners() {
attachCollapseListeners();
manageAreaState();
attachTaskClickListeners();
attachProjectModalListeners();
attachAreaModalListeners();
attachNoteClickListeners();
}
function initializeDarkModeToggle() {
const darkModeToggle = document.querySelector('#darkModeToggle');
const darkModeIcon = document.querySelector('#darkModeIcon');
const isDarkMode = localStorage.getItem('darkMode') === 'true';
updateDarkMode(isDarkMode);
darkModeToggle.addEventListener('click', function () {
const isDarkMode = localStorage.getItem('darkMode') === 'true';
updateDarkMode(!isDarkMode);
});
}
function updateDarkMode(enabled) {
document.body.classList.toggle('dark-mode', enabled);
localStorage.setItem('darkMode', enabled);
darkModeIcon.classList.toggle('bi-moon', !enabled);
darkModeIcon.classList.toggle('bi-sun', enabled);
// Update link colors and other necessary elements
updateLinkColors(enabled);
updateDropdownMenus(enabled);
}
function updateLinkColors(darkModeEnabled) {
const links = document.querySelectorAll('.link-dark, .link-light');
links.forEach(link => {
link.classList.toggle('link-dark', !darkModeEnabled);
link.classList.toggle('link-light', darkModeEnabled);
});
}
function setInitialDarkModeState() {
const isDarkMode = localStorage.getItem('darkMode') === 'true';
const darkModeToggle = document.querySelector('#darkModeToggle');
if (darkModeToggle) {
darkModeToggle.checked = isDarkMode;
document.body.classList.toggle('dark-mode', isDarkMode);
updateLinkColors(isDarkMode);
}
}
function updateDropdownMenus(enableDarkMode) {
document.querySelectorAll('.dropdown-menu').forEach(menu => {
menu.classList.toggle('dropdown-menu-dark', enableDarkMode);
});
}
function initializeTagifyOnNotes() {
document.querySelectorAll('[id^="note_tags_new_"]').forEach(function(element) {
new Tagify(element);
});
}
function attachCollapseListeners() {
document.querySelectorAll('.collapse').forEach(collapseElement => {
collapseElement.addEventListener('show.bs.collapse', () => toggleFolderIcon(collapseElement, true));
collapseElement.addEventListener('hide.bs.collapse', () => toggleFolderIcon(collapseElement, false));
});
}
function manageAreaState() {
document.querySelectorAll('.area-item a.nav-link').forEach(link => {
const areaId = link.getAttribute('href').replace('#', '');
const areaElement = document.getElementById(areaId);
if (areaElement) {
const isExpanded = localStorage.getItem('#' + areaId) === 'true';
if (isExpanded) {
link.setAttribute('aria-expanded', 'true');
areaElement.classList.add('show');
} else {
link.setAttribute('aria-expanded', 'false');
areaElement.classList.remove('show');
}
}
});
// Save the state of areas when clicked
document.querySelectorAll('.area-item a.nav-link').forEach(link => {
link.addEventListener('click', function (event) {
event.preventDefault(); // Prevent default action to manually control collapse
const areaId = this.getAttribute('href').replace('#', '');
const isExpanded = this.getAttribute('aria-expanded') === 'false'; // Toggle the state based on current state
localStorage.setItem('#' + areaId, isExpanded);
if (isExpanded) {
this.setAttribute('aria-expanded', 'true');
document.getElementById(areaId).classList.add('show');
} else {
this.setAttribute('aria-expanded', 'false');
document.getElementById(areaId).classList.remove('show');
}
});
});
}
// Ensure this function is called when the DOM is fully loaded
document.addEventListener("DOMContentLoaded", manageAreaState);
function toggleFolderIcon(collapseElement, isOpening) {
const closedFolderIcon = collapseElement.previousElementSibling?.querySelector('.bi-folder');
const openFolderIcon = collapseElement.previousElementSibling?.querySelector('.bi-folder2-open');
if (closedFolderIcon && openFolderIcon) {
closedFolderIcon.classList.toggle('d-none', isOpening);
openFolderIcon.classList.toggle('d-none', !isOpening);
}
}
function attachTaskClickListeners() {
document.querySelectorAll('.task-item').forEach(taskElement => {
taskElement.addEventListener('click', event => {
if (!event.target.closest('.toggle-completion')) {
openEditTaskModal(taskElement.dataset.taskId);
}
});
});
}
function openEditTaskModal(taskId) {
const formContainer = document.getElementById('edit_task_form_' + taskId);
if (!formContainer) {
console.error('Edit form not found for task: ' + taskId);
return;
}
const formHtml = formContainer.innerHTML;
const editTaskFormContainer = document.getElementById('editTaskFormContainer');
editTaskFormContainer.innerHTML = formHtml;
new Tagify(editTaskFormContainer.querySelector('#task_tags_' + taskId));
new bootstrap.Modal(document.getElementById('editTaskModal')).show();
}
function attachProjectModalListeners() {
document.querySelectorAll('[data-bs-toggle="modal"][data-project-id]').forEach(button => {
button.addEventListener('click', () => openProjectModalForEdit(button.getAttribute('data-project-id')));
});
}
function attachNoteClickListeners() {
document.querySelectorAll('.note-item').forEach(noteElement => {
noteElement.addEventListener('click', event => {
const noteId = noteElement.getAttribute('data-note-id');
if (noteId) {
openEditNoteModal(noteId);
} else {
console.error('Note ID not found for element:', noteElement);
}
});
});
}
function openEditNoteModal(noteId) {
const formContainer = document.getElementById('edit_note_form_' + noteId);
if (!formContainer) {
console.error('Edit form not found for note: ' + noteId);
return;
}
const formHtml = formContainer.innerHTML;
const editNoteFormContainer = document.getElementById('editNoteFormContainer');
editNoteFormContainer.innerHTML = formHtml;
new Tagify(editNoteFormContainer.querySelector('#note_tags_' + noteId));
new bootstrap.Modal(document.getElementById('editNoteModal')).show();
}
function openProjectModalForEdit(projectId) {
fetch('/project/' + projectId)
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok.');
}
return response.json();
})
.then(projectData => {
document.getElementById('projectName').value = projectData.name;
document.getElementById('projectDescription').value = projectData.description || '';
var projectForm = document.getElementById('projectForm');
if (projectForm) {
projectForm.action = '/projects/' + projectId;
projectForm.method = 'patch';
}
var modal = new bootstrap.Modal(document.getElementById('editProjectModal'));
modal.show();
})
.catch(error => {
console.error('Error fetching project data:', error);
});
}
function attachAreaModalListeners() {
document.querySelectorAll('.open-new-area-modal').forEach(button => {
button.addEventListener('click', () => new bootstrap.Modal(document.getElementById('newAreaModal')).show());
});
document.querySelectorAll('.open-edit-area-modal').forEach(button => {
button.addEventListener('click', () => openEditAreaModal(button.dataset.areaId));
});
}
function deleteProject(projectId) {
if (confirm('Are you sure you want to delete this project?')) {
const form = document.getElementById('delete_project_' + projectId);
form.submit();
}
}
function deleteArea(areaId) {
if (confirm('Are you sure you want to delete this area?')) {
const form = document.getElementById('delete_area_' + areaId);
form.submit();
}
}
function openEditAreaModal(areaId) {
fetchAreaDataAndPopulateModal(areaId);
const modal = new bootstrap.Modal(document.getElementById('editAreaModal'));
modal.show();
}
function fetchAreaDataAndPopulateModal(areaId) {
fetch('/areas/' + areaId + '/data')
.then(response => response.json())
.then(areaData => {
populateAreaEditForm(areaData);
})
.catch(error => console.error('Error fetching area data:', error));
}
function populateAreaEditForm(areaData) {
document.getElementById('editAreaName').value = areaData.name;
}
function toggleTaskCompletion(event, taskId) {
event.stopPropagation();
fetch('/task/' + taskId + '/toggle_completion', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ _method: 'patch' })
})
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => updateTaskCompletionStatus(taskId, data))
.catch(error => console.error('There has been a problem with your fetch operation:', error));
}
function updateTaskCompletionStatus(taskId, data) {
const iconSpan = document.querySelector('.task-item[data-task-id="' + taskId + '"] .toggle-completion');
const taskIcon = iconSpan.querySelector('.bi');
const taskDiv = iconSpan.closest('.task-item');
if (data.done) {
taskIcon.classList.remove('bi-circle', 'text-warning', 'text-danger');
taskIcon.classList.add('bi-check-circle-fill', 'text-success');
taskDiv.classList.add('opacity-50');
} else {
taskIcon.classList.remove('bi-check-circle-fill', 'text-success');
taskIcon.classList.add('bi-circle');
taskDiv.classList.remove('opacity-50');
applyPriorityColor(taskIcon, data.priority);
}
setTimeout(() => taskDiv.remove(), 200);
}
function applyPriorityColor(taskIcon, priority) {
taskIcon.classList.remove('text-warning', 'text-danger', 'text-secondary');
switch (priority) {
case 'Medium':
taskIcon.classList.add('text-warning');
break;
case 'High':
taskIcon.classList.add('text-danger');
break;
default:
taskIcon.classList.add('text-secondary');
}
}

View file

@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es5",
"module": "esnext",
"moduleResolution": "node", // Ensures TypeScript resolves modules like 'react-router-dom'
"moduleResolution": "node",
"jsx": "react",
"strict": true,
"esModuleInterop": true,

View file

@ -9,7 +9,7 @@ module.exports = {
output: {
path: path.resolve(__dirname, 'public/js'),
filename: 'bundle.js',
publicPath: '/js/', // Ensure HMR works properly
publicPath: '/js/',
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],