Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 44 additions & 48 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,63 @@
import { Outlet, createBrowserRouter, RouterProvider } from 'react-router-dom'
import { Suspense } from 'react'
import { lazy } from 'react'
import {HomeProvider} from './providers/HomeProvider'
import { HomeProvider } from './providers/HomeProvider'
import AppProvider from './providers/AppProvider'
import MainLayout from './components/layouts/MainLayout'
import DetailProvider from './providers/DetailProvider'
import { AuthProvider } from './providers/AuthProvider'
import ScrollToTop from './utils/scrollToTop'
import { ToastContainer } from 'react-toastify'
import { useAuth } from './hooks/useAuth.jsx'
import RequireLoginModal from './components/common/Modals/RequireLoginModal.jsx'

const Home = lazy(() => import('./pages/Home'))
const Login = lazy(() => import('./pages/Auth/Login/Login'))
const Register = lazy(() => import('./pages/Auth/Register/Register'))
const ForgotPassword = lazy(() => import('./pages/Auth/ForgotPassword/ForgotPassword'))
const ForgotPassword = lazy(
() => import('./pages/Auth/ForgotPassword/ForgotPassword'),
)
const MyList = lazy(() => import('./pages/MyList/MyList'))
const Search = lazy(() => import('./pages/Search/Search'))
const Movies = lazy(() => import('./pages/Movies'))
const TVShows = lazy(() => import('./pages/TVShows'))
const MediaDetails = lazy(() => import('./pages/MediaDetails/MediaDetails'))
const MediaPlayer = lazy(() => import('./pages/MediaPlayer/MediaPlayer'))
const PremiumCheckout = lazy(() => import('./pages/PremiumCheckout/PremiumCheckout'))
const ProfileSelection = lazy(() => import('./pages/Profile/ProfileSelection'))
const ProfileManage = lazy(() => import('./pages/Profile/ProfileManage'))
const ProfileForm = lazy(() => import('./pages/Profile/ProfileForm'))
const PremiumCheckout = lazy(
() => import('./pages/PremiumCheckout/PremiumCheckout'),
)
const AccountSettings = lazy(() => import('./pages/Account/AccountSettings'))

const PageLoader = () => <div className="min-h-screen flex items-center justify-center" />
const PageLoader = () => (
<div className="min-h-screen flex items-center justify-center" />
)

// 1. Tạo component con để xử lý UI và Gọi hook useAuth hợp lệ
const AppContent = () => {
const { isLoginModalOpen, setIsLoginModalOpen } = useAuth()

return (
<>
<ScrollToTop />
<Outlet />
<ToastContainer position="bottom-right" autoClose={1000} theme="dark" />

{/* Modal nằm ở đây sẽ nhận được state từ AuthProvider toàn cục */}
<RequireLoginModal
isOpen={isLoginModalOpen}
onClose={() => setIsLoginModalOpen(false)}
message="Bạn cần đăng nhập để lưu phim vào danh sách yêu thích."
/>
</>
)
}

// 2. RootLayout đóng vai trò bọc Provider thiết lập môi trường
const RootLayout = () => {
return (
<AppProvider>
<AuthProvider>
<ScrollToTop />
<Outlet />
<ToastContainer/>
<AppContent />
</AuthProvider>
</AppProvider>
)
Expand Down Expand Up @@ -104,9 +129,7 @@ const router = createBrowserRouter([
path: '/movies',
element: (
<Suspense fallback={<PageLoader />}>

<Movies />

<Movies />
</Suspense>
),
},
Expand All @@ -118,9 +141,16 @@ const router = createBrowserRouter([
</Suspense>
),
},
{
path: '/account',
element: (
<Suspense fallback={<PageLoader />}>
<AccountSettings />
</Suspense>
),
},
],
},

{
path: '/login',
element: (
Expand All @@ -145,39 +175,6 @@ const router = createBrowserRouter([
</Suspense>
),
},

{
path: '/profiles',
element: (
<Suspense fallback={<PageLoader />}>
<ProfileSelection />
</Suspense>
),
},
{
path: '/profiles/manage',
element: (
<Suspense fallback={<PageLoader />}>
<ProfileManage />
</Suspense>
),
},
{
path: '/profiles/add',
element: (
<Suspense fallback={<PageLoader />}>
<ProfileForm />
</Suspense>
),
},
{
path: '/profiles/edit/:id',
element: (
<Suspense fallback={<PageLoader />}>
<ProfileForm />
</Suspense>
),
},
],
},
])
Expand All @@ -191,4 +188,3 @@ const App = () => {
}

