Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
V
VCCI-News
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Văn Hoàng
VCCI-News
Commits
e7743761
Commit
e7743761
authored
May 21, 2026
by
Lê Đức Huy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement admin sidebar, layout shell, and login page components
parent
91a2bc40
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
70 additions
and
72 deletions
+70
-72
footer.tsx
src/app/(main)/_lib/layout/footer.tsx
+2
-27
header.tsx
src/app/(main)/_lib/layout/header.tsx
+27
-25
page.tsx
src/app/admin/login/page.tsx
+20
-10
admin-sidebar.tsx
src/components/shared/admin-sidebar.tsx
+21
-10
No files found.
src/app/(main)/_lib/layout/footer.tsx
View file @
e7743761
...
...
@@ -109,6 +109,7 @@ function Footer() {
const
contactInfo
=
{
name
:
primaryBranch
?.
branch_name
??
siteInformation
?.
website_name
??
"LIÊN ĐOÀN THƯƠNG MẠI & CÔNG NGHIỆP VIỆT NAM - CHI NHÁNH KHU VỰC THÀNH PHỐ HỒ CHÍ MINH"
,
address
:
...
...
@@ -285,7 +286,7 @@ function Footer() {
{
extraBranches
.
length
>
0
?
(
<
div
className=
"pt-4"
>
<
p
className=
"text-[14px] font-semibold uppercase text-[#dce7ff]"
>
Chi nhánh khác
C
ác c
hi nhánh khác
</
p
>
<
div
className=
"mt-3 space-y-3 text-[14px] text-[#c7d8ff]"
>
{
extraBranches
.
map
((
branch
)
=>
(
...
...
@@ -295,32 +296,6 @@ function Footer() {
{
branch
.
branch_name
}
</
p
>
)
:
null
}
{
branch
.
address
?
(
<
div
className=
"flex items-start gap-2"
>
<
MapPin
className=
"mt-0.5 h-3.5 w-3.5 shrink-0 text-[#f7b500]"
/>
<
span
>
{
branch
.
address
}
</
span
>
</
div
>
)
:
null
}
{
branch
.
telephone
||
branch
.
hotline
?
(
<
div
className=
"flex items-center gap-2"
>
<
Phone
className=
"h-3.5 w-3.5 shrink-0 text-[#f7b500]"
/>
<
span
>
{
branch
.
telephone
||
branch
.
hotline
}
</
span
>
</
div
>
)
:
null
}
{
branch
.
fax
?
(
<
div
className=
"flex items-center gap-2"
>
<
Printer
className=
"h-3.5 w-3.5 shrink-0 text-[#f7b500]"
/>
<
span
>
{
branch
.
fax
}
</
span
>
</
div
>
)
:
null
}
{
branch
.
email
?
(
<
div
className=
"flex items-center gap-2"
>
<
Mail
className=
"h-3.5 w-3.5 shrink-0 text-[#f7b500]"
/>
<
a
href=
{
`mailto:${branch.email}`
}
>
{
branch
.
email
}
</
a
>
</
div
>
)
:
null
}
</
div
>
))
}
</
div
>
...
...
src/app/(main)/_lib/layout/header.tsx
View file @
e7743761
...
...
@@ -7,8 +7,9 @@ import { useQuery } from "@tanstack/react-query";
import
Image
from
"next/image"
;
import
Link
from
"next/link"
;
import
logo
from
"@/assets/VCCI-HCM-logo-VN-2025.png"
;
import
{
g
etLogo
}
from
"@/api/endpoints/logo"
;
import
{
useG
etLogo
}
from
"@/api/endpoints/logo"
;
import
{
getSiteInformation
}
from
"@/api/endpoints/site-information"
;
import
{
resolveUploadUrl
}
from
"@/links"
;
import
type
{
Logo
}
from
"@/api/models/logo"
;
import
type
{
SiteInformationData
,
...
...
@@ -176,20 +177,23 @@ function Header() {
staleTime
:
5
*
60
*
1000
,
});
const
{
data
:
currentLogo
=
null
}
=
useQuery
({
queryKey
:
[
"header-logo"
],
queryFn
:
()
=>
getLogo
({
page
:
1
,
pageSize
:
1
,
sortField
:
"updated_at"
,
sortOrder
:
"desc"
,
}).
catch
(()
=>
null
),
select
:
(
response
)
=>
(
response
as
LogoListEnvelope
|
null
)?.
data
?.
responseData
?.
rows
?.[
0
]
??
null
,
staleTime
:
5
*
60
*
1000
,
});
const
{
data
:
currentLogo
=
null
}
=
useGetLogo
(
{
page
:
1
,
pageSize
:
1
,
sortField
:
"updated_at"
,
sortOrder
:
"desc"
,
},
{
query
:
{
select
:
(
response
:
any
)
=>
{
const
responseData
=
response
?.
responseData
??
response
?.
data
?.
responseData
;
return
(
responseData
?.
rows
?.[
0
]
as
Logo
|
undefined
)
??
null
;
},
staleTime
:
5
*
60
*
1000
,
},
}
);
const
{
data
:
siteInformationResponse
}
=
useQuery
<
ApiEnvelope
<
SiteInformationData
>
|
null
>
({
...
...
@@ -252,9 +256,8 @@ function Header() {
return
(
<
header
className=
"sticky top-0 z-50 shadow-[0_1px_0_rgba(15,23,42,0.05)]"
>
<
div
className=
{
`hidden w-full items-center justify-center overflow-hidden bg-[#25439a] ${
isTopBarHidden ? "lg:hidden" : "h-10 lg:flex"
}`
}
className=
{
`hidden w-full items-center justify-center overflow-hidden bg-[#25439a] ${isTopBarHidden ? "lg:hidden" : "h-10 lg:flex"
}`
}
>
<
div
className=
"mx-auto flex h-full w-full max-w-[1460px] items-center justify-between gap-6 px-6 xl:px-8"
>
<
div
className=
"flex items-center gap-2"
>
...
...
@@ -321,8 +324,8 @@ function Header() {
<
Image
width=
{
108
}
height=
{
40
}
className=
"h-auto w-[108px] object-contain"
src=
{
currentLogo
?.
logo_url
||
logo
}
className=
"h-auto
max-h-10
w-[108px] object-contain"
src=
{
currentLogo
?.
logo_url
?
resolveUploadUrl
(
currentLogo
.
logo_url
)
:
logo
}
alt=
{
currentLogo
?.
logo_name
||
"VCCI-HCM"
}
priority
/>
...
...
@@ -360,11 +363,10 @@ function Header() {
</
div
>
<
div
className=
{
`fixed inset-0 z-60 bg-white transition-all duration-300 lg:hidden ${
toggleMenu
className=
{
`fixed inset-0 z-60 bg-white transition-all duration-300 lg:hidden ${toggleMenu
? "pointer-events-auto translate-y-0 opacity-100"
: "pointer-events-none -translate-y-2 opacity-0"
}`
}
}`
}
>
<
div
className=
"flex h-full flex-col overflow-hidden"
>
<
div
className=
"sticky top-0 z-10 flex h-[78px] shrink-0 items-center justify-between border-b border-slate-100 bg-white px-6 shadow-[0_1px_0_rgba(15,23,42,0.04)]"
>
...
...
@@ -376,8 +378,8 @@ function Header() {
<
Image
width=
{
108
}
height=
{
40
}
className=
"h-auto w-[108px] object-contain"
src=
{
currentLogo
?.
logo_url
||
logo
}
className=
"h-auto
max-h-10
w-[108px] object-contain"
src=
{
currentLogo
?.
logo_url
?
resolveUploadUrl
(
currentLogo
.
logo_url
)
:
logo
}
alt=
{
currentLogo
?.
logo_name
||
"VCCI-HCM"
}
priority
/>
...
...
src/app/admin/login/page.tsx
View file @
e7743761
...
...
@@ -15,14 +15,14 @@ import {
}
from
"lucide-react"
;
import
{
toast
}
from
"sonner"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
g
etLogo
}
from
"@/api/endpoints/logo"
;
import
{
useG
etLogo
}
from
"@/api/endpoints/logo"
;
import
type
{
Logo
}
from
"@/api/models/logo"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Checkbox
}
from
"@/components/ui/checkbox"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
Label
}
from
"@/components/ui/label"
;
import
logo
from
"@/assets/VCCI-HCM-logo-VN-2025.png"
;
import
links
from
"@/links"
;
import
links
,
{
resolveUploadUrl
}
from
"@/links"
;
import
{
loginAdmin
}
from
"@/lib/auth/admin-auth"
;
import
useAuthStore
from
"@/store/useAuthStore"
;
...
...
@@ -131,12 +131,22 @@ function AuthShell({
mode
:
AuthMode
;
children
:
React
.
ReactNode
;
})
{
const
{
data
:
logoData
}
=
useQuery
({
queryKey
:
[
"logo"
,
{
page
:
1
,
pageSize
:
1
,
sortOrder
:
"desc"
}],
queryFn
:
()
=>
getLogo
({
page
:
1
,
pageSize
:
1
,
sortOrder
:
"desc"
}),
select
:
(
response
)
=>
(
response
as
LogoListEnvelope
)?.
data
?.
responseData
?.
rows
?.[
0
],
});
const
{
data
:
logoData
}
=
useGetLogo
(
{
page
:
1
,
pageSize
:
1
,
sortField
:
"updated_at"
,
sortOrder
:
"desc"
,
},
{
query
:
{
select
:
(
response
:
any
)
=>
{
const
responseData
=
response
?.
responseData
??
response
?.
data
?.
responseData
;
return
(
responseData
?.
rows
?.[
0
]
as
Logo
|
undefined
)
??
null
;
},
},
}
);
const
title
=
mode
===
"login"
...
...
@@ -163,7 +173,7 @@ function AuthShell({
<
div
className=
"flex items-center gap-4"
>
<
div
className=
"flex h-16 w-16 items-center justify-center rounded-2xl border border-[#063e8e]/10 bg-white shadow-sm"
>
<
Image
src=
{
logoData
?.
logo_url
||
logo
}
src=
{
logoData
?.
logo_url
?
resolveUploadUrl
(
logoData
.
logo_url
)
:
logo
}
alt=
{
logoData
?.
logo_name
||
"VCCI HCM"
}
width=
{
48
}
height=
{
48
}
...
...
@@ -217,7 +227,7 @@ function AuthShell({
<
div
className=
"mb-8 flex items-center gap-3 lg:hidden"
>
<
div
className=
"flex h-12 w-12 items-center justify-center rounded-2xl border border-[#063e8e]/10 bg-[#f8fbff]"
>
<
Image
src=
{
logoData
?.
logo_url
||
logo
}
src=
{
logoData
?.
logo_url
?
resolveUploadUrl
(
logoData
.
logo_url
)
:
logo
}
alt=
{
logoData
?.
logo_name
||
"VCCI HCM"
}
width=
{
36
}
height=
{
36
}
...
...
src/components/shared/admin-sidebar.tsx
View file @
e7743761
...
...
@@ -17,9 +17,10 @@ import {
Video
,
}
from
"lucide-react"
;
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
{
g
etLogo
}
from
"@/api/endpoints/logo"
;
import
{
useG
etLogo
}
from
"@/api/endpoints/logo"
;
import
type
{
Logo
}
from
"@/api/models/logo"
;
import
logo
from
"@/assets/VCCI-HCM-logo-VN-2025.png"
;
import
{
resolveUploadUrl
}
from
"@/links"
;
type
LogoListEnvelope
=
{
data
?:
{
...
...
@@ -89,12 +90,22 @@ export function AdminSidebar() {
Record
<
string
,
boolean
>
>
({});
const
{
data
:
logoData
}
=
useQuery
({
queryKey
:
[
"logo"
,
{
page
:
1
,
pageSize
:
1
,
sortOrder
:
"desc"
}],
queryFn
:
()
=>
getLogo
({
page
:
1
,
pageSize
:
1
,
sortOrder
:
"desc"
}),
select
:
(
response
)
=>
(
response
as
LogoListEnvelope
)?.
data
?.
responseData
?.
rows
?.[
0
],
});
const
{
data
:
logoData
}
=
useGetLogo
(
{
page
:
1
,
pageSize
:
1
,
sortField
:
"updated_at"
,
sortOrder
:
"desc"
,
},
{
query
:
{
select
:
(
response
:
any
)
=>
{
const
responseData
=
response
?.
responseData
??
response
?.
data
?.
responseData
;
return
(
responseData
?.
rows
?.[
0
]
as
Logo
|
undefined
)
??
null
;
},
},
}
);
const
isItemActive
=
React
.
useCallback
(
(
href
:
string
)
=>
{
...
...
@@ -146,7 +157,7 @@ export function AdminSidebar() {
>
<
div
className=
"flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl border border-[#063e8e]/10 bg-[#f8fbff] shadow-sm"
>
<
Image
src=
{
logoData
?.
logo_url
||
logo
}
src=
{
logoData
?.
logo_url
?
resolveUploadUrl
(
logoData
.
logo_url
)
:
logo
}
alt=
{
logoData
?.
logo_name
||
"VCCI HCM"
}
width=
{
40
}
height=
{
40
}
...
...
@@ -194,8 +205,8 @@ export function AdminSidebar() {
className=
{
cn
(
"rounded-[26px] border border-transparent transition-all duration-200"
,
isOpen
&&
expanded
&&
"border-[#063e8e]/10 bg-white/70 p-2 shadow-sm"
,
expanded
&&
"border-[#063e8e]/10 bg-white/70 p-2 shadow-sm"
,
)
}
>
<
button
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment