Merge branch 'master' of github.com:Pregnant-Guild/FE_User_history_web
This commit is contained in:
@@ -64,4 +64,7 @@ export const API = {
|
|||||||
UPDATE_STATUS: (id: number | string) => `${API_URL_ROOT}/submissions/${id}/status`,
|
UPDATE_STATUS: (id: number | string) => `${API_URL_ROOT}/submissions/${id}/status`,
|
||||||
DELETE: (id: number | string) => `${API_URL_ROOT}/submissions/${id}`,
|
DELETE: (id: number | string) => `${API_URL_ROOT}/submissions/${id}`,
|
||||||
},
|
},
|
||||||
|
Chatbot:{
|
||||||
|
CHAT: `${API_URL_ROOT}/chatbot/chat`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,300 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default function LandingPage() {
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
title: "Bản đồ dòng thời gian",
|
||||||
|
desc: "Giao diện bản đồ tự động thay đổi biên giới, địa danh và sự kiện tương ứng với mốc thời gian được lựa chọn.",
|
||||||
|
icon: "🗺️",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tương tác thực tế",
|
||||||
|
desc: "Hiển thị chi tiết bối cảnh, nhân vật và số liệu khi người dùng thao tác vào các điểm neo sự kiện trên bản đồ.",
|
||||||
|
icon: "📍",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Trợ lý ảo & Công cụ học",
|
||||||
|
desc: "Tích hợp AI giải đáp thắc mắc lịch sử, kết hợp hệ thống giao bài tập và làm Quiz trực tuyến cho học đường.",
|
||||||
|
icon: "🤖",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const team = [
|
||||||
|
{
|
||||||
|
name: "Trần Anh Đức",
|
||||||
|
role: "Project Manager",
|
||||||
|
desc: "Đẹp trai cao m8",
|
||||||
|
avatar: "/images/teamdev/tad.jpeg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Đỗ Duy Khánh",
|
||||||
|
role: "Backend Developer",
|
||||||
|
desc: "Kì nhân dị sỹ",
|
||||||
|
avatar: "/images/teamdev/ddk2.jpeg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ngô Cung Đức Anh",
|
||||||
|
role: "Frontend Developer",
|
||||||
|
desc: "Cũng đẹp trai nhưng cao m7 thôi",
|
||||||
|
avatar: "/images/teamdev/ncda.jpeg",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
// Sử dụng tông màu Vàng cổ (Parchment) và Xanh rêu (Dark Slate Green)
|
||||||
|
<div className="relative min-h-screen w-full text-[#2D3A3A] font-sans selection:bg-[#A88B4C] selection:text-white overflow-x-hidden">
|
||||||
|
{/* --- BACKGROUND IMAGE --- */}
|
||||||
|
<div className="fixed inset-0 -z-20 pointer-events-none">
|
||||||
|
<Image
|
||||||
|
src="/images/map.jpeg"
|
||||||
|
alt="World Map Background"
|
||||||
|
fill
|
||||||
|
className="object-cover object-center opacity-40"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Lớp overlay mờ để làm dịu background */}
|
||||||
|
<div className="fixed inset-0 bg-gradient-to-b from-[#FDFBF7]/80 via-[#FDFBF7]/70 to-[#FDFBF7]/90 -z-10 pointer-events-none"></div>
|
||||||
|
|
||||||
|
{/* --- HEADER NAVBAR --- */}
|
||||||
|
<header className="fixed top-0 w-full px-6 py-4 flex justify-between items-center backdrop-blur-sm bg-[#FDFBF7]/70 z-50 border-b border-[#A88B4C]/20">
|
||||||
|
<div className="text-xl font-bold tracking-widest text-[#2D3A3A] uppercase">
|
||||||
|
<span className="text-[#A88B4C]">Geo</span>History
|
||||||
|
</div>
|
||||||
|
<nav className="flex gap-4 items-center">
|
||||||
|
<Link
|
||||||
|
href="/auth/signin"
|
||||||
|
className="text-sm font-semibold text-[#2D3A3A] hover:text-[#A88B4C] transition-colors"
|
||||||
|
>
|
||||||
|
Đăng nhập
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/user"
|
||||||
|
className="text-sm font-semibold px-4 py-2 bg-[#2D3A3A] text-[#FDFBF7] rounded-lg hover:bg-[#1a2323] transition-colors"
|
||||||
|
>
|
||||||
|
Vào Hệ thống
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="max-w-6xl mx-auto px-6 pt-32 pb-24 flex flex-col gap-32 w-full relative">
|
||||||
|
{/* --- PHẦN 1: GIỚI THIỆU TỔNG QUAN --- */}
|
||||||
|
<section className="min-h-[70vh] flex flex-col justify-center relative">
|
||||||
|
<h1 className="text-5xl md:text-7xl font-black leading-tight tracking-tight mb-6">
|
||||||
|
Bách khoa toàn thư <br />
|
||||||
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-[#A88B4C] to-[#806835]">
|
||||||
|
Bản đồ số Lịch sử
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<div className="max-w-3xl space-y-6 text-lg md:text-xl text-[#4A5555] leading-relaxed">
|
||||||
|
<p>
|
||||||
|
Hệ thống thông tin địa lý (GIS) tiên phong trong việc trực quan
|
||||||
|
hóa dữ liệu lịch sử. Nền tảng của chúng tôi cho phép hiển thị động
|
||||||
|
các thông tin như biên giới quốc gia, diễn biến trận chiến và sự
|
||||||
|
kiện theo đúng tiến trình thời gian.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Đây là không gian tập trung tri thức được tinh lọc, nơi các chuyên
|
||||||
|
gia, nhà sử học và giáo viên đóng góp dữ liệu tọa độ, vector, được
|
||||||
|
hệ thống kiểm duyệt chặt chẽ trước khi xuất bản.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10 flex gap-4">
|
||||||
|
<Link
|
||||||
|
href="#mission"
|
||||||
|
className="px-8 py-4 bg-[#A88B4C] text-white font-bold rounded-xl shadow-lg shadow-[#A88B4C]/20 hover:bg-[#8e743c]"
|
||||||
|
>
|
||||||
|
Khám phá sứ mệnh
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* --- PHẦN 2: SỨ MỆNH & CHỨC NĂNG --- */}
|
||||||
|
<section id="mission" className="scroll-mt-24 relative">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-center">
|
||||||
|
{/* Sứ mệnh */}
|
||||||
|
<div>
|
||||||
|
<div className="inline-block px-3 py-1 bg-[#A88B4C]/10 text-[#A88B4C] font-bold text-sm tracking-widest uppercase rounded-full mb-4 border border-[#A88B4C]/20">
|
||||||
|
Sứ mệnh của chúng tôi
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-6">
|
||||||
|
Định hình lại cách học Lịch sử
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-6 text-[#4A5555]">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-[#2D3A3A] mb-2 flex items-center gap-2">
|
||||||
|
<span className="w-8 h-8 rounded-full bg-[#A88B4C]/20 flex items-center justify-center text-[#A88B4C]">
|
||||||
|
1
|
||||||
|
</span>
|
||||||
|
Giải quyết rào cản giáo dục
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Khắc phục sự nhàm chán và khó tiếp cận của phương pháp học
|
||||||
|
lịch sử truyền thống bằng cách biến dữ liệu chữ viết thành
|
||||||
|
hình ảnh không gian, thời gian trực quan.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-px bg-[#A88B4C]/20"></div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-bold text-[#2D3A3A] mb-2 flex items-center gap-2">
|
||||||
|
<span className="w-8 h-8 rounded-full bg-[#A88B4C]/20 flex items-center justify-center text-[#A88B4C]">
|
||||||
|
2
|
||||||
|
</span>
|
||||||
|
Tập trung hóa tri thức
|
||||||
|
</h3>
|
||||||
|
<p>
|
||||||
|
Xây dựng một kho dữ liệu lịch sử thống nhất, chuẩn xác, phục
|
||||||
|
vụ đa dạng đối tượng từ chính phủ, chuyên gia nghiên cứu đến
|
||||||
|
học sinh, sinh viên và cộng đồng.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Chức năng */}
|
||||||
|
<div>
|
||||||
|
<div className="inline-block px-3 py-1 bg-[#2D3A3A]/10 text-[#2D3A3A] font-bold text-sm tracking-widest uppercase rounded-full mb-4 border border-[#2D3A3A]/20">
|
||||||
|
Tính năng cốt lõi
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-6">
|
||||||
|
Công nghệ hội tụ
|
||||||
|
</h2>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{features.map((feat, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="flex gap-4 items-start p-5 hover:bg-[#A88B4C]/10 rounded-2xl group"
|
||||||
|
>
|
||||||
|
<div className="text-3xl bg-transparent w-14 h-14 rounded-xl flex items-center justify-center shrink-0">
|
||||||
|
{feat.icon}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-[#2D3A3A] text-lg mb-1">
|
||||||
|
{feat.title}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-[#4A5555] leading-relaxed">
|
||||||
|
{feat.desc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* --- PHẦN 3: ĐỘI NGŨ PHÁT TRIỂN --- */}
|
||||||
|
<section className="relative">
|
||||||
|
<div className="text-center max-w-2xl mx-auto mb-12">
|
||||||
|
<div className="inline-block px-3 py-1 bg-[#A88B4C]/10 text-[#A88B4C] font-bold text-sm tracking-widest uppercase rounded-full mb-4 border border-[#A88B4C]/20">
|
||||||
|
Về chúng tôi
|
||||||
|
</div>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-4">
|
||||||
|
Đội ngũ phát triển
|
||||||
|
</h2>
|
||||||
|
<p className="text-[#4A5555]">
|
||||||
|
Những con người đam mê lịch sử và công nghệ chung tay xây dựng cỗ
|
||||||
|
máy thời gian kỹ thuật số.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex mx-auto justify-center gap-12 flex-wrap">
|
||||||
|
{team.map((member, i) => (
|
||||||
|
<div key={i} className="p-6 text-center min-w-[264px]">
|
||||||
|
<div className="w-24 aspect-square mx-auto bg-gradient-to-tr from-[#A88B4C]/20 to-[#2D3A3A]/20 rounded-full mb-4 flex items-center justify-center overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={member.avatar}
|
||||||
|
alt={member.name}
|
||||||
|
width={96}
|
||||||
|
height={96}
|
||||||
|
className="w-full h-full object-cover object-center rounded-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-bold text-lg text-[#2D3A3A]">
|
||||||
|
{member.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs font-bold text-[#A88B4C] uppercase tracking-wider my-2">
|
||||||
|
{member.role}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-[#4A5555]">{member.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{/* --- PHẦN 4: GÓP Ý & LIÊN HỆ --- */}
|
||||||
|
<section className="relative mt-8 mb-16 w-full">
|
||||||
|
<div className="flex flex-col lg:flex-row justify-between gap-10 border rounded-2xl p-8 bg-[#FDFBF7]/80 backdrop-blur-sm border-[#A88B4C]/20">
|
||||||
|
{/* Box Text */}
|
||||||
|
<div className="lg:w-1/4 text-center lg:text-left flex flex-col justify-center">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-3">
|
||||||
|
Góp ý cho chúng tôi!
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 leading-relaxed">
|
||||||
|
Đăng ký nhận tin tức mới nhất hoặc để lại ý kiến đóng góp giúp hệ
|
||||||
|
thống hoàn thiện hơn.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Box Form Nhập Liệu (Dòng trên - Dòng dưới) */}
|
||||||
|
<div className="flex-1 w-full max-w-3xl flex flex-col gap-4">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder="Email của bạn..."
|
||||||
|
className="w-full px-5 py-3.5 text-gray-700 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:bg-white focus:border-[#FFDE00] focus:ring-4 focus:ring-[#FFDE00]/20 transition-all text-sm placeholder:text-gray-400"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="Nội dung góp ý của bạn..."
|
||||||
|
rows={3}
|
||||||
|
className="w-full px-5 py-3.5 text-gray-700 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:bg-white focus:border-[#FFDE00] focus:ring-4 focus:ring-[#FFDE00]/20 transition-all text-sm placeholder:text-gray-400 resize-none"
|
||||||
|
></textarea>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button className="bg-[#FFDE00] hover:bg-[#F0D100] text-black font-bold uppercase tracking-wide px-8 py-3.5 rounded-xl transition-colors text-sm shadow-sm">
|
||||||
|
Gửi Góp Ý
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Box Socials */}
|
||||||
|
<div className="lg:w-auto flex flex-col items-center lg:items-start pl-0 lg:pl-8 border-t lg:border-t-0 lg:border-l border-gray-100 pt-8 lg:pt-0 justify-center">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 mb-5">Follow us</h3>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<a
|
||||||
|
href="https://www.youtube.com/@BlackCatStudio-mw2sq"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="w-12 h-12 rounded-full bg-[#FF0000] flex items-center justify-center text-white hover:opacity-90 hover:-translate-y-1 transition-all shadow-lg group"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6 fill-current" viewBox="0 0 24 24">
|
||||||
|
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* FOOTER */}
|
||||||
|
<footer className="border-t border-[#A88B4C]/20 bg-[#2D3A3A] text-white py-12 text-center w-full mt-auto rounded-2xl">
|
||||||
|
<div className="max-w-6xl mx-auto px-6 flex flex-col items-center">
|
||||||
|
<div className="text-2xl font-bold tracking-widest uppercase mb-4">
|
||||||
|
<span className="text-[#A88B4C]">Geo</span>History
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-400 text-sm mb-4 max-w-md">
|
||||||
|
Bách khoa toàn thư bản đồ số lịch sử. Kết nối quá khứ, thấu hiểu
|
||||||
|
hiện tại, kiến tạo tương lai.
|
||||||
|
</p>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
© {new Date().getFullYear()} GeoHistory Project. All rights
|
||||||
|
reserved.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { apiGetCurrentUser } from "@/service/auth";
|
|||||||
import { setUserData } from "@/store/features/userSlice";
|
import { setUserData } from "@/store/features/userSlice";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
import ChatbotWidget from "@/components/ui/chat/ChatbotWidget";
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
@@ -51,6 +52,7 @@ export default function AdminLayout({
|
|||||||
{/* Page Content */}
|
{/* Page Content */}
|
||||||
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">{children}</div>
|
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ChatbotWidget />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export default function ProjectsPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isExportingProjectId === String(project.id)}
|
disabled={isExportingProjectId === String(project.id)}
|
||||||
onClick={() => handleExportHeadSnapshot(project)}
|
onClick={() => handleExportHeadSnapshot(project)}
|
||||||
title="Export head commit snapshot_json"
|
// title="Export head commit snapshot_json"
|
||||||
>
|
>
|
||||||
ExportJSON
|
ExportJSON
|
||||||
</Button>
|
</Button>
|
||||||
@@ -484,7 +484,7 @@ export default function ProjectsPage() {
|
|||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="bg-gray-900 hover:bg-gray-800 text-white"
|
className="bg-gray-900 hover:bg-gray-800 text-white"
|
||||||
onClick={handleCreateProjectWithJson}
|
onClick={handleCreateProjectWithJson}
|
||||||
title="Tạo dự án và tạo commit đầu tiên từ JSON snapshot"
|
// title="Tạo dự án và tạo commit đầu tiên từ JSON snapshot"
|
||||||
>
|
>
|
||||||
Tạo với JSON
|
Tạo với JSON
|
||||||
</Button>
|
</Button>
|
||||||
@@ -494,4 +494,4 @@ export default function ProjectsPage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const faqData = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
question: "1. Phần mềm này tương thích với những hệ điều hành nào?",
|
||||||
|
answer: "Hệ thống tương thích hoàn toàn với Windows 10/11, macOS 12 trở lên. Đối với môi trường máy chủ, chúng tôi hỗ trợ các bản phân phối Linux phổ biến như Ubuntu và Debian."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
question: "2. Sự khác biệt giữa phiên bản Miễn phí và Trả phí là gì?",
|
||||||
|
answer: "Phiên bản trả phí cung cấp băng thông không giới hạn, hỗ trợ kỹ thuật ưu tiên 24/7, và quyền truy cập sớm vào các tính năng nâng cao. Bản miễn phí sẽ giới hạn một số tính năng xuất dữ liệu."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
question: "3. Hệ thống hỗ trợ những phương thức kết nối nào?",
|
||||||
|
answer: "Chúng tôi hỗ trợ kết nối qua REST API, WebSocket cho dữ liệu thời gian thực và cung cấp sẵn SDK cho các ngôn ngữ phổ biến như TypeScript, Go."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
question: "4. Làm thế nào để tôi có thể tích hợp vào dự án Next.js hiện tại?",
|
||||||
|
answer: "Bạn chỉ cần cài đặt package qua npm/yarn, thêm API Key vào file .env và gọi component Provider ở file layout.tsx gốc. Tài liệu chi tiết có sẵn trong mục Developer Docs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
question: "5. Dữ liệu của tôi được bảo mật như thế nào?",
|
||||||
|
answer: "Toàn bộ dữ liệu được mã hóa đầu cuối (End-to-End Encryption). Chúng tôi tuân thủ nghiêm ngặt các tiêu chuẩn bảo mật quốc tế và thường xuyên rà soát hệ thống để phòng chống các lỗ hổng bảo mật."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
// Lưu index của câu hỏi đang được mở. Mặc định mở câu đầu tiên (index 0).
|
||||||
|
const [openIndex, setOpenIndex] = useState<number | null>(0);
|
||||||
|
|
||||||
|
const toggleFAQ = (index: number) => {
|
||||||
|
// Nếu click lại vào câu đang mở thì đóng nó, ngược lại thì mở câu mới
|
||||||
|
setOpenIndex(openIndex === index ? null : index);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-white text-slate-900 py-16 px-4 sm:px-8">
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-4xl font-bold mb-10">FAQs</h1>
|
||||||
|
|
||||||
|
<div className="border-t border-slate-200">
|
||||||
|
{faqData.map((faq, index) => {
|
||||||
|
const isOpen = openIndex === index;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={faq.id} className="border-b border-slate-200">
|
||||||
|
<button
|
||||||
|
onClick={() => toggleFAQ(index)}
|
||||||
|
className="w-full py-6 flex justify-between items-center text-left focus:outline-none group"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-bold group-hover:text-indigo-600 transition-colors">
|
||||||
|
{faq.question}
|
||||||
|
</span>
|
||||||
|
<span className="text-3xl font-light ml-4 text-indigo-600 shrink-0 leading-none">
|
||||||
|
{isOpen ? '−' : '+'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Phần nội dung có hiệu ứng trượt */}
|
||||||
|
<div
|
||||||
|
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||||
|
isOpen ? 'max-h-96 opacity-100 pb-6' : 'max-h-0 opacity-0'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="text-slate-600 text-base leading-relaxed pr-8">
|
||||||
|
{faq.answer}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { fullDataUser } from "@/interface/admin";
|
|||||||
import { UserMetaCardProps } from "@/interface/user";
|
import { UserMetaCardProps } from "@/interface/user";
|
||||||
import { apiGetCurrentUser, apiLogout } from "@/service/auth";
|
import { apiGetCurrentUser, apiLogout } from "@/service/auth";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ListIcon } from "@/icons";
|
import { ListIcon, ShootingStarIcon } from "@/icons";
|
||||||
|
|
||||||
export default function UserDropdown() {
|
export default function UserDropdown() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -152,6 +152,19 @@ export default function UserDropdown() {
|
|||||||
</span>
|
</span>
|
||||||
Nhà Sử Học
|
Nhà Sử Học
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<DropdownItem
|
||||||
|
onItemClick={closeDropdown}
|
||||||
|
tag="a"
|
||||||
|
href="/user/role-upgrade"
|
||||||
|
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg group text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<span className="menu-item-icon">
|
||||||
|
<ShootingStarIcon />
|
||||||
|
</span>
|
||||||
|
Về chúng tôi
|
||||||
|
</DropdownItem>
|
||||||
</li>
|
</li>
|
||||||
{/* <li>
|
{/* <li>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import { ChatbotPayload } from "@/interface/chatbot";
|
||||||
|
import { apiChatbot } from "@/service/chatbotService";
|
||||||
|
|
||||||
|
type Message = {
|
||||||
|
id: string;
|
||||||
|
sender: "user" | "bot";
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ChatbotWidget({
|
||||||
|
projectId = "",
|
||||||
|
}: {
|
||||||
|
projectId?: string;
|
||||||
|
}) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [messages, setMessages] = useState<Message[]>([
|
||||||
|
{
|
||||||
|
id: "init",
|
||||||
|
sender: "bot",
|
||||||
|
text: "Xin chào! Tôi là trợ lý lịch sử thân thiện. Tôi có thể giúp gì cho bạn?",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const [input, setInput] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
}, [messages, isOpen]);
|
||||||
|
|
||||||
|
const handleSend = async () => {
|
||||||
|
if (!input.trim()) return;
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
sender: "user",
|
||||||
|
text: input.trim(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, userMessage]);
|
||||||
|
setInput("");
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: ChatbotPayload = {
|
||||||
|
project_id: projectId,
|
||||||
|
question: userMessage.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await apiChatbot(payload);
|
||||||
|
|
||||||
|
const botMessage: Message = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
sender: "bot",
|
||||||
|
text: res?.status
|
||||||
|
? res?.data
|
||||||
|
: "Xin lỗi, tôi không thể trả lời lúc này.",
|
||||||
|
};
|
||||||
|
|
||||||
|
setMessages((prev) => [...prev, botMessage]);
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMessage: Message = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
sender: "bot",
|
||||||
|
text:
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
"Có lỗi xảy ra khi kết nối. Vui lòng thử lại sau.",
|
||||||
|
};
|
||||||
|
setMessages((prev) => [...prev, errorMessage]);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-8 right-8 z-50">
|
||||||
|
{!isOpen && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="w-14 h-14 bg-brand-500 hover:bg-brand-600 text-white rounded-full flex items-center justify-center shadow-[0_4px_14px_rgba(0,0,0,0.25)] transition-transform hover:scale-105"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Khung Chat */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="w-[360px] h-[520px] bg-white dark:bg-gray-900 rounded-2xl shadow-2xl flex flex-col border border-gray-200 dark:border-gray-800 overflow-hidden animate-in slide-in-from-bottom-4 duration-300">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-4 py-3 bg-brand-500 text-white flex items-center justify-between shadow-sm z-10">
|
||||||
|
<div className="font-semibold flex items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Trợ lý lịch sử.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="text-white hover:text-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={2}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nội dung Chat */}
|
||||||
|
<div className="flex-1 p-4 overflow-y-auto flex flex-col gap-3 bg-gray-50 dark:bg-[#0d1117] text-sm">
|
||||||
|
{messages.map((msg) => (
|
||||||
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
className={`max-w-[85%] rounded-2xl px-4 py-2 shadow-sm ${
|
||||||
|
msg.sender === "user"
|
||||||
|
? "bg-brand-500 text-white self-end rounded-br-sm"
|
||||||
|
: "bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 border border-gray-100 dark:border-gray-700 self-start rounded-bl-sm"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 self-start rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm flex items-center gap-1.5 max-w-[80%]">
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||||
|
style={{ animationDelay: "0.2s" }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||||
|
style={{ animationDelay: "0.4s" }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Khu vực Nhập Input */}
|
||||||
|
<div className="p-3 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Nhập câu hỏi..."
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => setInput(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="flex-1 bg-gray-100 dark:bg-gray-800 border-transparent focus:border-brand-500 focus:bg-white dark:focus:bg-gray-900 focus:ring-1 focus:ring-brand-500/20 rounded-full px-4 py-2.5 text-sm outline-none transition-all"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!input.trim() || isLoading}
|
||||||
|
className={`p-2.5 rounded-full transition-colors flex shrink-0 items-center justify-center ${
|
||||||
|
!input.trim() || isLoading
|
||||||
|
? "text-gray-400 bg-gray-100 dark:bg-gray-800 cursor-not-allowed"
|
||||||
|
: "bg-brand-500 text-white hover:bg-brand-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export interface ChatbotPayload {
|
||||||
|
project_id?: string;
|
||||||
|
question: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatbotResponse {
|
||||||
|
status: boolean;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
+38
-26
@@ -15,6 +15,7 @@ import {
|
|||||||
PageIcon,
|
PageIcon,
|
||||||
PieChartIcon,
|
PieChartIcon,
|
||||||
PlugInIcon,
|
PlugInIcon,
|
||||||
|
ShootingStarIcon,
|
||||||
TableIcon,
|
TableIcon,
|
||||||
UserCircleIcon,
|
UserCircleIcon,
|
||||||
} from "../icons/index";
|
} from "../icons/index";
|
||||||
@@ -53,36 +54,47 @@ const ALL_NAV_ITEMS: NavItem[] = [
|
|||||||
name: "Tài Khoản",
|
name: "Tài Khoản",
|
||||||
path: "/user/account",
|
path: "/user/account",
|
||||||
},
|
},
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const OTHERS_ITEMS: NavItem[] = [
|
const OTHERS_ITEMS: NavItem[] = [
|
||||||
|
// {
|
||||||
|
// icon: <PieChartIcon />,
|
||||||
|
// name: "Charts",
|
||||||
|
// subItems: [
|
||||||
|
// { name: "Line Chart", path: "/line-chart", pro: false },
|
||||||
|
// { name: "Bar Chart", path: "/bar-chart", pro: false },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// icon: <BoxCubeIcon />,
|
||||||
|
// name: "UI Elements",
|
||||||
|
// subItems: [
|
||||||
|
// { name: "Alerts", path: "/alerts", pro: false },
|
||||||
|
// { name: "Avatar", path: "/avatars", pro: false },
|
||||||
|
// { name: "Badge", path: "/badge", pro: false },
|
||||||
|
// { name: "Buttons", path: "/buttons", pro: false },
|
||||||
|
// { name: "Images", path: "/images", pro: false },
|
||||||
|
// { name: "Videos", path: "/videos", pro: false },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// icon: <PlugInIcon />,
|
||||||
|
// name: "Authentication",
|
||||||
|
// subItems: [
|
||||||
|
// { name: "Sign In", path: "/signin", pro: false },
|
||||||
|
// { name: "Sign Up", path: "/signup", pro: false },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
icon: <PieChartIcon />,
|
icon: <ShootingStarIcon />,
|
||||||
name: "Charts",
|
name: "Về Chúng Tôi",
|
||||||
subItems: [
|
path: "/user/about-us",
|
||||||
{ name: "Line Chart", path: "/line-chart", pro: false },
|
|
||||||
{ name: "Bar Chart", path: "/bar-chart", pro: false },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <BoxCubeIcon />,
|
icon: <ShootingStarIcon />,
|
||||||
name: "UI Elements",
|
name: "Hỗ trợ",
|
||||||
subItems: [
|
path: "/user/quick-qa",
|
||||||
{ name: "Alerts", path: "/alerts", pro: false },
|
|
||||||
{ name: "Avatar", path: "/avatars", pro: false },
|
|
||||||
{ name: "Badge", path: "/badge", pro: false },
|
|
||||||
{ name: "Buttons", path: "/buttons", pro: false },
|
|
||||||
{ name: "Images", path: "/images", pro: false },
|
|
||||||
{ name: "Videos", path: "/videos", pro: false },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <PlugInIcon />,
|
|
||||||
name: "Authentication",
|
|
||||||
subItems: [
|
|
||||||
{ name: "Sign In", path: "/signin", pro: false },
|
|
||||||
{ name: "Sign Up", path: "/signup", pro: false },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -280,12 +292,12 @@ const AppSidebar: React.FC = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
{renderMenuItems(ALL_NAV_ITEMS, "main")}
|
{renderMenuItems(ALL_NAV_ITEMS, "main")}
|
||||||
</div>
|
</div>
|
||||||
{/* <div>
|
<div>
|
||||||
<h2 className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}>
|
<h2 className={`mb-4 text-xs uppercase flex leading-[20px] text-gray-400 ${!isExpanded && !isHovered ? "lg:justify-center" : "justify-start"}`}>
|
||||||
{isExpanded || isHovered || isMobileOpen ? "Others" : <HorizontaLDots />}
|
{isExpanded || isHovered || isMobileOpen ? "Others" : <HorizontaLDots />}
|
||||||
</h2>
|
</h2>
|
||||||
{renderMenuItems(OTHERS_ITEMS, "others")}
|
{renderMenuItems(OTHERS_ITEMS, "others")}
|
||||||
</div> */}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import api from "@/config/config";
|
||||||
|
import { API } from "../../api";
|
||||||
|
import { ChatbotPayload, ChatbotResponse } from "@/interface/chatbot";
|
||||||
|
|
||||||
|
export const apiChatbot = async (payload: ChatbotPayload): Promise<ChatbotResponse> => {
|
||||||
|
const response = await api.post(API.Chatbot.CHAT,payload);
|
||||||
|
return await response?.data;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user