fix: sửa lỗi các nút bị che dưới thanh địa chỉ của chrome, hỗ trợ iphone upload ảnh heic
This commit is contained in:
Vendored
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"continue.enableConsole": true
|
"continue.enableConsole": true,
|
||||||
|
"remote.autoForwardPortsFallback": 0
|
||||||
}
|
}
|
||||||
Vendored
+31
-7
@@ -1695,7 +1695,7 @@ let UserController = class UserController {
|
|||||||
this.prisma = prisma;
|
this.prisma = prisma;
|
||||||
}
|
}
|
||||||
async getAllUsers(req, q) {
|
async getAllUsers(req, q) {
|
||||||
const currentUserId = req.user?.sub;
|
const currentUserId = req.user?.id;
|
||||||
const users = await this.prisma.user.findMany({
|
const users = await this.prisma.user.findMany({
|
||||||
where: q
|
where: q
|
||||||
? {
|
? {
|
||||||
@@ -1718,18 +1718,32 @@ let UserController = class UserController {
|
|||||||
orderBy: { capturedAt: 'desc' }
|
orderBy: { capturedAt: 'desc' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async updateUser(id, data) {
|
async updateUser(id, data, req) {
|
||||||
|
const requestingUser = req.user;
|
||||||
|
const targetUser = await this.prisma.user.findUnique({ where: { id } });
|
||||||
|
if (!targetUser) {
|
||||||
|
throw new common_1.NotFoundException('Không tìm thấy người dùng');
|
||||||
|
}
|
||||||
|
if (targetUser.isAdmin && !requestingUser.isAdmin) {
|
||||||
|
throw new common_1.ForbiddenException('Không có quyền thay đổi thông tin hoặc reset password của Quản trị viên');
|
||||||
|
}
|
||||||
|
if (!requestingUser.isAdmin && requestingUser.id !== id) {
|
||||||
|
throw new common_1.ForbiddenException('Bạn không có quyền thực hiện hành động này');
|
||||||
|
}
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
data.passwordHash = await bcrypt.hash(data.password, 10);
|
data.passwordHash = await bcrypt.hash(data.password, 10);
|
||||||
delete data.password;
|
delete data.password;
|
||||||
}
|
}
|
||||||
|
if (!requestingUser.isAdmin && data.isAdmin !== undefined) {
|
||||||
|
delete data.isAdmin;
|
||||||
|
}
|
||||||
return this.prisma.user.update({
|
return this.prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data,
|
data,
|
||||||
select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true }
|
select: { id: true, email: true, name: true, isAdmin: true, isBlocked: true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async deleteUser(id) {
|
async deleteUser(id, req) {
|
||||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
if (!user)
|
if (!user)
|
||||||
throw new common_1.NotFoundException('Không tìm thấy người dùng');
|
throw new common_1.NotFoundException('Không tìm thấy người dùng');
|
||||||
@@ -1757,10 +1771,13 @@ let UserController = class UserController {
|
|||||||
}
|
}
|
||||||
return { message: 'Đã xóa người dùng' };
|
return { message: 'Đã xóa người dùng' };
|
||||||
}
|
}
|
||||||
async toggleBlock(id) {
|
async toggleBlock(id, req) {
|
||||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
if (!user)
|
if (!user)
|
||||||
throw new common_1.NotFoundException('Người dùng không tồn tại');
|
throw new common_1.NotFoundException('Người dùng không tồn tại');
|
||||||
|
if (user.isAdmin) {
|
||||||
|
throw new common_1.BadRequestException('Không thể khóa tài khoản Quản trị viên');
|
||||||
|
}
|
||||||
const updated = await this.prisma.user.update({
|
const updated = await this.prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { isBlocked: !user.isBlocked },
|
data: { isBlocked: !user.isBlocked },
|
||||||
@@ -1771,6 +1788,7 @@ let UserController = class UserController {
|
|||||||
};
|
};
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, common_1.Get)(),
|
(0, common_1.Get)(),
|
||||||
|
(0, common_1.UseGuards)(admin_guard_1.AdminGuard),
|
||||||
__param(0, (0, common_1.Req)()),
|
__param(0, (0, common_1.Req)()),
|
||||||
__param(1, (0, common_1.Query)('q')),
|
__param(1, (0, common_1.Query)('q')),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
@@ -1790,27 +1808,33 @@ __decorate([
|
|||||||
(0, common_1.Patch)(':id'),
|
(0, common_1.Patch)(':id'),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
__param(1, (0, common_1.Body)()),
|
__param(1, (0, common_1.Body)()),
|
||||||
|
__param(2, (0, common_1.Req)()),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
__metadata("design:paramtypes", [String, Object]),
|
__metadata("design:paramtypes", [String, Object, Object]),
|
||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], UserController.prototype, "updateUser", null);
|
], UserController.prototype, "updateUser", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, exports.Roles)(client_1.ParticipantRole.OWNER),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER),
|
||||||
(0, common_1.Delete)(':id'),
|
(0, common_1.Delete)(':id'),
|
||||||
|
(0, common_1.UseGuards)(admin_guard_1.AdminGuard),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
|
__param(1, (0, common_1.Req)()),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
__metadata("design:paramtypes", [String]),
|
__metadata("design:paramtypes", [String, Object]),
|
||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], UserController.prototype, "deleteUser", null);
|
], UserController.prototype, "deleteUser", null);
|
||||||
__decorate([
|
__decorate([
|
||||||
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
(0, common_1.Post)('block/:id'),
|
(0, common_1.Post)('block/:id'),
|
||||||
|
(0, common_1.UseGuards)(admin_guard_1.AdminGuard),
|
||||||
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
|
||||||
|
__param(1, (0, common_1.Req)()),
|
||||||
__metadata("design:type", Function),
|
__metadata("design:type", Function),
|
||||||
__metadata("design:paramtypes", [String]),
|
__metadata("design:paramtypes", [String, Object]),
|
||||||
__metadata("design:returntype", Promise)
|
__metadata("design:returntype", Promise)
|
||||||
], UserController.prototype, "toggleBlock", null);
|
], UserController.prototype, "toggleBlock", null);
|
||||||
UserController = __decorate([
|
UserController = __decorate([
|
||||||
|
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
|
||||||
(0, common_1.Controller)('users'),
|
(0, common_1.Controller)('users'),
|
||||||
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
|
||||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const envPath = path.resolve(process.cwd(), '..', '.env');
|
||||||
|
dotenv.config({ path: envPath });
|
||||||
|
|
||||||
|
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
||||||
|
const adapter = new PrismaPg(pool);
|
||||||
|
const prisma = new PrismaClient({ adapter });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const users = await prisma.user.findMany();
|
||||||
|
console.log('USERS:', users);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().finally(() => pool.end());
|
||||||
+39
-4
@@ -1640,14 +1640,16 @@ class PhotoController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('users')
|
@Controller('users')
|
||||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for User management (Admin/Manager)
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for User management (Admin/Manager)
|
||||||
class UserController {
|
class UserController {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@UseGuards(AdminGuard)
|
||||||
async getAllUsers(@Req() req: any, @Query('q') q?: string) {
|
async getAllUsers(@Req() req: any, @Query('q') q?: string) {
|
||||||
const currentUserId = req.user?.sub;
|
const currentUserId = req.user?.id;
|
||||||
const users = await this.prisma.user.findMany({
|
const users = await this.prisma.user.findMany({
|
||||||
where: q
|
where: q
|
||||||
? {
|
? {
|
||||||
@@ -1677,11 +1679,36 @@ class UserController {
|
|||||||
|
|
||||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for updating user (Admin/Manager)
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for updating user (Admin/Manager)
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
async updateUser(@Param('id', ParseUUIDPipe) id: string, @Body() data: any) {
|
async updateUser(@Param('id', ParseUUIDPipe) id: string, @Body() data: any, @Req() req: any) {
|
||||||
|
const requestingUser = req.user;
|
||||||
|
|
||||||
|
// Fetch target user from DB
|
||||||
|
const targetUser = await this.prisma.user.findUnique({ where: { id } });
|
||||||
|
if (!targetUser) {
|
||||||
|
throw new NotFoundException('Không tìm thấy người dùng');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. If target user is an Admin, only an Admin can update them.
|
||||||
|
// (A manager/normal user cannot reset/change password of an Admin)
|
||||||
|
if (targetUser.isAdmin && !requestingUser.isAdmin) {
|
||||||
|
throw new ForbiddenException('Không có quyền thay đổi thông tin hoặc reset password của Quản trị viên');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. A non-admin can only update their own profile.
|
||||||
|
if (!requestingUser.isAdmin && requestingUser.id !== id) {
|
||||||
|
throw new ForbiddenException('Bạn không có quyền thực hiện hành động này');
|
||||||
|
}
|
||||||
|
|
||||||
if (data.password) {
|
if (data.password) {
|
||||||
data.passwordHash = await bcrypt.hash(data.password, 10);
|
data.passwordHash = await bcrypt.hash(data.password, 10);
|
||||||
delete data.password;
|
delete data.password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safety: prevent non-admins from promoting anyone to admin
|
||||||
|
if (!requestingUser.isAdmin && data.isAdmin !== undefined) {
|
||||||
|
delete data.isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
return this.prisma.user.update({
|
return this.prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data,
|
data,
|
||||||
@@ -1691,7 +1718,8 @@ class UserController {
|
|||||||
|
|
||||||
@Roles(ParticipantRole.OWNER) // Only owner can delete user
|
@Roles(ParticipantRole.OWNER) // Only owner can delete user
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async deleteUser(@Param('id', ParseUUIDPipe) id: string) {
|
@UseGuards(AdminGuard)
|
||||||
|
async deleteUser(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
|
||||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
if (!user) throw new NotFoundException('Không tìm thấy người dùng');
|
if (!user) throw new NotFoundException('Không tìm thấy người dùng');
|
||||||
if (user.isAdmin) {
|
if (user.isAdmin) {
|
||||||
@@ -1729,9 +1757,16 @@ class UserController {
|
|||||||
|
|
||||||
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for blocking user (Admin/Manager)
|
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for blocking user (Admin/Manager)
|
||||||
@Post('block/:id')
|
@Post('block/:id')
|
||||||
async toggleBlock(@Param('id', ParseUUIDPipe) id: string) {
|
@UseGuards(AdminGuard)
|
||||||
|
async toggleBlock(@Param('id', ParseUUIDPipe) id: string, @Req() req: any) {
|
||||||
const user = await this.prisma.user.findUnique({ where: { id } });
|
const user = await this.prisma.user.findUnique({ where: { id } });
|
||||||
if (!user) throw new NotFoundException('Người dùng không tồn tại');
|
if (!user) throw new NotFoundException('Người dùng không tồn tại');
|
||||||
|
|
||||||
|
// Safety check: Cannot block an Admin
|
||||||
|
if (user.isAdmin) {
|
||||||
|
throw new BadRequestException('Không thể khóa tài khoản Quản trị viên');
|
||||||
|
}
|
||||||
|
|
||||||
const updated = await this.prisma.user.update({
|
const updated = await this.prisma.user.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: { isBlocked: !user.isBlocked },
|
data: { isBlocked: !user.isBlocked },
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 704 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 187 KiB |
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+7
-7
File diff suppressed because one or more lines are too long
-2
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -2,11 +2,11 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<title>Travel Planner</title>
|
<title>Travel Planner</title>
|
||||||
<script type="module" crossorigin src="/assets/index-D1uuLlpI.js"></script>
|
<script type="module" crossorigin src="/assets/index-Dn1e9vte.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-f6lAQjsT.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-C4TR57aU.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<title>Travel Planner</title>
|
<title>Travel Planner</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -251,9 +251,9 @@ export const ExploreMap = ({ onBack, onLogout, user, onViewTour, onOpenMyPhotos,
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full relative">
|
<div className="h-dvh w-full relative overflow-hidden">
|
||||||
{/* Top Bar Container - Chứa tất cả các nút điều hướng phía trên */}
|
{/* Top Bar Container - Chứa tất cả các nút điều hướng phía trên */}
|
||||||
<div className="absolute top-4 left-4 right-4 z-[1002] flex items-center justify-between pointer-events-none">
|
<div className="absolute top-[calc(1rem+env(safe-area-inset-top,0px))] left-4 right-4 z-[1002] flex items-center justify-between pointer-events-none">
|
||||||
{/* Nhóm bên trái: Quay lại và Thông tin vị trí */}
|
{/* Nhóm bên trái: Quay lại và Thông tin vị trí */}
|
||||||
<div className="flex items-center gap-3 pointer-events-auto">
|
<div className="flex items-center gap-3 pointer-events-auto">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen w-full overflow-hidden font-sans bg-gray-900 relative">
|
<div className="h-dvh w-full overflow-hidden font-sans bg-gray-900 relative">
|
||||||
{/* Background Image - Hiển thị trên mọi thiết bị với hiệu ứng Crossfade & Panning */}
|
{/* Background Image - Hiển thị trên mọi thiết bị với hiệu ứng Crossfade & Panning */}
|
||||||
<div className="absolute inset-0 z-0 bg-gray-950 overflow-hidden">
|
<div className="absolute inset-0 z-0 bg-gray-950 overflow-hidden">
|
||||||
{/* Slot 1 */}
|
{/* Slot 1 */}
|
||||||
@@ -242,7 +242,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
|||||||
`}</style>
|
`}</style>
|
||||||
|
|
||||||
{/* Top Bar - Thanh điều hướng trên cùng */}
|
{/* Top Bar - Thanh điều hướng trên cùng */}
|
||||||
<div className="absolute top-0 left-0 right-0 z-20 p-4 flex justify-between items-center">
|
<div className="absolute top-0 left-0 right-0 z-20 p-4 pt-[calc(1rem+env(safe-area-inset-top,0px))] flex justify-between items-center">
|
||||||
<div className="flex items-center gap-2 text-white drop-shadow-lg">
|
<div className="flex items-center gap-2 text-white drop-shadow-lg">
|
||||||
<Compass className="w-8 h-8" />
|
<Compass className="w-8 h-8" />
|
||||||
<span className="text-xl font-black tracking-tighter uppercase hidden sm:block">Travel Planner</span>
|
<span className="text-xl font-black tracking-tighter uppercase hidden sm:block">Travel Planner</span>
|
||||||
@@ -274,7 +274,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
|||||||
|
|
||||||
{/* Community Gallery Previews */}
|
{/* Community Gallery Previews */}
|
||||||
{publicPhotos.length > 0 && (
|
{publicPhotos.length > 0 && (
|
||||||
<div className="absolute bottom-28 left-0 right-0 z-20 px-4 flex flex-col items-center gap-2">
|
<div className="absolute bottom-[calc(7rem+env(safe-area-inset-bottom,0px))] left-0 right-0 z-20 px-4 flex flex-col items-center gap-2">
|
||||||
<span className="text-xs font-bold uppercase tracking-wider text-white/70 drop-shadow-md">
|
<span className="text-xs font-bold uppercase tracking-wider text-white/70 drop-shadow-md">
|
||||||
Khoảnh khắc từ cộng đồng ({publicPhotos.length})
|
Khoảnh khắc từ cộng đồng ({publicPhotos.length})
|
||||||
</span>
|
</span>
|
||||||
@@ -305,7 +305,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onContinue, onGoToSign
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Bottom Bar - Nút hành động chính */}
|
{/* Bottom Bar - Nút hành động chính */}
|
||||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 z-20 w-full px-4">
|
<div className="absolute bottom-[calc(2rem+env(safe-area-inset-bottom,0px))] left-1/2 -translate-x-1/2 z-20 w-full px-4">
|
||||||
<button
|
<button
|
||||||
onClick={onGoToMap}
|
onClick={onGoToMap}
|
||||||
className="w-full max-w-md mx-auto flex items-center justify-center gap-3 bg-white/20 backdrop-blur-lg text-white font-bold py-4 px-8 rounded-2xl transition-all shadow-2xl border border-white/30 hover:bg-white/30 active:scale-95"
|
className="w-full max-w-md mx-auto flex items-center justify-center gap-3 bg-white/20 backdrop-blur-lg text-white font-bold py-4 px-8 rounded-2xl transition-all shadow-2xl border border-white/30 hover:bg-white/30 active:scale-95"
|
||||||
|
|||||||
@@ -2221,7 +2221,7 @@ export const TourDetailPage = ({
|
|||||||
{/* Nút X để quay lại */}
|
{/* Nút X để quay lại */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsMapFullscreen(false)}
|
onClick={() => setIsMapFullscreen(false)}
|
||||||
className="absolute top-4 left-4 z-[1002] w-11 h-11 bg-white/90 backdrop-blur-md rounded-full shadow-2xl flex items-center justify-center border border-white/20 hover:bg-white transition-all active:scale-95 group"
|
className="absolute top-[calc(1rem+env(safe-area-inset-top,0px))] left-4 z-[1002] w-11 h-11 bg-white/90 backdrop-blur-md rounded-full shadow-2xl flex items-center justify-center border border-white/20 hover:bg-white transition-all active:scale-95 group"
|
||||||
title="Đóng bản đồ"
|
title="Đóng bản đồ"
|
||||||
>
|
>
|
||||||
<X className="w-6 h-6 text-gray-800 group-hover:rotate-90 transition-transform duration-300" />
|
<X className="w-6 h-6 text-gray-800 group-hover:rotate-90 transition-transform duration-300" />
|
||||||
@@ -2229,7 +2229,7 @@ export const TourDetailPage = ({
|
|||||||
|
|
||||||
{/* Transparent Top Bar Label - Glassmorphism style */}
|
{/* Transparent Top Bar Label - Glassmorphism style */}
|
||||||
{selectedRouteInfo && drivingRoute.length > 0 && (
|
{selectedRouteInfo && drivingRoute.length > 0 && (
|
||||||
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-[1001] bg-white/20 backdrop-blur-lg px-6 py-2.5 rounded-full border border-white/30 flex items-center gap-4 animate-in fade-in slide-in-from-top-2 duration-500 shadow-xl">
|
<div className="absolute top-[calc(1rem+env(safe-area-inset-top,0px))] left-1/2 -translate-x-1/2 z-[1001] bg-white/20 backdrop-blur-lg px-6 py-2.5 rounded-full border border-white/30 flex items-center gap-4 animate-in fade-in slide-in-from-top-2 duration-500 shadow-xl">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Navigation className="w-4 h-4 text-blue-600 rotate-45 fill-blue-600" />
|
<Navigation className="w-4 h-4 text-blue-600 rotate-45 fill-blue-600" />
|
||||||
<span className="text-[11px] font-black text-gray-700 uppercase tracking-widest">{selectedRouteInfo.label}</span>
|
<span className="text-[11px] font-black text-gray-700 uppercase tracking-widest">{selectedRouteInfo.label}</span>
|
||||||
@@ -2286,7 +2286,7 @@ export const TourDetailPage = ({
|
|||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
{/* Overlay điều khiển trên bản đồ toàn màn hình */}
|
{/* Overlay điều khiển trên bản đồ toàn màn hình */}
|
||||||
<div className="absolute top-4 right-4 z-[1001] flex flex-col gap-2">
|
<div className="absolute top-[calc(1rem+env(safe-area-inset-top,0px))] right-4 z-[1001] flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsMapControlsOpen(!isMapControlsOpen)}
|
onClick={() => setIsMapControlsOpen(!isMapControlsOpen)}
|
||||||
className="w-11 h-11 bg-white/90 backdrop-blur-md rounded-2xl shadow-xl border border-white text-blue-600 flex items-center justify-center transition-all active:scale-95"
|
className="w-11 h-11 bg-white/90 backdrop-blur-md rounded-2xl shadow-xl border border-white text-blue-600 flex items-center justify-center transition-all active:scale-95"
|
||||||
|
|||||||
Reference in New Issue
Block a user