Sửa lỗi tái cấu trúc thư mục và khai báo import
This commit is contained in:
1 parent
967f6b4f6a
commit
4716b841dc
46 files changed
+542
-6549
No files matched your search
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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 {
|
||||||
File renamed without changes.
@@ -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/**/*"]
|
||||||
}
|
}
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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';
|
||||||
|
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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) => {
|
||||||
File renamed without changes.
@@ -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 };
|
||||||
|
};
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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();
|
||||||
|
|
||||||
File renamed without changes.
@@ -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
+126
-6418
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