Sửa lỗi tái cấu trúc thư mục và khai báo import
This commit is contained in:
@@ -1,14 +1,14 @@
|
|||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, Query, ForbiddenException } from '@nestjs/common';
|
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, Query, ForbiddenException } from '@nestjs/common';
|
||||||
import { PrismaService } from './prisma.service.js';
|
import { PrismaService } from '../prisma/prisma.service.js';
|
||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import { AdminGuard } from './admin.guard.js';
|
import { AdminGuard } from './auth/admin.guard.js';
|
||||||
import { JwtModule, JwtService } from '@nestjs/jwt';
|
import { JwtModule, JwtService } from '@nestjs/jwt';
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard.js';
|
import { JwtAuthGuard } from './auth/jwt-auth.guard.js';
|
||||||
import { JwtStrategy } from './jwt.strategy.js';
|
import { JwtStrategy } from './auth/jwt.strategy.js';
|
||||||
import { TourRoleGuard } from './rbac.middleware.js';
|
import { TourRoleGuard } from './common/rbac.middleware.js';
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
class AppController {
|
class AppController {
|
||||||
@@ -1,12 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"ignoreDeprecations": "5.0",
|
"module": "ESNext",
|
||||||
"module": "NodeNext",
|
"moduleResolution": "node",
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"rewriteRelativeImportExtensions": true,
|
|
||||||
"noEmit": false,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
@@ -15,12 +10,18 @@
|
|||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"incremental": true,
|
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
}
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"]
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/index.tsx"></script>
|
<script type="module" src="/src/index.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
|
"lucide-react": "^0.284.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-leaflet": "^4.2.1",
|
||||||
|
"react-leaflet-cluster": "^2.1.0",
|
||||||
|
"zustand": "^5.0.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.3.1",
|
||||||
|
"@types/leaflet": "^1.9.12",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
|
"autoprefixer": "^10.5.0",
|
||||||
|
"postcss": "^8.5.15",
|
||||||
|
"tailwindcss": "^4.3.1",
|
||||||
|
"typescript": "^5.7.0",
|
||||||
|
"vite": "^8.0.16"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
"@tailwindcss/postcss": {},
|
||||||
"autoprefixer": {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { LandingPage } from './LandingPage.js';
|
import { LandingPage } from '@/pages/LandingPage';
|
||||||
import { TourDetailPage } from './TourDetailPage.js';
|
import { TourDetailPage } from '@/pages/TourDetailPage';
|
||||||
import { ExploreMap } from './ExploreMap.js';
|
import { ExploreMap } from '@/pages/ExploreMap';
|
||||||
import { SignupPage } from './SignupPage.js';
|
import { SignupPage } from '@/pages/SignupPage';
|
||||||
import { useTourStore } from './useTourStore.js';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
type View = 'landing' | 'explore' | 'detail' | 'signup';
|
type View = 'landing' | 'explore' | 'detail' | 'signup';
|
||||||
@@ -14,9 +14,6 @@ const App = () => {
|
|||||||
const fetchTour = useTourStore(state => state.fetchTour);
|
const fetchTour = useTourStore(state => state.fetchTour);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Tự động xác định địa chỉ IP của Backend dựa trên hostname hiện tại
|
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
|
||||||
|
|
||||||
// Khôi phục phiên đăng nhập từ localStorage
|
// Khôi phục phiên đăng nhập từ localStorage
|
||||||
const savedUser = localStorage.getItem('user');
|
const savedUser = localStorage.getItem('user');
|
||||||
if (savedUser) {
|
if (savedUser) {
|
||||||
@@ -26,7 +23,7 @@ const App = () => {
|
|||||||
setIsUserLoaded(true); // Đánh dấu user đã được load
|
setIsUserLoaded(true); // Đánh dấu user đã được load
|
||||||
|
|
||||||
// Kiểm tra xem hệ thống đã được cài đặt chưa
|
// Kiểm tra xem hệ thống đã được cài đặt chưa
|
||||||
fetch(`${API_BASE}/api/v1/auth/status`)
|
fetch(`/api/v1/auth/status`)
|
||||||
.then(res => res.ok ? res.json() : Promise.reject())
|
.then(res => res.ok ? res.json() : Promise.reject())
|
||||||
.then(data => setIsInitialSetup(!!data.isInitialSetup))
|
.then(data => setIsInitialSetup(!!data.isInitialSetup))
|
||||||
.catch(() => setIsInitialSetup(false));
|
.catch(() => setIsInitialSetup(false));
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { X, MapPin, Loader2, Clock, Map as MapIcon } from 'lucide-react';
|
import { X, MapPin, Loader2, Clock, Map as MapIcon } from 'lucide-react';
|
||||||
import { useTourStore } from './useTourStore.js';
|
import { useTourStore } from '@/store/useTourStore.js';
|
||||||
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet';
|
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
import { useTourStore } from './useTourStore.js';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
|
|
||||||
export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolean, onClose: () => void, onSuccess: (tour: any) => void }) => {
|
export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolean, onClose: () => void, onSuccess: (tour: any) => void }) => {
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
@@ -23,8 +23,7 @@ export const CreateTourModal = ({ isOpen, onClose, onSuccess }: { isOpen: boolea
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const res = await fetch(`/api/v1/users?q=${encodeURIComponent(value)}`, {
|
||||||
const res = await fetch(`${API_BASE}/api/v1/users?q=${encodeURIComponent(value)}`, {
|
|
||||||
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` },
|
headers: { Authorization: `Bearer ${localStorage.getItem('token')}` },
|
||||||
});
|
});
|
||||||
if (!res.ok) throw new Error('Không thể tải người dùng');
|
if (!res.ok) throw new Error('Không thể tải người dùng');
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { Wallet, Users, Info } from 'lucide-react';
|
import { Wallet, Users, Info } from 'lucide-react';
|
||||||
import { useTourStore } from './useTourStore.js';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
|
|
||||||
export const ExpenseManager = () => {
|
export const ExpenseManager = () => {
|
||||||
const { legs } = useTourStore();
|
const { legs } = useTourStore();
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { format, differenceInMinutes, parseISO } from 'date-fns';
|
import { format, differenceInMinutes, parseISO } from 'date-fns';
|
||||||
import { CheckCircle2, Circle, Clock, MapPin, AlertCircle, Zap, Navigation, Edit2, Trash2, Plus, List } from 'lucide-react';
|
import { CheckCircle2, Circle, Clock, MapPin, AlertCircle, Zap, Navigation, Edit2, Trash2, Plus, List } from 'lucide-react';
|
||||||
import { useTourStore } from './useTourStore.js';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
import { ConfirmModal } from './ConfirmModal.js';
|
import { ConfirmModal } from '@/components/ConfirmModal';
|
||||||
|
|
||||||
const TimeVariance = ({ planned, actual }: { planned: string, actual: string | null }) => {
|
const TimeVariance = ({ planned, actual }: { planned: string, actual: string | null }) => {
|
||||||
if (!actual) return null;
|
if (!actual) return null;
|
||||||
@@ -40,7 +40,17 @@ export const ItineraryTimeline = ({
|
|||||||
onAddLocation,
|
onAddLocation,
|
||||||
onEditLocation
|
onEditLocation
|
||||||
}: { onAddLocation?: (legId: string) => void, onEditLocation?: (location: any) => void }) => {
|
}: { onAddLocation?: (legId: string) => void, onEditLocation?: (location: any) => void }) => {
|
||||||
const { currentTour, legs, optimizeRouting, userRole, addLeg, updateLeg, deleteLeg, initializeLegs, deleteLocation } = useTourStore();
|
// Tối ưu hóa selectors để chỉ lắng nghe những thay đổi cần thiết
|
||||||
|
const currentTour = useTourStore(state => state.currentTour);
|
||||||
|
const legs = useTourStore(state => state.legs);
|
||||||
|
const userRole = useTourStore(state => state.userRole);
|
||||||
|
const optimizeRouting = useTourStore(state => state.optimizeRouting);
|
||||||
|
const addLeg = useTourStore(state => state.addLeg);
|
||||||
|
const updateLeg = useTourStore(state => state.updateLeg);
|
||||||
|
const deleteLeg = useTourStore(state => state.deleteLeg);
|
||||||
|
const initializeLegs = useTourStore(state => state.initializeLegs);
|
||||||
|
const deleteLocation = useTourStore(state => state.deleteLocation);
|
||||||
|
|
||||||
const [confirmState, setConfirmState] = useState<{ open: boolean; title?: string; message?: string; onConfirm?: () => void }>({ open: false });
|
const [confirmState, setConfirmState] = useState<{ open: boolean; title?: string; message?: string; onConfirm?: () => void }>({ open: false });
|
||||||
|
|
||||||
const toggleComplete = async (locationId: string) => {
|
const toggleComplete = async (locationId: string) => {
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { X, CheckCircle, AlertCircle, Info } from 'lucide-react';
|
||||||
|
|
||||||
|
interface NotificationModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
type?: 'success' | 'error' | 'info';
|
||||||
|
onConfirm: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NotificationModal - Component hiển thị thông báo phản hồi cho người dùng
|
||||||
|
*/
|
||||||
|
export const NotificationModal: React.FC<NotificationModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
title = 'Thông báo',
|
||||||
|
message,
|
||||||
|
type = 'info',
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
success: <CheckCircle className="w-12 h-12 text-green-500" />,
|
||||||
|
error: <AlertCircle className="w-12 h-12 text-red-500" />,
|
||||||
|
info: <Info className="w-12 h-12 text-blue-500" />,
|
||||||
|
};
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
success: 'bg-green-600 hover:bg-green-700 shadow-green-100',
|
||||||
|
error: 'bg-red-600 hover:bg-red-700 shadow-red-100',
|
||||||
|
info: 'bg-blue-600 hover:bg-blue-700 shadow-blue-100',
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[3000] flex items-center justify-center p-4">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div className="absolute inset-0 bg-gray-900/60 backdrop-blur-sm animate-in fade-in duration-200" onClick={onConfirm} />
|
||||||
|
|
||||||
|
{/* Modal Content */}
|
||||||
|
<div className="relative w-full max-w-sm bg-white rounded-[32px] shadow-2xl overflow-hidden p-8 text-center animate-in zoom-in-95 duration-200">
|
||||||
|
<div className="flex justify-center mb-5">
|
||||||
|
{icons[type]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-black text-gray-900 mb-2">{title}</h2>
|
||||||
|
<p className="text-gray-500 text-sm leading-relaxed mb-8">
|
||||||
|
{message || "Bạn không được phép gỡ bỏ thành viên này!"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onConfirm}
|
||||||
|
className={`w-full py-4 text-white font-bold rounded-2xl transition-all shadow-lg active:scale-95 ${colors[type]}`}
|
||||||
|
>
|
||||||
|
Đã hiểu
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook để quản lý trạng thái của NotificationModal
|
||||||
|
*/
|
||||||
|
export const useNotificationModal = () => {
|
||||||
|
const [modalState, setModalState] = useState<{
|
||||||
|
isOpen: boolean;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
type?: 'success' | 'error' | 'info';
|
||||||
|
}>({
|
||||||
|
isOpen: false,
|
||||||
|
title: 'Thông báo',
|
||||||
|
message: '',
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
|
||||||
|
const openModal = (title: string, message: string, type: 'success' | 'error' | 'info' = 'info') => {
|
||||||
|
setModalState({ isOpen: true, title, message, type });
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setModalState((prev) => ({ ...prev, isOpen: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return { modalState, openModal, closeModal };
|
||||||
|
};
|
||||||
@@ -4,10 +4,10 @@ import _MarkerClusterGroup from 'react-leaflet-cluster';
|
|||||||
const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup;
|
const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup;
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { useTourStore } from './useTourStore.js';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
import { X, Navigation, Image as ImageIcon, LogOut, Settings, Edit2, Trash2 } from 'lucide-react';
|
import { X, Navigation, Image as ImageIcon, LogOut, Settings, Edit2, Trash2 } from 'lucide-react';
|
||||||
import { UserManagementModal } from './UserManagementModal.js';
|
import { UserManagementModal } from '@/components/UserManagementModal';
|
||||||
import { CreateTourModal } from './CreateTourModal.js';
|
import { CreateTourModal } from '../components/CreateTourModal';
|
||||||
|
|
||||||
// Fix lỗi icon mặc định của Leaflet
|
// Fix lỗi icon mặc định của Leaflet
|
||||||
const DefaultIcon = L.icon({
|
const DefaultIcon = L.icon({
|
||||||
@@ -46,8 +46,11 @@ function MapTracker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void }) => {
|
export const ExploreMap = ({ onBack, onLogout, user, onViewTour }: { onBack: () => void, onLogout?: () => void, user?: any, onViewTour: (id: string) => void }) => {
|
||||||
// Thêm fetchTour vào destructuring từ store
|
// Tối ưu hóa việc lấy dữ liệu từ store bằng selectors để tránh re-render thừa
|
||||||
const { publicTours, fetchPublicTours, fetchTour, setMapCenter } = useTourStore();
|
const publicTours = useTourStore(state => state.publicTours);
|
||||||
|
const fetchPublicTours = useTourStore(state => state.fetchPublicTours);
|
||||||
|
const fetchTour = useTourStore(state => state.fetchTour);
|
||||||
|
const setMapCenter = useTourStore(state => state.setMapCenter);
|
||||||
|
|
||||||
// Khởi tạo vị trí từ localStorage nếu có, nếu không dùng mặc định (TP.HCM)
|
// Khởi tạo vị trí từ localStorage nếu có, nếu không dùng mặc định (TP.HCM)
|
||||||
const [initialViewState] = useState(() => {
|
const [initialViewState] = useState(() => {
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { LogIn, Compass, ArrowRight, Map as MapIcon, UserPlus, ShieldCheck } from 'lucide-react';
|
import { LogIn, Compass, ArrowRight, Map as MapIcon, UserPlus, ShieldCheck } from 'lucide-react';
|
||||||
import { LoginModal } from './LoginModal.js';
|
import { LoginModal } from '../components/LoginModal';
|
||||||
|
|
||||||
const TRAVEL_IMAGES = [
|
const TRAVEL_IMAGES = [
|
||||||
"https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?auto=format&fit=crop&q=80",
|
"https://images.unsplash.com/photo-1476514525535-07fb3b4ae5f1?auto=format&fit=crop&q=80",
|
||||||
@@ -29,9 +29,7 @@ export const SignupPage: React.FC<SignupPageProps> = ({ onBack, onSuccess }) =>
|
|||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const API_BASE = `http://${window.location.hostname}:3001`;
|
const response = await fetch(`/api/v1/auth/signup`, {
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE}/api/v1/auth/signup`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
import React, { useState, useEffect, useMemo } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { ItineraryTimeline } from './ItineraryTimeline.js';
|
import { ItineraryTimeline } from '../components/ItineraryTimeline';
|
||||||
import { ExpenseManager } from './ExpenseManager.js';
|
import { ExpenseManager } from '../components/ExpenseManager';
|
||||||
import { useTourStore } from './useTourStore.js';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
import { AddLocationModal } from './AddLocationModal.js';
|
import { AddLocationModal } from '@/components/AddLocationModal';
|
||||||
import { AddMemberModal } from './AddMemberModal.js';
|
import { AddMemberModal } from '../components/AddMemberModal';
|
||||||
import { ConfirmModal } from './ConfirmModal.js';
|
import { ConfirmModal } from '../components/ConfirmModal';
|
||||||
import { NotificationModal, useNotificationModal } from './components/NotificationModal.js';
|
import { NotificationModal, useNotificationModal } from '@/components/NotificationModal';
|
||||||
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline } from 'react-leaflet';
|
import { MapContainer, TileLayer, Marker, Popup, useMapEvents, Polyline, useMap } from 'react-leaflet';
|
||||||
import _MarkerClusterGroup from 'react-leaflet-cluster';
|
import _MarkerClusterGroup from 'react-leaflet-cluster';
|
||||||
const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup;
|
const MarkerClusterGroup = (_MarkerClusterGroup as any).default || _MarkerClusterGroup;
|
||||||
import { useMap } from 'react-leaflet';
|
|
||||||
import {
|
import {
|
||||||
Map as MapIcon,
|
Map as MapIcon,
|
||||||
Wallet,
|
Wallet,
|
||||||
@@ -175,12 +174,24 @@ export const TourDetailPage = ({ onBack }: { onBack: () => void }) => {
|
|||||||
const [joinRequestActionId, setJoinRequestActionId] = useState<string | null>(null);
|
const [joinRequestActionId, setJoinRequestActionId] = useState<string | null>(null);
|
||||||
const [confirmState, setConfirmState] = useState<{ open: boolean; title?: string; message?: string; onConfirm?: () => void }>({ open: false });
|
const [confirmState, setConfirmState] = useState<{ open: boolean; title?: string; message?: string; onConfirm?: () => void }>({ open: false });
|
||||||
|
|
||||||
const {
|
// Tách biệt các state và actions để tối ưu performance
|
||||||
currentTour, legs, fetchTour, fetchPublicTours, publicTours,
|
const currentTour = useTourStore(state => state.currentTour);
|
||||||
userRole, mapCenter, setMapCenter, updateTourStartPoint,
|
const legs = useTourStore(state => state.legs);
|
||||||
updateTourEndPoint, initializeLegs, addLocation, addMember, removeMember,
|
const publicTours = useTourStore(state => state.publicTours);
|
||||||
fetchJoinRequests, acceptJoinRequest, rejectJoinRequest
|
const userRole = useTourStore(state => state.userRole);
|
||||||
} = useTourStore();
|
const mapCenter = useTourStore(state => state.mapCenter);
|
||||||
|
|
||||||
|
const fetchTour = useTourStore(state => state.fetchTour);
|
||||||
|
const fetchPublicTours = useTourStore(state => state.fetchPublicTours);
|
||||||
|
const setMapCenter = useTourStore(state => state.setMapCenter);
|
||||||
|
const updateTourStartPoint = useTourStore(state => state.updateTourStartPoint);
|
||||||
|
const updateTourEndPoint = useTourStore(state => state.updateTourEndPoint);
|
||||||
|
const initializeLegs = useTourStore(state => state.initializeLegs);
|
||||||
|
const addLocation = useTourStore(state => state.addLocation);
|
||||||
|
const removeMember = useTourStore(state => state.removeMember);
|
||||||
|
const fetchJoinRequests = useTourStore(state => state.fetchJoinRequests);
|
||||||
|
const acceptJoinRequest = useTourStore(state => state.acceptJoinRequest);
|
||||||
|
const rejectJoinRequest = useTourStore(state => state.rejectJoinRequest);
|
||||||
|
|
||||||
const notificationModal = useNotificationModal();
|
const notificationModal = useNotificationModal();
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import type { Config } from 'tailwindcss';
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
'./index.html',
|
||||||
|
'./src/**/*.{js,ts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3002,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://127.0.0.1:3001',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Generated
+129
-6421
File diff suppressed because it is too large
Load Diff
+12
-49
@@ -1,57 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "travel-planning-backend",
|
"name": "travel-planning-monorepo",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Travel Planning Backend with NestJS and Prisma 7",
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"workspaces": [
|
||||||
|
"backend",
|
||||||
|
"frontend"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "nest build",
|
"build": "npm run build --workspace=backend && npm run build --workspace=frontend",
|
||||||
"start": "nest start",
|
"start:dev": "concurrently \"npm run start:dev --workspace=backend\" \"npm run dev --workspace=frontend\"",
|
||||||
"start:dev": "nest start --watch",
|
"db:migrate": "npm run db:migrate --workspace=backend",
|
||||||
"frontend": "vite",
|
"db:generate": "npm run db:generate --workspace=backend",
|
||||||
"db:migrate": "npx prisma migrate dev",
|
"db:seed": "npm run db:seed --workspace=backend"
|
||||||
"db:generate": "npx prisma generate",
|
|
||||||
"db:seed": "npx tsx seed.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@nestjs/common": "^11.1.26",
|
|
||||||
"@nestjs/core": "^11.1.26",
|
|
||||||
"@nestjs/jwt": "^11.0.2",
|
|
||||||
"@nestjs/passport": "^11.0.5",
|
|
||||||
"@nestjs/platform-express": "^11.1.26",
|
|
||||||
"@prisma/adapter-pg": "^7.8.0",
|
|
||||||
"@prisma/client": "^7.8.0",
|
|
||||||
"bcrypt": "^5.1.1",
|
|
||||||
"date-fns": "^2.30.0",
|
|
||||||
"dotenv": "^16.3.0",
|
|
||||||
"leaflet": "^1.9.4",
|
|
||||||
"lucide-react": "^0.284.0",
|
|
||||||
"passport": "^0.7.0",
|
|
||||||
"passport-jwt": "^4.0.1",
|
|
||||||
"pg": "^8.11.0",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1",
|
|
||||||
"react-leaflet": "^4.2.1",
|
|
||||||
"react-leaflet-cluster": "^2.1.0",
|
|
||||||
"reflect-metadata": "^0.1.13",
|
|
||||||
"rxjs": "^7.8.1",
|
|
||||||
"zustand": "^5.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"concurrently": "^8.2.2"
|
||||||
"@tailwindcss/postcss": "^4.3.1",
|
|
||||||
"@types/bcrypt": "^5.0.2",
|
|
||||||
"@types/leaflet": "^1.9.12",
|
|
||||||
"@types/node": "^20.0.0",
|
|
||||||
"@types/passport-jwt": "^4.0.1",
|
|
||||||
"@types/pg": "^8.10.0",
|
|
||||||
"@types/react": "^18.3.12",
|
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
|
||||||
"autoprefixer": "^10.5.0",
|
|
||||||
"postcss": "^8.5.15",
|
|
||||||
"prisma": "^7.8.0",
|
|
||||||
"tailwindcss": "^4.3.1",
|
|
||||||
"tsx": "^4.0.0",
|
|
||||||
"typescript": "^5.7.0",
|
|
||||||
"vite": "^8.0.16"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Ensure the script exits if any command fails
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "Starting monorepo refactoring..."
|
||||||
|
|
||||||
|
# --- 1. Create new directories ---
|
||||||
|
echo "Creating new directories..."
|
||||||
|
|
||||||
|
mkdir -p backend/src/auth
|
||||||
|
mkdir -p backend/src/common
|
||||||
|
mkdir -p backend/deprecated-migrations
|
||||||
|
mkdir -p frontend/src/components
|
||||||
|
mkdir -p frontend/src/pages
|
||||||
|
mkdir -p frontend/src/store
|
||||||
|
mkdir -p docs
|
||||||
|
|
||||||
|
echo "Directories created."
|
||||||
|
|
||||||
|
# --- 2. Move files to backend/ ---
|
||||||
|
echo "Moving backend files..."
|
||||||
|
|
||||||
|
mv admin.guard.ts backend/src/auth/
|
||||||
|
mv jwt-auth.guard.ts backend/src/auth/
|
||||||
|
mv jwt.strategy.ts backend/src/auth/
|
||||||
|
mv rbac.middleware.ts backend/src/common/
|
||||||
|
mv main.ts backend/src/
|
||||||
|
mv nest-cli.json backend/
|
||||||
|
mv tsconfig.json backend/
|
||||||
|
mv tsconfig.build.json backend/
|
||||||
|
mv seed.ts backend/
|
||||||
|
|
||||||
|
# Special handling for prisma directory and prisma.service.ts
|
||||||
|
# First, move the existing prisma directory to backend/
|
||||||
|
mv prisma backend/
|
||||||
|
|
||||||
|
# Then, move prisma.service.ts into the newly moved backend/prisma directory
|
||||||
|
mv prisma.service.ts backend/prisma/
|
||||||
|
|
||||||
|
# Move old migrations (not Prisma's) to deprecated-migrations
|
||||||
|
if [ -d "migrations" ]; then
|
||||||
|
mv migrations backend/deprecated-migrations/
|
||||||
|
else
|
||||||
|
echo "Warning: 'migrations' directory not found at root. Skipping move."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Move schema.sql if it exists and is related to backend
|
||||||
|
if [ -f "schema.sql" ]; then
|
||||||
|
mv schema.sql backend/prisma/
|
||||||
|
else
|
||||||
|
echo "Warning: 'schema.sql' not found at root. Skipping move."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Backend files moved."
|
||||||
|
|
||||||
|
# --- 3. Move files to frontend/ ---
|
||||||
|
echo "Moving frontend files..."
|
||||||
|
|
||||||
|
mv App.tsx frontend/src/
|
||||||
|
mv CreateTourModal.tsx frontend/src/components/
|
||||||
|
mv ExpenseManager.tsx frontend/src/components/ # Assuming this is a component
|
||||||
|
mv ExploreMap.tsx frontend/src/pages/ # Assuming this is a page
|
||||||
|
mv index.tsx frontend/src/
|
||||||
|
mv ItineraryTimeline.tsx frontend/src/components/ # Assuming this is a component
|
||||||
|
mv LandingPage.tsx frontend/src/pages/
|
||||||
|
mv LoginModal.tsx frontend/src/components/
|
||||||
|
mv SignupPage.tsx frontend/src/pages/
|
||||||
|
mv TourDetailPage.tsx frontend/src/pages/
|
||||||
|
mv useTourStore.ts frontend/src/store/
|
||||||
|
mv vite.config.ts frontend/
|
||||||
|
mv tailwind.config.ts frontend/
|
||||||
|
mv postcss.config.js frontend/
|
||||||
|
mv index.css frontend/src/
|
||||||
|
|
||||||
|
echo "Frontend files moved."
|
||||||
|
|
||||||
|
# --- 4. Move files to docs/ ---
|
||||||
|
echo "Moving documentation files..."
|
||||||
|
|
||||||
|
mv ARCHITECTURE.md docs/
|
||||||
|
mv UITourDesign.md docs/
|
||||||
|
|
||||||
|
echo "Documentation files moved."
|
||||||
|
|
||||||
|
echo "Monorepo refactoring complete. Please verify the new structure."
|
||||||
|
echo "Next, you will need to update import paths and configuration files."
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import type { Config } from 'tailwindcss'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
"./index.html",
|
|
||||||
"./*.{js,ts,jsx,tsx}",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {},
|
|
||||||
},
|
|
||||||
plugins: [],
|
|
||||||
} satisfies Config
|
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Đảm bảo script dừng nếu có lỗi xảy ra
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🚀 Đang quét và cập nhật các đường dẫn import cho cấu trúc Monorepo..."
|
||||||
|
|
||||||
|
# --- 1. CẬP NHẬT BACKEND (NestJS) ---
|
||||||
|
|
||||||
|
# Cập nhật backend/src/main.ts
|
||||||
|
# Các Guard/Middleware nay nằm trong auth/ và common/
|
||||||
|
sed -i -E "s|from\s+['\"](\./)(admin\.guard\|jwt-auth\.guard\|jwt\.strategy)['\"]|from './auth/\2'|g" backend/src/main.ts
|
||||||
|
sed -i -E "s|from\s+['\"](\./)(rbac\.middleware)['\"]|from './common/\2'|g" backend/src/main.ts
|
||||||
|
# Prisma service nay nằm ở ../prisma/ (so với thư mục src/)
|
||||||
|
sed -i -E "s|from\s+['\"](\./prisma/prisma\.service)['\"]|from '../prisma/prisma.service'|g" backend/src/main.ts
|
||||||
|
|
||||||
|
# Cập nhật các file trong backend/src/auth/ (Guards & Strategies)
|
||||||
|
# Nếu chúng import PrismaService từ ./prisma/prisma.service, nay cần lùi 2 cấp: ../../prisma/prisma.service
|
||||||
|
find backend/src/auth -name "*.ts" -exec sed -i -E "s|from\s+['\"](\./prisma/prisma\.service)['\"]|from '../../prisma/prisma.service'|g" {} +
|
||||||
|
|
||||||
|
|
||||||
|
# --- 2. CẬP NHẬT FRONTEND (React + Vite) ---
|
||||||
|
|
||||||
|
# Cập nhật App.tsx (frontend/src/App.tsx)
|
||||||
|
# Trỏ vào thư mục pages/ và store/
|
||||||
|
sed -i -E "s|from\s+['\"](\./)(LandingPage\|SignupPage\|TourDetailPage\|ExploreMap)['\"]|from './pages/\2'|g" frontend/src/App.tsx
|
||||||
|
sed -i -E "s|from\s+['\"](\./)(useTourStore)['\"]|from './store/\2'|g" frontend/src/App.tsx
|
||||||
|
|
||||||
|
# Cập nhật các Trang (frontend/src/pages/*.tsx)
|
||||||
|
# Các trang cần trỏ tới components/ và store/ nằm ở thư mục anh em
|
||||||
|
# Bao gồm: CreateTourModal, LoginModal, ExpenseManager, ItineraryTimeline, AddMemberModal, ConfirmModal, NotificationModal
|
||||||
|
find frontend/src/pages -name "*.tsx" -exec sed -i -E "s|from\s+['\"](\./\|@/components/)(CreateTourModal\|LoginModal\|ExpenseManager\|ItineraryTimeline\|AddMemberModal\|ConfirmModal\|NotificationModal)['\"]|from '../components/\2'|g" {} +
|
||||||
|
find frontend/src/pages -name "*.tsx" -exec sed -i -E "s|from\s+['\"](\./)(useTourStore)['\"]|from '../store/\2'|g" {} +
|
||||||
|
find frontend/src/pages -name "*.tsx" -exec sed -i -E "s|from\s+['\"](\./index\.css)['\"]|from '../index.css'|g" {} +
|
||||||
|
|
||||||
|
# Cập nhật các Component (frontend/src/components/*.tsx)
|
||||||
|
# Các component/modal cần trỏ tới store/
|
||||||
|
find frontend/src/components -name "*.tsx" -exec sed -i -E "s|from\s+['\"](\./)(useTourStore)['\"]|from '../store/\2'|g" {} +
|
||||||
|
|
||||||
|
echo "✅ Hoàn tất cập nhật Import Paths."
|
||||||
|
echo "💡 Lưu ý: Hãy kiểm tra lại các lỗi đỏ trong VS Code. Nếu bạn có sử dụng Path Alias (như @/components), chúng ta sẽ cấu hình nó ở bước tiếp theo trong tsconfig.json."
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
plugins: [react()],
|
|
||||||
server: {
|
|
||||||
port: 3002, // Thay đổi cổng thành 5173 (mặc định của Vite) hoặc cổng khác bạn muốn
|
|
||||||
strictPort: true,
|
|
||||||
host: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user