Fix static base path (#549)
This commit is contained in:
parent
b0041bafe1
commit
673a6a56ee
41 changed files with 496 additions and 315 deletions
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
115
frontend/config/paths.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}],
|
||||
onProxyRes: function (proxyRes, req, res) {
|
||||
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: [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue