Fix static base path (#549)

This commit is contained in:
Chris 2025-11-16 22:43:06 +02:00 committed by GitHub
parent b0041bafe1
commit 673a6a56ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 496 additions and 315 deletions

View file

@ -25,6 +25,8 @@ const credentials = {
},
};
const defaultHost = environment === 'test' ? '127.0.0.1' : '0.0.0.0';
const config = {
allowedOrigins: process.env.TUDUDI_ALLOWED_ORIGINS
? process.env.TUDUDI_ALLOWED_ORIGINS.split(',').map((origin) =>
@ -51,7 +53,9 @@ const config = {
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:8080',
host: process.env.HOST || '0.0.0.0',
// Some CI/sandbox environments disallow binding to 0.0.0.0, so force
// loopback for tests unless HOST is explicitly provided.
host: process.env.HOST || defaultHost,
port: process.env.PORT || 3002,

View file

@ -23,6 +23,7 @@ import TaskDetails from './components/Task/TaskDetails';
import LoadingScreen from './components/Shared/LoadingScreen';
import InboxItems from './components/Inbox/InboxItems';
import { setCurrentUser as setUserInStorage } from './utils/userUtils';
import { getApiPath, getLocalesPath } from './config/paths';
// Lazy load Tasks component to prevent issues with tags loading
const Tasks = lazy(() => import('./components/Tasks'));
@ -37,7 +38,7 @@ const App: React.FC = () => {
const fetchCurrentUser = async () => {
try {
const response = await fetch('/api/current_user', {
const response = await fetch(getApiPath('current_user'), {
credentials: 'include',
headers: {
Accept: 'application/json',
@ -93,7 +94,7 @@ const App: React.FC = () => {
useEffect(() => {
if (i18n.isInitialized) {
fetch(`/locales/${i18n.language}/translation.json`)
fetch(getLocalesPath(`${i18n.language}/translation.json`))
.then((response) => {
return response.json();
})

View file

@ -28,6 +28,7 @@ import {
} from './utils/projectsService';
import { createTask, updateTask } from './utils/tasksService';
import { isAuthError } from './utils/authUtils';
import { Link } from 'react-router-dom';
interface LayoutProps {
currentUser: User;
@ -204,12 +205,12 @@ const Layout: React.FC<LayoutProps> = ({
const taskLink = (
<span>
{t('task.updated', 'Task')}{' '}
<a
href="/tasks"
<Link
to="/tasks"
className="text-green-200 underline hover:text-green-100"
>
{taskData.name}
</a>{' '}
</Link>{' '}
{t('task.updatedSuccessfully', 'updated successfully!')}
</span>
);

View file

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { HeartIcon } from '@heroicons/react/24/outline';
import { getApiPath } from '../config/paths';
interface AboutProps {
isDarkMode?: boolean;
@ -12,7 +13,7 @@ const About: React.FC<AboutProps> = ({ isDarkMode = false }) => {
useEffect(() => {
// Fetch version from the deployed app
fetch('/api/version')
fetch(getApiPath('version'))
.then((response) => response.json())
.then((data) => {
if (data.version) {

View file

@ -8,6 +8,7 @@ import {
TrashIcon,
} from '@heroicons/react/24/outline';
import ConfirmDialog from '../Shared/ConfirmDialog';
import { getApiPath } from '../../config/paths';
interface AdminUserItem {
id: number;
@ -19,7 +20,7 @@ interface AdminUserItem {
}
const fetchAdminUsers = async (t: any): Promise<AdminUserItem[]> => {
const res = await fetch('/api/admin/users', {
const res = await fetch(getApiPath('admin/users'), {
credentials: 'include',
headers: { Accept: 'application/json' },
});
@ -41,7 +42,7 @@ const createAdminUser = async (
surname?: string,
role?: 'admin' | 'user'
): Promise<AdminUserItem> => {
const res = await fetch('/api/admin/users', {
const res = await fetch(getApiPath('admin/users'), {
method: 'POST',
credentials: 'include',
headers: {
@ -82,7 +83,7 @@ const updateAdminUser = async (
const body: any = { email, name, surname, role };
if (password) body.password = password;
const res = await fetch(`/api/admin/users/${id}`, {
const res = await fetch(getApiPath(`admin/users/${id}`), {
method: 'PUT',
credentials: 'include',
headers: {
@ -114,7 +115,7 @@ const updateAdminUser = async (
};
const deleteAdminUser = async (id: number, t: any): Promise<void> => {
const res = await fetch(`/api/admin/users/${id}`, {
const res = await fetch(getApiPath(`admin/users/${id}`), {
method: 'DELETE',
credentials: 'include',
headers: { Accept: 'application/json' },

View file

@ -16,6 +16,8 @@ import { el, enUS, es, ja, uk, de } from 'date-fns/locale';
import CalendarMonthView from './Calendar/CalendarMonthView';
import CalendarWeekView from './Calendar/CalendarWeekView';
import CalendarDayView from './Calendar/CalendarDayView';
import { getApiPath } from '../config/paths';
import { Link } from 'react-router-dom';
const getLocale = (language: string) => {
switch (language) {
@ -94,7 +96,7 @@ const Calendar: React.FC = () => {
const checkGoogleCalendarStatus = async () => {
try {
const response = await fetch('/api/calendar/status', {
const response = await fetch(getApiPath('calendar/status'), {
credentials: 'include',
});
if (response.ok) {
@ -110,7 +112,7 @@ const Calendar: React.FC = () => {
const loadTasks = async () => {
setIsLoadingTasks(true);
try {
const response = await fetch('/api/tasks', {
const response = await fetch(getApiPath('tasks'), {
credentials: 'include',
});
if (response.ok) {
@ -205,7 +207,7 @@ const Calendar: React.FC = () => {
const loadProjects = async () => {
try {
const response = await fetch('/api/projects', {
const response = await fetch(getApiPath('projects'), {
credentials: 'include',
});
if (response.ok) {
@ -222,7 +224,7 @@ const Calendar: React.FC = () => {
setIsConnecting(true);
try {
const response = await fetch('/api/calendar/auth', {
const response = await fetch(getApiPath('calendar/auth'), {
credentials: 'include',
});
if (response.ok) {
@ -259,7 +261,7 @@ const Calendar: React.FC = () => {
}
// Real disconnect API call
const response = await fetch('/api/calendar/disconnect', {
const response = await fetch(getApiPath('calendar/disconnect'), {
method: 'POST',
credentials: 'include',
});
@ -372,7 +374,7 @@ const Calendar: React.FC = () => {
const handleCreateProject = async (name: string): Promise<Project> => {
try {
const response = await fetch('/api/projects', {
const response = await fetch(getApiPath('projects'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
@ -712,13 +714,13 @@ const TaskEventModal: React.FC<TaskEventModalProps> = ({
{/* Action Buttons */}
<div className="mt-6 flex justify-between">
<a
href="/tasks"
<Link
to="/tasks"
className="inline-flex items-center px-3 py-2 text-sm font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300"
>
<ArrowTopRightOnSquareIcon className="w-4 h-4 mr-1" />
{t('calendar.goToTasks')}
</a>
</Link>
<div className="flex space-x-3">
<button

View file

@ -13,6 +13,7 @@ import { XMarkIcon, TagIcon, FolderIcon } from '@heroicons/react/24/outline';
import { useStore } from '../../store/useStore';
import { Link } from 'react-router-dom';
import { isUrl } from '../../utils/urlService';
import { getApiPath } from '../../config/paths';
// import UrlPreview from "../Shared/UrlPreview";
// import { UrlTitleResult } from "../../utils/urlService";
@ -697,7 +698,7 @@ const InboxModal: React.FC<InboxModalProps> = ({
try {
setIsAnalyzing(true);
const response = await fetch('/api/inbox/analyze-text', {
const response = await fetch(getApiPath('inbox/analyze-text'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import i18n from 'i18next';
import { useTranslation } from 'react-i18next';
import { getApiPath, getAssetPath } from '../config/paths';
const Login: React.FC = () => {
const [email, setEmail] = useState('');
@ -24,7 +25,7 @@ const Login: React.FC = () => {
e.preventDefault();
try {
const response = await fetch('/api/login', {
const response = await fetch(getApiPath('login'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -137,7 +138,7 @@ const Login: React.FC = () => {
{/* Right side - Graphic */}
<div className="hidden lg:flex items-center justify-center">
<img
src="/login-gfx.png"
src={getAssetPath('login-gfx.png')}
alt="Login illustration"
className="max-w-md w-full h-auto"
/>

View file

@ -10,6 +10,7 @@ import { EnvelopeIcon, MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import { useTranslation } from 'react-i18next';
import PomodoroTimer from './Shared/PomodoroTimer';
import UniversalSearch from './UniversalSearch/UniversalSearch';
import { getApiPath } from '../config/paths';
interface NavbarProps {
isDarkMode: boolean;
@ -84,7 +85,7 @@ const Navbar: React.FC<NavbarProps> = ({
useEffect(() => {
const fetchProfile = async () => {
try {
const response = await fetch('/api/profile', {
const response = await fetch(getApiPath('profile'), {
credentials: 'include',
});
if (response.ok) {
@ -127,7 +128,7 @@ const Navbar: React.FC<NavbarProps> = ({
const handleLogout = async () => {
try {
const response = await fetch('/api/logout', {
const response = await fetch(getApiPath('logout'), {
method: 'GET',
credentials: 'include',
});

View file

@ -6,6 +6,7 @@ import React, {
useCallback,
} from 'react';
import { useTranslation } from 'react-i18next';
import { getLocalesPath, getApiPath } from '../../config/paths';
import {
InformationCircleIcon,
EyeIcon,
@ -255,7 +256,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
const resources = i18n.getResourceBundle(value, 'translation');
if (!resources || Object.keys(resources).length === 0) {
const loadPath = `/locales/${value}/translation.json`;
const loadPath = getLocalesPath(`${value}/translation.json`);
try {
const response = await fetch(loadPath);
if (response.ok) {
@ -438,7 +439,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
const fetchProfile = async () => {
try {
setLoading(true);
const response = await fetch('/api/profile');
const response = await fetch(getApiPath('profile'));
if (!response.ok) {
throw new Error(
@ -504,7 +505,9 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
const fetchPollingStatus = async () => {
try {
const response = await fetch('/api/telegram/polling-status');
const response = await fetch(
getApiPath('telegram/polling-status')
);
if (!response.ok) {
throw new Error(
@ -535,7 +538,9 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
if (profile?.telegram_bot_token) {
try {
// Fetch bot info
const setupResponse = await fetch('/api/telegram/setup', {
const setupResponse = await fetch(
getApiPath('telegram/setup'),
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -543,7 +548,8 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
body: JSON.stringify({
token: profile.telegram_bot_token,
}),
});
}
);
if (setupResponse.ok) {
const setupData = await setupResponse.json();
@ -559,7 +565,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
// Also fetch and auto-start polling status
const pollingResponse = await fetch(
'/api/telegram/polling-status',
getApiPath('telegram/polling-status'),
{
credentials: 'include',
headers: {
@ -639,7 +645,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
throw new Error(t('profile.invalidTelegramToken'));
}
const response = await fetch('/api/telegram/setup', {
const response = await fetch(getApiPath('telegram/setup'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -689,7 +695,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
// Send welcome message on first setup
if (profile?.telegram_chat_id) {
try {
await fetch('/api/telegram/send-welcome', {
await fetch(getApiPath('telegram/send-welcome'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -720,7 +726,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
const handleStartPolling = async () => {
try {
const response = await fetch('/api/telegram/start-polling', {
const response = await fetch(getApiPath('telegram/start-polling'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -753,7 +759,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
const handleStopPolling = async () => {
try {
const response = await fetch('/api/telegram/stop-polling', {
const response = await fetch(getApiPath('telegram/stop-polling'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -813,7 +819,7 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
delete dataToSend.confirmPassword;
}
const response = await fetch('/api/profile', {
const response = await fetch(getApiPath('profile'), {
method: 'PATCH',
credentials: 'include',
headers: {
@ -2140,7 +2146,9 @@ const ProfileSettings: React.FC<ProfileSettingsProps> = ({
onClick={async () => {
try {
const response = await fetch(
'/api/profile/task-summary/send-now',
getApiPath(
'profile/task-summary/send-now'
),
{
method: 'POST',
headers: {

View file

@ -48,6 +48,7 @@ import SortFilterButton, { SortOption } from '../Shared/SortFilterButton';
import LoadingSpinner from '../Shared/LoadingSpinner';
import { usePersistedModal } from '../../hooks/usePersistedModal';
import BannerBadge from '../Shared/BannerBadge';
import { getApiPath } from '../../config/paths';
const ProjectDetails: React.FC = () => {
const { uidSlug } = useParams<{ uidSlug: string }>();
@ -287,7 +288,7 @@ const ProjectDetails: React.FC = () => {
try {
// Use direct fetch call like Tasks.tsx to ensure proper tag saving
const response = await fetch(`/api/task/${updatedTask.id}`, {
const response = await fetch(getApiPath(`task/${updatedTask.id}`), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
@ -488,7 +489,7 @@ const ProjectDetails: React.FC = () => {
try {
// Save preferences directly via API call
const response = await fetch(`/api/project/${project.id}`, {
const response = await fetch(getApiPath(`project/${project.id}`), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
@ -544,7 +545,7 @@ const ProjectDetails: React.FC = () => {
const handleEditNote = async (note: Note) => {
try {
// Fetch the complete note data including tags
const response = await fetch(`/api/note/${note.uid}`, {
const response = await fetch(getApiPath(`note/${note.uid}`), {
credentials: 'include',
headers: { Accept: 'application/json' },
});

View file

@ -22,6 +22,7 @@ import {
ExclamationTriangleIcon,
PlayIcon,
} from '@heroicons/react/24/outline';
import { getApiPath } from '../../config/paths';
interface ProjectModalProps {
isOpen: boolean;
@ -295,7 +296,7 @@ const ProjectModal: React.FC<ProjectModalProps> = ({
const formData = new FormData();
formData.append('image', imageFile);
const response = await fetch('/api/upload/project-image', {
const response = await fetch(getApiPath('upload/project-image'), {
method: 'POST',
credentials: 'include',
body: formData,

View file

@ -9,6 +9,7 @@ import {
} from '../../utils/sharesService';
import { ChevronDownIcon, CheckIcon } from '@heroicons/react/24/outline';
import { getCurrentUser } from '../../utils/userUtils';
import { getApiPath } from '../../config/paths';
interface ProjectShareModalProps {
isOpen: boolean;
@ -67,7 +68,7 @@ const ProjectShareModal: React.FC<ProjectShareModalProps> = ({
const loadUsers = async () => {
setLoadingUsers(true);
try {
const res = await fetch('/api/users', {
const res = await fetch(getApiPath('users'), {
credentials: 'include',
headers: { Accept: 'application/json' },
});

View file

@ -1,4 +1,6 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { getAssetPath } from '../../config/paths';
interface SidebarHeaderProps {
isDarkMode: boolean;
@ -7,20 +9,20 @@ interface SidebarHeaderProps {
const SidebarHeader: React.FC<SidebarHeaderProps> = ({ isDarkMode }) => {
return (
<div className="flex justify-center mb-6 mt-2">
<a
href="/"
<Link
to="/"
className="flex justify-center items-center mb-2 no-underline"
>
<img
src={
src={getAssetPath(
isDarkMode
? '/wide-logo-light.png'
: '/wide-logo-dark.png'
}
? 'wide-logo-light.png'
: 'wide-logo-dark.png'
)}
alt="tududi"
className="h-12 w-auto"
/>
</a>
</Link>
</div>
);
};

View file

@ -20,6 +20,7 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { getApiPath } from '../../config/paths';
interface View {
id: number;
@ -138,7 +139,7 @@ const SidebarViews: React.FC<SidebarViewsProps> = ({
const fetchUserSettings = async () => {
try {
const response = await fetch('/api/profile', {
const response = await fetch(getApiPath('profile'), {
credentials: 'include',
});
if (response.ok) {
@ -159,7 +160,7 @@ const SidebarViews: React.FC<SidebarViewsProps> = ({
const fetchPinnedViews = async () => {
try {
const response = await fetch('/api/views/pinned', {
const response = await fetch(getApiPath('views/pinned'), {
credentials: 'include',
});
if (response.ok) {
@ -174,7 +175,7 @@ const SidebarViews: React.FC<SidebarViewsProps> = ({
const togglePin = async (view: View, e: React.MouseEvent) => {
e.stopPropagation();
try {
const response = await fetch(`/api/views/${view.uid}`, {
const response = await fetch(getApiPath(`views/${view.uid}`), {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
@ -208,7 +209,7 @@ const SidebarViews: React.FC<SidebarViewsProps> = ({
// Save to backend
try {
await fetch('/api/profile/sidebar-settings', {
await fetch(getApiPath('profile/sidebar-settings'), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',

View file

@ -22,6 +22,7 @@ import ConfirmDialog from '../Shared/ConfirmDialog';
import { Tag } from '../../entities/Tag';
import { useStore } from '../../store/useStore';
import { updateTag, deleteTag } from '../../utils/tagsService';
import { getApiPath } from '../../config/paths';
const TagDetails: React.FC = () => {
const { t } = useTranslation();
@ -77,8 +78,16 @@ const TagDetails: React.FC = () => {
// Now fetch entities that have this tag using the tag name
const [tasksResponse, notesResponse] = await Promise.all([
fetch(`/api/tasks?tag=${encodeURIComponent(tagData.name)}`),
fetch(`/api/notes?tag=${encodeURIComponent(tagData.name)}`),
fetch(
getApiPath(
`tasks?tag=${encodeURIComponent(tagData.name)}`
)
),
fetch(
getApiPath(
`notes?tag=${encodeURIComponent(tagData.name)}`
)
),
]);
if (tasksResponse.ok) {
@ -121,7 +130,7 @@ const TagDetails: React.FC = () => {
// Task handlers
const handleTaskUpdate = async (updatedTask: Task) => {
try {
const response = await fetch(`/api/task/${updatedTask.id}`, {
const response = await fetch(getApiPath(`task/${updatedTask.id}`), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedTask),
@ -141,7 +150,7 @@ const TagDetails: React.FC = () => {
const handleTaskDelete = async (taskId: number) => {
try {
const response = await fetch(`/api/task/${taskId}`, {
const response = await fetch(getApiPath(`task/${taskId}`), {
method: 'DELETE',
});

View file

@ -131,6 +131,7 @@ import { isTaskOverdue } from '../../utils/dateUtils';
import { useTranslation } from 'react-i18next';
import ConfirmDialog from '../Shared/ConfirmDialog';
import { useStore } from '../../store/useStore';
import { getApiPath } from '../../config/paths';
interface TaskItemProps {
task: Task;
@ -289,7 +290,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
const handleSave = async (updatedTask: Task) => {
try {
await onTaskUpdate(updatedTask);
modalStore.closeTaskModal();
// Let TaskModal invoke onClose so unsaved-change checks remain consistent
} catch (error: any) {
console.error('Task update failed:', error);
showErrorToast(t('errors.permissionDenied', 'Permission denied'));
@ -316,7 +317,6 @@ const TaskItem: React.FC<TaskItemProps> = ({
const handleDelete = async () => {
if (task.id) {
try {
modalStore.closeTaskModal();
await onTaskDelete(task.id);
} catch (error: any) {
console.error('Task delete failed:', error);
@ -378,7 +378,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
try {
// Refetch the current task with updated subtasks
const updatedTaskResponse = await fetch(
`/api/task/${task.id}`
getApiPath(`task/${task.id}`)
);
if (updatedTaskResponse.ok) {
const updatedTaskData =
@ -404,7 +404,7 @@ const TaskItem: React.FC<TaskItemProps> = ({
const handleCreateProject = async (name: string): Promise<Project> => {
try {
const response = await fetch('/api/project', {
const response = await fetch(getApiPath('project'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -3,6 +3,7 @@ import { format } from 'date-fns';
import { el, enUS, es, ja, uk, de } from 'date-fns/locale';
import { useTranslation } from 'react-i18next';
import i18n from 'i18next';
import { getLocalesPath, getApiPath } from '../../config/paths';
import {
ClipboardDocumentListIcon,
ArrowPathIcon,
@ -248,7 +249,7 @@ const TasksToday: React.FC = () => {
// Load all profile settings in a single API call instead of multiple calls
try {
const response = await fetch('/api/profile', {
const response = await fetch(getApiPath('profile'), {
credentials: 'include',
});
if (response.ok) {
@ -370,7 +371,7 @@ const TasksToday: React.FC = () => {
// Load daily quote from translations
try {
const response = await fetch(
`/locales/${i18n.language}/quotes.json`
getLocalesPath(`${i18n.language}/quotes.json`)
);
if (response.ok) {
const data = await response.json();
@ -388,7 +389,7 @@ const TasksToday: React.FC = () => {
} else {
// Fallback to English if language file doesn't exist
const fallbackResponse = await fetch(
'/locales/en/quotes.json'
getLocalesPath('en/quotes.json')
);
if (fallbackResponse.ok) {
const fallbackData = await fallbackResponse.json();

View file

@ -9,6 +9,7 @@ import {
ChatBubbleBottomCenterTextIcon,
ListBulletIcon,
} from '@heroicons/react/24/outline';
import { getApiPath } from '../../config/paths';
interface TodaySettingsDropdownProps {
isOpen: boolean;
@ -80,7 +81,7 @@ const TodaySettingsDropdown: React.FC<TodaySettingsDropdownProps> = ({
const saveSettings = async (settingsToSave: typeof localSettings) => {
setIsSaving(true);
try {
const response = await fetch('/api/profile/today-settings', {
const response = await fetch(getApiPath('profile/today-settings'), {
method: 'PUT',
credentials: 'include',
headers: {

View file

@ -23,6 +23,7 @@ import {
MagnifyingGlassIcon,
} from '@heroicons/react/24/solid';
import { InformationCircleIcon } from '@heroicons/react/24/outline';
import { getApiPath } from '../config/paths';
const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
@ -192,7 +193,9 @@ const Tasks: React.FC = () => {
const searchParams = allTasksUrl.toString();
const tasksResponse = await fetch(
`/api/tasks?${searchParams}${tagId ? `&tag=${tagId}` : ''}`
getApiPath(
`tasks?${searchParams}${tagId ? `&tag=${tagId}` : ''}`
)
);
if (tasksResponse.ok) {
@ -286,7 +289,7 @@ const Tasks: React.FC = () => {
const handleTaskUpdate = async (updatedTask: Task) => {
try {
const response = await fetch(`/api/task/${updatedTask.id}`, {
const response = await fetch(getApiPath(`task/${updatedTask.id}`), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedTask),
@ -356,7 +359,7 @@ const Tasks: React.FC = () => {
const handleTaskDelete = async (taskId: number) => {
try {
const response = await fetch(`/api/task/${taskId}`, {
const response = await fetch(getApiPath(`task/${taskId}`), {
method: 'DELETE',
});
@ -388,12 +391,12 @@ const Tasks: React.FC = () => {
const project = params.get('project');
const priority = params.get('priority');
let apiPath = `/api/tasks?type=${type}&order_by=${orderBy}`;
let apiPath = `tasks?type=${type}&order_by=${orderBy}`;
if (tag) apiPath += `&tag=${tag}`;
if (project) apiPath += `&project=${project}`;
if (priority) apiPath += `&priority=${priority}`;
const response = await fetch(apiPath, {
const response = await fetch(getApiPath(apiPath), {
credentials: 'include',
});

View file

@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { getApiPath } from '../../config/paths';
interface SaveViewModalProps {
searchQuery: string;
@ -34,7 +35,7 @@ const SaveViewModal: React.FC<SaveViewModalProps> = ({
setError('');
try {
const response = await fetch('/api/views', {
const response = await fetch(getApiPath('views'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -10,6 +10,7 @@ import {
import FilterBadge from './FilterBadge';
import SearchResults from './SearchResults';
import { useToast } from '../Shared/ToastContext';
import { getApiPath } from '../../config/paths';
interface SearchMenuProps {
searchQuery: string;
@ -80,7 +81,7 @@ const SearchMenu: React.FC<SearchMenuProps> = ({
useEffect(() => {
const fetchTags = async () => {
try {
const response = await fetch('/api/tags', {
const response = await fetch(getApiPath('tags'), {
credentials: 'include',
});
if (response.ok) {
@ -120,7 +121,7 @@ const SearchMenu: React.FC<SearchMenuProps> = ({
setSaveError('');
try {
const response = await fetch('/api/views', {
const response = await fetch(getApiPath('views'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',

View file

@ -17,6 +17,7 @@ import TaskList from './Task/TaskList';
import ProjectItem from './Project/ProjectItem';
import ConfirmDialog from './Shared/ConfirmDialog';
import { searchUniversal } from '../utils/searchService';
import { getApiPath } from '../config/paths';
interface View {
id: number;
@ -112,7 +113,7 @@ const ViewDetail: React.FC = () => {
try {
// Fetch view details
const viewResponse = await fetch(`/api/views/${uid}`, {
const viewResponse = await fetch(getApiPath(`views/${uid}`), {
credentials: 'include',
});
if (!viewResponse.ok) {
@ -163,7 +164,7 @@ const ViewDetail: React.FC = () => {
// Task handlers
const handleTaskUpdate = async (updatedTask: Task) => {
try {
const response = await fetch(`/api/task/${updatedTask.id}`, {
const response = await fetch(getApiPath(`task/${updatedTask.id}`), {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updatedTask),
@ -183,7 +184,7 @@ const ViewDetail: React.FC = () => {
const handleTaskDelete = async (taskId: number) => {
try {
const response = await fetch(`/api/task/${taskId}`, {
const response = await fetch(getApiPath(`task/${taskId}`), {
method: 'DELETE',
});
@ -245,7 +246,7 @@ const ViewDetail: React.FC = () => {
if (!view || !editedName.trim()) return;
try {
const response = await fetch(`/api/views/${view.uid}`, {
const response = await fetch(getApiPath(`views/${view.uid}`), {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
@ -275,7 +276,7 @@ const ViewDetail: React.FC = () => {
if (!view) return;
try {
const response = await fetch(`/api/views/${view.uid}`, {
const response = await fetch(getApiPath(`views/${view.uid}`), {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
@ -299,7 +300,7 @@ const ViewDetail: React.FC = () => {
if (!view) return;
try {
const response = await fetch(`/api/views/${view.uid}`, {
const response = await fetch(getApiPath(`views/${view.uid}`), {
method: 'DELETE',
credentials: 'include',
});

View file

@ -8,6 +8,7 @@ import {
} from '@heroicons/react/24/outline';
import { StarIcon as StarIconSolid } from '@heroicons/react/24/solid';
import ConfirmDialog from './Shared/ConfirmDialog';
import { getApiPath } from '../config/paths';
interface View {
id: number;
@ -36,7 +37,7 @@ const Views: React.FC = () => {
const fetchViews = async () => {
try {
const response = await fetch('/api/views', {
const response = await fetch(getApiPath('views'), {
credentials: 'include',
});
if (response.ok) {
@ -53,10 +54,13 @@ const Views: React.FC = () => {
const handleDeleteView = async () => {
if (!viewToDelete) return;
try {
const response = await fetch(`/api/views/${viewToDelete.uid}`, {
const response = await fetch(
getApiPath(`views/${viewToDelete.uid}`),
{
method: 'DELETE',
credentials: 'include',
});
}
);
if (response.ok) {
setViews(views.filter((v) => v.uid !== viewToDelete.uid));
// Notify sidebar to refresh
@ -72,7 +76,7 @@ const Views: React.FC = () => {
const togglePin = async (view: View) => {
try {
const response = await fetch(`/api/views/${view.uid}`, {
const response = await fetch(getApiPath(`views/${view.uid}`), {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',

115
frontend/config/paths.ts Normal file
View file

@ -0,0 +1,115 @@
/**
* Path configuration for the application
* This allows the app to work when hosted at the root or in a subdirectory
* (e.g., Home Assistant Ingress)
*/
declare global {
interface Window {
__TUDUDI_BASE_PATH__?: string;
}
}
const envBasePath = process.env.TUDUDI_BASE_PATH || '';
const sanitizeBasePath = (value: string): string => {
if (!value) {
return '';
}
const trimmed = value.trim();
if (!trimmed || trimmed === '/') {
return '';
}
const withoutTrailing = trimmed.endsWith('/')
? trimmed.slice(0, -1)
: trimmed;
return withoutTrailing.startsWith('/')
? withoutTrailing
: `/${withoutTrailing}`;
};
const detectHassioIngressBase = (): string => {
if (typeof window === 'undefined') {
return '';
}
const match = window.location.pathname.match(
/^\/api\/hassio_ingress\/[^/]+/
);
return match ? match[0] : '';
};
const runtimeBasePath = (() => {
if (typeof window === 'undefined') {
return sanitizeBasePath(envBasePath);
}
if (typeof window.__TUDUDI_BASE_PATH__ === 'string') {
return sanitizeBasePath(window.__TUDUDI_BASE_PATH__);
}
const hassBase = sanitizeBasePath(detectHassioIngressBase());
if (hassBase) {
return hassBase;
}
return sanitizeBasePath(envBasePath);
})();
const stripLeadingSlash = (value: string): string =>
value.startsWith('/') ? value.slice(1) : value;
const withBasePath = (path: string): string => {
const clean = stripLeadingSlash(path);
if (!runtimeBasePath) {
return `/${clean}`;
}
return `${runtimeBasePath}/${clean}`;
};
export function getBasePath(): string {
return runtimeBasePath;
}
/**
* Get the API endpoint path
* @param path - The API path (e.g., '/tasks', 'tasks', or '/api/tasks')
* @returns The full API path
*/
export function getApiPath(path: string): string {
const cleanPath = stripLeadingSlash(path);
if (cleanPath.startsWith('api/')) {
return withBasePath(cleanPath);
}
return withBasePath(`api/${cleanPath}`);
}
/**
* Get the locales path
* @param path - The locales path (e.g., '/locales/en/translation.json')
* @returns The full locales path
*/
export function getLocalesPath(path: string): string {
const cleanPath = stripLeadingSlash(path);
if (cleanPath.startsWith('locales/')) {
return withBasePath(cleanPath);
}
return withBasePath(`locales/${cleanPath}`);
}
/**
* Get an asset path
* @param path - The asset path
* @returns The full asset path
*/
export function getAssetPath(path: string): string {
const cleanPath = stripLeadingSlash(path);
return withBasePath(cleanPath);
}

View file

@ -5,6 +5,7 @@ import React, {
useCallback,
useEffect,
} from 'react';
import { getApiPath } from '../config/paths';
type TelegramStatus = 'healthy' | 'problem' | 'none';
@ -27,7 +28,7 @@ export const TelegramStatusProvider: React.FC<{
useCallback(async (): Promise<TelegramStatus> => {
try {
// Check if user has telegram bot token
const profileResponse = await fetch('/api/profile', {
const profileResponse = await fetch(getApiPath('profile'), {
credentials: 'include',
});
@ -43,7 +44,7 @@ export const TelegramStatusProvider: React.FC<{
// Check polling status
const pollingResponse = await fetch(
'/api/telegram/polling-status',
getApiPath('telegram/polling-status'),
{
credentials: 'include',
}

View file

@ -2,6 +2,7 @@ import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { getLocalesPath } from './config/paths';
const isDevelopment = process.env.NODE_ENV === 'development';
@ -84,7 +85,7 @@ i18nInstance
defaultNS: 'translation',
ns: ['translation'],
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
loadPath: 'locales/{{lng}}/{{ns}}.json',
queryStringParams: { v: '1' },
requestOptions: {
cache: 'default',
@ -94,18 +95,11 @@ i18nInstance
},
})
.then(() => {
const loadPath = isDevelopment
? `./locales/${i18n.language}/translation.json`
: `/locales/${i18n.language}/translation.json`;
const loadPath = getLocalesPath(`${i18n.language}/translation.json`);
fetch(loadPath)
.then((response) => {
if (!response.ok) {
if (isDevelopment) {
return fetch(
`/locales/${i18n.language}/translation.json`
);
}
throw new Error(
`Failed to fetch translation: ${response.status}`
);
@ -125,13 +119,13 @@ i18nInstance
if (isDevelopment) {
try {
setTimeout(() => {
fetch(
`/locales/${i18n.language}/translation.json`,
{
const retryPath = getLocalesPath(
`${i18n.language}/translation.json`
);
fetch(retryPath, {
headers: { Accept: 'application/json' },
mode: 'cors',
}
)
})
.then((res) => res.json())
.then((data) => {
i18n.addResourceBundle(
@ -182,17 +176,9 @@ i18n.on('languageChanged', (lng) => {
};
if (!i18n.hasResourceBundle(lng, 'translation')) {
const loadPath = isDevelopment
? `./locales/${lng}/translation.json`
: `/locales/${lng}/translation.json`;
const loadPath = getLocalesPath(`${lng}/translation.json`);
fetch(loadPath)
.then((response) => {
if (!response.ok) {
return fetch(`/locales/${lng}/translation.json`);
}
return response;
})
.then((response) => response.json())
.then((data) => {
if (data) {

View file

@ -8,6 +8,7 @@ import './i18n'; // Import i18n config to initialize it
import './styles/markdown.css'; // Import markdown styles
import { I18nextProvider } from 'react-i18next';
import i18n from './i18n'; // Import the i18n instance with its configuration
import { getBasePath } from './config/paths';
const isDevelopment = process.env.NODE_ENV !== 'production';
@ -50,9 +51,10 @@ const container = document.getElementById('root');
if (container) {
const root = createRoot(container);
const basename = getBasePath();
root.render(
<I18nextProvider i18n={i18n}>
<BrowserRouter>
<BrowserRouter basename={basename || undefined}>
<ToastProvider>
<TelegramStatusProvider>
<App />

View file

@ -1,3 +1,5 @@
import { getApiPath } from '../config/paths';
export interface ApiKeySummary {
id: number;
name: string;
@ -24,7 +26,7 @@ async function handleResponse<T>(response: Response): Promise<T> {
}
export async function fetchApiKeys(): Promise<ApiKeySummary[]> {
const response = await fetch('/api/profile/api-keys', {
const response = await fetch(getApiPath('profile/api-keys'), {
credentials: 'include',
});
return handleResponse<ApiKeySummary[]>(response);
@ -34,7 +36,7 @@ export async function createApiKey(payload: {
name: string;
expires_at?: string | null;
}): Promise<CreateApiKeyResponse> {
const response = await fetch('/api/profile/api-keys', {
const response = await fetch(getApiPath('profile/api-keys'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
@ -44,7 +46,7 @@ export async function createApiKey(payload: {
}
export async function revokeApiKey(id: number): Promise<ApiKeySummary> {
const response = await fetch(`/api/profile/api-keys/${id}/revoke`, {
const response = await fetch(getApiPath(`profile/api-keys/${id}/revoke`), {
method: 'POST',
credentials: 'include',
});
@ -52,7 +54,7 @@ export async function revokeApiKey(id: number): Promise<ApiKeySummary> {
}
export async function deleteApiKey(id: number): Promise<void> {
const response = await fetch(`/api/profile/api-keys/${id}`, {
const response = await fetch(getApiPath(`profile/api-keys/${id}`), {
method: 'DELETE',
credentials: 'include',
});

View file

@ -1,8 +1,9 @@
import { Area } from '../entities/Area';
import { handleAuthResponse } from './authUtils';
import { getApiPath } from '../config/paths';
export const fetchAreas = async (): Promise<Area[]> => {
const response = await fetch('/api/areas', {
const response = await fetch(getApiPath('areas'), {
credentials: 'include',
headers: {
Accept: 'application/json',
@ -13,7 +14,7 @@ export const fetchAreas = async (): Promise<Area[]> => {
};
export const createArea = async (areaData: Partial<Area>): Promise<Area> => {
const response = await fetch('/api/areas', {
const response = await fetch(getApiPath('areas'), {
method: 'POST',
credentials: 'include',
headers: {
@ -31,7 +32,7 @@ export const updateArea = async (
areaUid: string,
areaData: Partial<Area>
): Promise<Area> => {
const response = await fetch(`/api/areas/${areaUid}`, {
const response = await fetch(getApiPath(`areas/${areaUid}`), {
method: 'PATCH',
credentials: 'include',
headers: {
@ -46,7 +47,7 @@ export const updateArea = async (
};
export const deleteArea = async (areaUid: string): Promise<void> => {
const response = await fetch(`/api/areas/${areaUid}`, {
const response = await fetch(getApiPath(`areas/${areaUid}`), {
method: 'DELETE',
credentials: 'include',
headers: {

View file

@ -1,6 +1,7 @@
import { InboxItem } from '../entities/InboxItem';
import { useStore } from '../store/useStore';
import { handleAuthResponse } from './authUtils';
import { getApiPath } from '../config/paths';
// API functions
export const fetchInboxItems = async (
@ -20,7 +21,7 @@ export const fetchInboxItems = async (
offset: offset.toString(),
});
const response = await fetch(`/api/inbox?${params}`, {
const response = await fetch(getApiPath(`inbox?${params}`), {
credentials: 'include',
headers: {
Accept: 'application/json',
@ -55,7 +56,7 @@ export const createInboxItem = async (
content: string,
source?: string
): Promise<InboxItem> => {
const response = await fetch('/api/inbox', {
const response = await fetch(getApiPath('inbox'), {
method: 'POST',
credentials: 'include',
headers: {
@ -73,7 +74,7 @@ export const updateInboxItem = async (
itemUid: string,
content: string
): Promise<InboxItem> => {
const response = await fetch(`/api/inbox/${itemUid}`, {
const response = await fetch(getApiPath(`inbox/${itemUid}`), {
method: 'PATCH',
credentials: 'include',
headers: {
@ -88,7 +89,7 @@ export const updateInboxItem = async (
};
export const processInboxItem = async (itemUid: string): Promise<InboxItem> => {
const response = await fetch(`/api/inbox/${itemUid}/process`, {
const response = await fetch(getApiPath(`inbox/${itemUid}/process`), {
method: 'PATCH',
credentials: 'include',
headers: {
@ -101,7 +102,7 @@ export const processInboxItem = async (itemUid: string): Promise<InboxItem> => {
};
export const deleteInboxItem = async (itemUid: string): Promise<void> => {
const response = await fetch(`/api/inbox/${itemUid}`, {
const response = await fetch(getApiPath(`inbox/${itemUid}`), {
method: 'DELETE',
credentials: 'include',
headers: {

View file

@ -4,9 +4,10 @@ import {
getDefaultHeaders,
getPostHeaders,
} from './authUtils';
import { getApiPath } from '../config/paths';
export const fetchNotes = async (): Promise<Note[]> => {
const response = await fetch('/api/notes', {
const response = await fetch(getApiPath('notes'), {
credentials: 'include',
headers: getDefaultHeaders(),
});
@ -16,7 +17,7 @@ export const fetchNotes = async (): Promise<Note[]> => {
};
export const createNote = async (noteData: Note): Promise<Note> => {
const response = await fetch('/api/note', {
const response = await fetch(getApiPath('note'), {
method: 'POST',
credentials: 'include',
headers: getPostHeaders(),
@ -48,7 +49,7 @@ export const updateNote = async (
// Use the provided noteUid
const noteIdentifier = noteUid;
const response = await fetch(`/api/note/${noteIdentifier}`, {
const response = await fetch(getApiPath(`note/${noteIdentifier}`), {
method: 'PATCH',
credentials: 'include',
headers: getPostHeaders(),
@ -60,7 +61,7 @@ export const updateNote = async (
};
export const deleteNote = async (noteUid: string): Promise<void> => {
const response = await fetch(`/api/note/${noteUid}`, {
const response = await fetch(getApiPath(`note/${noteUid}`), {
method: 'DELETE',
credentials: 'include',
headers: getDefaultHeaders(),
@ -70,7 +71,7 @@ export const deleteNote = async (noteUid: string): Promise<void> => {
};
export const fetchNoteBySlug = async (uidSlug: string): Promise<Note> => {
const response = await fetch(`/api/note/${uidSlug}`, {
const response = await fetch(getApiPath(`note/${uidSlug}`), {
credentials: 'include',
headers: getDefaultHeaders(),
});

View file

@ -1,4 +1,5 @@
import { handleAuthResponse } from './authUtils';
import { getApiPath } from '../config/paths';
interface Profile {
id: number;
@ -34,7 +35,7 @@ interface TelegramBotInfo {
}
export const fetchProfile = async (): Promise<Profile> => {
const response = await fetch('/api/profile', {
const response = await fetch(getApiPath('profile'), {
credentials: 'include',
headers: {
Accept: 'application/json',
@ -47,7 +48,7 @@ export const fetchProfile = async (): Promise<Profile> => {
export const updateProfile = async (
profileData: Partial<Profile>
): Promise<Profile> => {
const response = await fetch('/api/profile', {
const response = await fetch(getApiPath('profile'), {
method: 'PATCH',
credentials: 'include',
headers: {
@ -61,7 +62,7 @@ export const updateProfile = async (
};
export const fetchSchedulerStatus = async (): Promise<SchedulerStatus> => {
const response = await fetch('/api/profile/task-summary/status', {
const response = await fetch(getApiPath('profile/task-summary/status'), {
credentials: 'include',
headers: {
Accept: 'application/json',
@ -72,7 +73,7 @@ export const fetchSchedulerStatus = async (): Promise<SchedulerStatus> => {
};
export const sendTaskSummaryNow = async (): Promise<any> => {
const response = await fetch('/api/profile/task-summary/send-now', {
const response = await fetch(getApiPath('profile/task-summary/send-now'), {
method: 'POST',
credentials: 'include',
headers: {
@ -85,7 +86,7 @@ export const sendTaskSummaryNow = async (): Promise<any> => {
};
export const fetchTelegramPollingStatus = async (): Promise<any> => {
const response = await fetch('/api/telegram/polling-status', {
const response = await fetch(getApiPath('telegram/polling-status'), {
credentials: 'include',
headers: {
Accept: 'application/json',
@ -99,7 +100,7 @@ export const setupTelegram = async (
botToken: string,
chatId: string
): Promise<TelegramBotInfo> => {
const response = await fetch('/api/telegram/setup', {
const response = await fetch(getApiPath('telegram/setup'), {
method: 'POST',
credentials: 'include',
headers: {
@ -116,7 +117,7 @@ export const setupTelegram = async (
};
export const startTelegramPolling = async (): Promise<any> => {
const response = await fetch('/api/telegram/start-polling', {
const response = await fetch(getApiPath('telegram/start-polling'), {
method: 'POST',
credentials: 'include',
headers: {
@ -129,7 +130,7 @@ export const startTelegramPolling = async (): Promise<any> => {
};
export const stopTelegramPolling = async (): Promise<any> => {
const response = await fetch('/api/telegram/stop-polling', {
const response = await fetch(getApiPath('telegram/stop-polling'), {
method: 'POST',
credentials: 'include',
headers: {
@ -145,7 +146,7 @@ export const testTelegram = async (
userId: number,
message: string
): Promise<any> => {
const response = await fetch(`/api/telegram/test/${userId}`, {
const response = await fetch(getApiPath(`telegram/test/${userId}`), {
method: 'POST',
credentials: 'include',
headers: {
@ -159,7 +160,7 @@ export const testTelegram = async (
};
export const toggleTaskSummary = async (): Promise<any> => {
const response = await fetch('/api/profile/task-summary/toggle', {
const response = await fetch(getApiPath('profile/task-summary/toggle'), {
method: 'POST',
credentials: 'include',
headers: {
@ -174,7 +175,7 @@ export const toggleTaskSummary = async (): Promise<any> => {
export const updateTaskSummaryFrequency = async (
frequency: string
): Promise<any> => {
const response = await fetch('/api/profile/task-summary/frequency', {
const response = await fetch(getApiPath('profile/task-summary/frequency'), {
method: 'POST',
credentials: 'include',
headers: {

View file

@ -1,18 +1,19 @@
import { Project } from '../entities/Project';
import { handleAuthResponse } from './authUtils';
import { getApiPath } from '../config/paths';
export const fetchProjects = async (
stateFilter = 'all',
areaFilter = ''
): Promise<Project[]> => {
let url = `/api/projects`;
let url = 'projects';
const params = new URLSearchParams();
if (stateFilter !== 'all') params.append('state', stateFilter);
if (areaFilter) params.append('area', areaFilter);
if (params.toString()) url += `?${params.toString()}`;
const response = await fetch(url, {
const response = await fetch(getApiPath(url), {
credentials: 'include',
headers: { Accept: 'application/json' },
});
@ -27,7 +28,7 @@ export const fetchGroupedProjects = async (
stateFilter = 'all',
areaFilter = ''
): Promise<Record<string, Project[]>> => {
let url = `/api/projects`;
let url = 'projects';
const params = new URLSearchParams();
params.append('grouped', 'true');
@ -35,7 +36,7 @@ export const fetchGroupedProjects = async (
if (areaFilter) params.append('area', areaFilter);
if (params.toString()) url += `?${params.toString()}`;
const response = await fetch(url, {
const response = await fetch(getApiPath(url), {
credentials: 'include',
headers: { Accept: 'application/json' },
});
@ -47,7 +48,7 @@ export const fetchGroupedProjects = async (
};
export const fetchProjectById = async (projectId: string): Promise<Project> => {
const response = await fetch(`/api/project/${projectId}`, {
const response = await fetch(getApiPath(`project/${projectId}`), {
credentials: 'include',
headers: { Accept: 'application/json' },
});
@ -59,7 +60,7 @@ export const fetchProjectById = async (projectId: string): Promise<Project> => {
export const createProject = async (
projectData: Partial<Project>
): Promise<Project> => {
const response = await fetch('/api/project', {
const response = await fetch(getApiPath('project'), {
method: 'POST',
credentials: 'include',
headers: {
@ -77,7 +78,7 @@ export const updateProject = async (
projectUid: string,
projectData: Partial<Project>
): Promise<Project> => {
const response = await fetch(`/api/project/${projectUid}`, {
const response = await fetch(getApiPath(`project/${projectUid}`), {
method: 'PATCH',
credentials: 'include',
headers: {
@ -98,7 +99,7 @@ export const deleteProject = async (projectUid: string): Promise<void> => {
console.log('Attempting to delete project with UID:', projectUid);
const response = await fetch(`/api/project/${projectUid}`, {
const response = await fetch(getApiPath(`project/${projectUid}`), {
method: 'DELETE',
credentials: 'include',
headers: {
@ -120,7 +121,7 @@ export const deleteProject = async (projectUid: string): Promise<void> => {
};
export const fetchProjectBySlug = async (uidSlug: string): Promise<Project> => {
const response = await fetch(`/api/project/${uidSlug}`, {
const response = await fetch(getApiPath(`project/${uidSlug}`), {
credentials: 'include',
headers: {
Accept: 'application/json',

View file

@ -1,3 +1,5 @@
import { getApiPath } from '../config/paths';
interface SearchParams {
query: string;
filters?: string[];
@ -43,13 +45,16 @@ export const searchUniversal = async (
queryParams.append('tags', params.tags.join(','));
}
const response = await fetch(`/api/search?${queryParams.toString()}`, {
const response = await fetch(
getApiPath(`search?${queryParams.toString()}`),
{
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
});
}
);
if (!response.ok) {
throw new Error('Search request failed');

View file

@ -1,3 +1,5 @@
import { getApiPath } from '../config/paths';
export type AccessLevel = 'ro' | 'rw';
export interface ShareGrantRequest {
@ -8,7 +10,7 @@ export interface ShareGrantRequest {
}
export async function grantShare(req: ShareGrantRequest): Promise<void> {
const res = await fetch('/api/shares', {
const res = await fetch(getApiPath('shares'), {
method: 'POST',
credentials: 'include',
headers: {
@ -41,7 +43,7 @@ export async function listShares(
resource_uid: string
): Promise<ListSharesResponseRow[]> {
const params = new URLSearchParams({ resource_type, resource_uid });
const res = await fetch(`/api/shares?${params.toString()}`, {
const res = await fetch(getApiPath(`shares?${params.toString()}`), {
method: 'GET',
credentials: 'include',
headers: { Accept: 'application/json' },
@ -65,7 +67,7 @@ export async function revokeShare(
resource_uid: string,
target_user_id: number
): Promise<void> {
const res = await fetch('/api/shares', {
const res = await fetch(getApiPath('shares'), {
method: 'DELETE',
credentials: 'include',
headers: {

View file

@ -1,10 +1,11 @@
import { Tag } from '../entities/Tag';
import { handleAuthResponse } from './authUtils';
import { extractUidFromSlug } from './slugUtils';
import { getApiPath } from '../config/paths';
export const fetchTags = async (): Promise<Tag[]> => {
try {
const response = await fetch('/api/tags', {
const response = await fetch(getApiPath('tags'), {
credentials: 'include',
headers: {
Accept: 'application/json',
@ -21,7 +22,7 @@ export const fetchTags = async (): Promise<Tag[]> => {
};
export const createTag = async (tagData: Tag): Promise<Tag> => {
const response = await fetch('/api/tag', {
const response = await fetch(getApiPath('tag'), {
method: 'POST',
credentials: 'include',
headers: {
@ -53,7 +54,7 @@ export const createTag = async (tagData: Tag): Promise<Tag> => {
};
export const updateTag = async (tagUid: string, tagData: Tag): Promise<Tag> => {
const response = await fetch(`/api/tag/${tagUid}`, {
const response = await fetch(getApiPath(`tag/${tagUid}`), {
method: 'PATCH',
credentials: 'include',
headers: {
@ -84,7 +85,7 @@ export const updateTag = async (tagUid: string, tagData: Tag): Promise<Tag> => {
};
export const deleteTag = async (tagUid: string): Promise<void> => {
const response = await fetch(`/api/tag/${tagUid}`, {
const response = await fetch(getApiPath(`tag/${tagUid}`), {
method: 'DELETE',
credentials: 'include',
headers: {
@ -99,12 +100,15 @@ export const fetchTagBySlug = async (uidSlug: string): Promise<Tag> => {
// Extract uid from uidSlug using proper extraction function
const uid = extractUidFromSlug(uidSlug);
const response = await fetch(`/api/tag?uid=${encodeURIComponent(uid)}`, {
const response = await fetch(
getApiPath(`tag?uid=${encodeURIComponent(uid)}`),
{
credentials: 'include',
headers: {
Accept: 'application/json',
},
});
}
);
await handleAuthResponse(response, 'Failed to fetch tag.');
return await response.json();

View file

@ -5,6 +5,7 @@ import {
getDefaultHeaders,
getPostHeaders,
} from './authUtils';
import { getApiPath } from '../config/paths';
export interface GroupedTasks {
[groupName: string]: Task[];
@ -30,11 +31,11 @@ export const fetchTasks = async (
// Fetch tasks and metrics in parallel for better performance
const [tasksResponse, metricsResponse] = await Promise.all([
fetch(`/api/tasks${tasksQuery}`, {
fetch(getApiPath(`tasks${tasksQuery}`), {
credentials: 'include',
headers: getDefaultHeaders(),
}),
fetch('/api/tasks/metrics', {
fetch(getApiPath('tasks/metrics'), {
credentials: 'include',
headers: getDefaultHeaders(),
}),
@ -63,7 +64,7 @@ export const fetchTasks = async (
};
export const createTask = async (taskData: Task): Promise<Task> => {
const response = await fetch('/api/task', {
const response = await fetch(getApiPath('task'), {
method: 'POST',
credentials: 'include',
headers: getPostHeaders(),
@ -78,7 +79,7 @@ export const updateTask = async (
taskId: number,
taskData: Task
): Promise<Task> => {
const response = await fetch(`/api/task/${taskId}`, {
const response = await fetch(getApiPath(`task/${taskId}`), {
method: 'PATCH',
credentials: 'include',
headers: getPostHeaders(),
@ -108,7 +109,7 @@ export const toggleTaskCompletion = async (
};
export const deleteTask = async (taskId: number): Promise<void> => {
const response = await fetch(`/api/task/${taskId}`, {
const response = await fetch(getApiPath(`task/${taskId}`), {
method: 'DELETE',
credentials: 'include',
headers: getDefaultHeaders(),
@ -118,7 +119,7 @@ export const deleteTask = async (taskId: number): Promise<void> => {
};
export const fetchTaskById = async (taskId: number): Promise<Task> => {
const response = await fetch(`/api/task/${taskId}`, {
const response = await fetch(getApiPath(`task/${taskId}`), {
credentials: 'include',
headers: getDefaultHeaders(),
});
@ -128,17 +129,20 @@ export const fetchTaskById = async (taskId: number): Promise<Task> => {
};
export const fetchTaskByUid = async (uid: string): Promise<Task> => {
const response = await fetch(`/api/task/${encodeURIComponent(uid)}`, {
const response = await fetch(
getApiPath(`task/${encodeURIComponent(uid)}`),
{
credentials: 'include',
headers: getDefaultHeaders(),
});
}
);
await handleAuthResponse(response, 'Failed to fetch task.');
return await response.json();
};
export const fetchSubtasks = async (parentTaskId: number): Promise<Task[]> => {
const response = await fetch(`/api/task/${parentTaskId}/subtasks`, {
const response = await fetch(getApiPath(`task/${parentTaskId}/subtasks`), {
credentials: 'include',
headers: getDefaultHeaders(),
});
@ -171,8 +175,10 @@ export const fetchTaskNextIterations = async (
startFromDate?: string
): Promise<TaskIteration[]> => {
const url = startFromDate
? `/api/task/${taskId}/next-iterations?startFromDate=${startFromDate}`
: `/api/task/${taskId}/next-iterations`;
? getApiPath(
`task/${taskId}/next-iterations?startFromDate=${startFromDate}`
)
: getApiPath(`task/${taskId}/next-iterations`);
const response = await fetch(url, {
credentials: 'include',

View file

@ -2,6 +2,7 @@
* Service for URL-related operations like extracting titles from web pages
*/
import { handleAuthResponse } from './authUtils';
import { getApiPath } from '../config/paths';
export interface UrlTitleResult {
url: string;
@ -20,7 +21,7 @@ export interface UrlTitleResult {
export const extractUrlTitle = async (url: string): Promise<UrlTitleResult> => {
try {
const response = await fetch(
`/api/url/title?url=${encodeURIComponent(url)}`,
getApiPath(`url/title?url=${encodeURIComponent(url)}`),
{
credentials: 'include',
headers: {
@ -46,7 +47,7 @@ export const extractTitleFromText = async (
text: string
): Promise<UrlTitleResult | null> => {
try {
const response = await fetch('/api/url/extract-from-text', {
const response = await fetch(getApiPath('url/extract-from-text'), {
method: 'POST',
credentials: 'include',
headers: {

View file

@ -3,19 +3,18 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/">
<title>tududi</title>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16.png">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16.png">
<!-- Web app manifest for PWA support -->
<link rel="manifest" href="/manifest.json">
<link rel="manifest" href="manifest.json">
</head>
<body>
<div id="root"></div>

View file

@ -17,8 +17,8 @@ module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
publicPath: '/',
clean: true
publicPath: '',
clean: true,
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
@ -32,20 +32,24 @@ module.exports = {
port: 8080,
host: '0.0.0.0',
historyApiFallback: true,
proxy: [{
proxy: [
{
context: ['/api', '/locales'],
target: 'http://localhost:3002',
changeOrigin: true,
secure: false,
cookieDomainRewrite: 'localhost',
headers: {
'Access-Control-Allow-Origin': '*'
'Access-Control-Allow-Origin': '*',
},
onProxyRes: function (proxyRes, req, res) {
proxyRes.headers['Access-Control-Allow-Origin'] = 'http://localhost:8080';
proxyRes.headers['Access-Control-Allow-Credentials'] = 'true';
}
}],
proxyRes.headers['Access-Control-Allow-Origin'] =
'http://localhost:8080';
proxyRes.headers['Access-Control-Allow-Credentials'] =
'true';
},
},
],
// Add middleware to log requests for translation files to help with debugging
setupMiddlewares: (middlewares, devServer) => {
if (!devServer) {
@ -67,6 +71,7 @@ module.exports = {
Object.fromEntries(
Object.entries({
ENABLE_NOTE_COLOR: process.env.ENABLE_NOTE_COLOR,
TUDUDI_BASE_PATH: process.env.TUDUDI_BASE_PATH || '',
}).map(([key, value]) => [
`process.env.${key}`,
JSON.stringify(value),
@ -76,7 +81,7 @@ module.exports = {
new HtmlWebpackPlugin({
title: 'tududi',
filename: 'index.html',
template: 'public/index.html'
template: 'public/index.html',
}),
new CopyWebpackPlugin({
patterns: [