export default App

25 changes: 25 additions & 0 deletions client/src/api/userApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { apiClient } from './axiosClient'

export const getUserProfileApi = () =>
apiClient.get('/users/profile').then((res) => res.data.data)

export const updateUserProfileApi = (data) => {
const formData = new FormData()
if (data.fullName !== undefined) formData.append('fullName', data.fullName)
if (data.phone !== undefined) formData.append('phone', data.phone)
if (data.dateOfBirth !== undefined) formData.append('dateOfBirth', data.dateOfBirth)
if (data.gender !== undefined) formData.append('gender', data.gender)
if (data.avatar !== undefined) formData.append('avatar', data.avatar)

return apiClient.put('/users/profile', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
}).then((res) => res.data.data)
}

export const changePasswordApi = (data) =>
apiClient.post('/users/change-password', data).then((res) => res.data.data)

export const getSubscriptionHistoryApi = () =>
apiClient.get('/users/subscription-history').then((res) => res.data.data)
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,21 @@ import {
fetchCollectionsApi,
} from '../../../../api/collection.api.js'
import { useAuth } from '../../../../hooks/useAuth.jsx'
import { useHome } from '../../../../contexts/HomeContext.jsx'


// THÊM prop variant vào đây (mặc định là 'detail')
const FavouriteButton = ({ movie, variant = 'detail' }) => {
const location = useLocation()
const isHome = location.pathname === '/'
const queryClient = useQueryClient()
const [isModalOpen, setIsModalOpen] = useState(false)

const { isAuthenticated } = useAuth()
const { setIsLoginModalOpen } = useHome()

const { isAuthenticated, isLoginModalOpen, setIsLoginModalOpen } = useAuth()

const { data: collections = [], isLoading: isLoadingCollections } = useQuery({
queryKey: ['collections'],
queryFn: fetchCollectionsApi,
enabled: isAuthenticated,
})


const isLoved = useMemo(() => {
if (!collections || collections.length === 0 || !movie?.id) return false
Expand Down Expand Up @@ -64,6 +60,7 @@ const FavouriteButton = ({ movie, variant = 'detail' }) => {
e.stopPropagation()
if (!isAuthenticated) {
setIsLoginModalOpen(true)
return
}
if (!movie?.id) {
toast.error('Không tìm thấy thông tin phim!')
Expand Down Expand Up @@ -120,8 +117,6 @@ const FavouriteButton = ({ movie, variant = 'detail' }) => {
</div>
)}



{/* Modal chọn list */}
{isModalOpen && (
<div className="fixed inset-0 z-90 flex items-center justify-center p-4">
Expand Down
23 changes: 15 additions & 8 deletions client/src/components/common/AppBar/AppBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import { useMyPremiumSubscription } from '../../../hooks/usePremium.jsx'
import { useDebounce } from '../../../hooks/useDebounce.jsx'
import { useSearch } from '../../../hooks/useSearch.jsx'
import { createSlug } from '../../../utils/formatters.js'
import { useQueryClient } from '@tanstack/react-query'

const AppBar = () => {
const { user, isAuthenticated: isLogged, logout } = useAuth()
const queryClient = useQueryClient()
const { data: premiumSubscription } = useMyPremiumSubscription({
enabled: isLogged,
})
Expand Down Expand Up @@ -66,7 +68,16 @@ const AppBar = () => {
setSearchTerm('')
}
}

const handleLogoutClick = async () => {
try {
await logout()
queryClient.clear()
setIsUserMenuOpen(false)
navigate('/')
} catch (error) {
console.log("Lỗi khi đăng xuất: ", error)
}
}
return (
<div id="AppBar" className="relative w-full">
<div className="flex border-b border-white/10 justify-between items-center px-8 h-15 bg-black/95 backdrop-blur-md top-0 w-full z-50">
Expand Down Expand Up @@ -238,7 +249,7 @@ const AppBar = () => {
{/* Dropdown Menu */}
{isUserMenuOpen && (
<div className="absolute top-[130%] right-0 mt-3 w-72 bg-[#141414]/95 backdrop-blur-2xl border border-white/10 rounded-2xl shadow-2xl z-50 overflow-hidden ring-1 ring-black/50 transform transition-all animate-in fade-in slide-in-from-top-2">
<div className="p-5 bg-gradient-to-b from-white/[0.04] to-transparent border-b border-white/5">
<div className="p-5 bg-linear-to-b from-white/4 to-transparent border-b border-white/5">
<div className="flex items-center gap-4">
<div className="relative shrink-0">
{getAvatarUrl() ? (
Expand Down Expand Up @@ -297,7 +308,7 @@ const AppBar = () => {
<div className="p-2 flex flex-col gap-1">
<button
onClick={() => {
navigate('/profiles')
navigate('/account')
setIsUserMenuOpen(false)
}}
className="w-full px-4 py-2.5 text-sm font-medium text-gray-300 hover:text-white hover:bg-white/10 rounded-xl transition-all flex items-center gap-3 group"
Expand All @@ -312,11 +323,7 @@ const AppBar = () => {
<div className="h-px bg-white/5 my-1 mx-3"></div>

<button
onClick={async () => {
await logout()
setIsUserMenuOpen(false)
navigate('/login')
}}
onClick={handleLogoutClick}
className="w-full px-4 py-2.5 text-sm font-medium text-red-400 hover:text-white hover:bg-red-600/90 rounded-xl transition-all flex items-center gap-3 group"
>
<LogOut
Expand Down
74 changes: 74 additions & 0 deletions client/src/pages/Account/AccountSettings.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState } from 'react'
import { User, Shield, CreditCard, Users } from 'lucide-react'
import PersonalInfo from './PersonalInfo'
import SecuritySettings from './SecuritySettings'
import SubscriptionHistory from './SubscriptionHistory'
import ProfilesManagement from './ProfilesManagement'

export default function AccountSettings() {
const [activeTab, setActiveTab] = useState('personal')

const tabs = [
{ id: 'personal', label: 'Thông tin cá nhân', icon: User },
{ id: 'security', label: 'Bảo mật', icon: Shield },
{ id: 'subscription', label: 'Gói dịch vụ', icon: CreditCard },
{ id: 'profiles', label: 'Hồ sơ xem chung', icon: Users },
]

const renderContent = () => {
switch (activeTab) {
case 'personal':
return <PersonalInfo />
case 'security':
return <SecuritySettings />
case 'subscription':
return <SubscriptionHistory />
case 'profiles':
return <ProfilesManagement />
default:
return <PersonalInfo />
}
}

return (
<div className="min-h-screen bg-bg-default text-slate-200 pt-7 pb-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-6xl mx-auto">
<div className="mb-8">
<h1 className="text-3xl font-bold text-white mb-2">Cài đặt tài khoản</h1>
<p className="text-slate-400">Quản lý thông tin, bảo mật và các gói dịch vụ của bạn</p>
</div>

<div className="flex flex-col lg:flex-row gap-8">
{/* Sidebar Navigation */}
<div className="w-full lg:w-64 shrink-0">
<nav className="flex lg:flex-col gap-2 overflow-x-auto lg:overflow-visible pb-4 lg:pb-0 hide-scrollbar">
{tabs.map((tab) => {
const Icon = tab.icon
const isActive = activeTab === tab.id
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-3 px-4 py-3 rounded transition-all duration-200 whitespace-nowrap lg:whitespace-normal
${isActive
? 'bg-red-500/10 text-red-500 border border-red-500/20'
: 'text-slate-400 hover:bg-white/5 hover:text-slate-200 border border-transparent'
}`}
>
<Icon className={`w-5 h-5 ${isActive ? 'text-red-500' : 'text-slate-400'}`} />
<span className="font-medium text-sm">{tab.label}</span>
</button>
)
})}
</nav>
</div>

{/* Main Content Area */}
<div className="flex-1 min-w-0">
{renderContent()}
</div>
</div>
</div>
</div>
)
}
Loading