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
b49044df
Commit
b49044df
authored
May 20, 2026
by
Lê Đức Huy
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement admin base configuration management page with API and store integration
parent
67165167
Changes
3
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
327 additions
and
92 deletions
+327
-92
page.tsx
src/app/admin/base-config/page.tsx
+192
-58
logo.ts
src/lib/api/logo.ts
+103
-0
useAuthStore.ts
src/store/useAuthStore.ts
+32
-34
No files found.
src/app/admin/base-config/page.tsx
View file @
b49044df
...
@@ -36,6 +36,14 @@ import { Switch } from "@/components/ui/switch";
...
@@ -36,6 +36,14 @@ import { Switch } from "@/components/ui/switch";
import
{
Tabs
,
TabsContent
,
TabsList
,
TabsTrigger
}
from
"@/components/ui/tabs"
;
import
{
Tabs
,
TabsContent
,
TabsList
,
TabsTrigger
}
from
"@/components/ui/tabs"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
{
Textarea
}
from
"@/components/ui/textarea"
;
import
type
{
AdminMediaItem
}
from
"@/mockdata/admin-news"
;
import
type
{
AdminMediaItem
}
from
"@/mockdata/admin-news"
;
import
{
fetchCmsLogos
,
createCmsLogo
,
updateCmsLogo
,
deleteCmsLogo
,
type
LogoItem
,
}
from
"@/lib/api/logo"
;
import
{
fetchCmsFileById
,
toAdminMediaItem
}
from
"@/lib/api/files"
;
import
{
import
{
type
BaseConfigBannerItem
,
type
BaseConfigBannerItem
,
type
BaseConfigBranchItem
,
type
BaseConfigBranchItem
,
...
@@ -324,7 +332,60 @@ export default function AdminBaseConfigPage() {
...
@@ -324,7 +332,60 @@ export default function AdminBaseConfigPage() {
}
|
null
>
(
null
);
}
|
null
>
(
null
);
React
.
useEffect
(()
=>
{
React
.
useEffect
(()
=>
{
setConfig
(
readBaseConfig
());
const
init
=
async
()
=>
{
// 1. Read base config from local storage as initial state
const
localConfig
=
readBaseConfig
();
setConfig
(
localConfig
);
// 2. Fetch active logo from API
try
{
const
logoData
=
await
fetchCmsLogos
();
const
activeLogo
=
logoData
.
rows
?.[
0
]
??
null
;
if
(
activeLogo
)
{
// Construct the LogoItem config structure
const
apiLogo
:
BaseConfigLogoItem
=
{
id
:
activeLogo
.
id
,
name
:
activeLogo
.
logo_name
,
imageId
:
activeLogo
.
file_id
,
isActive
:
true
,
};
// Fetch the file details to construct the image preview URL
try
{
const
fileInfo
=
await
fetchCmsFileById
(
activeLogo
.
file_id
);
if
(
fileInfo
)
{
const
mediaItem
=
toAdminMediaItem
(
fileInfo
);
setMediaItems
((
prev
)
=>
{
const
nextMap
=
new
Map
(
prev
.
map
((
e
)
=>
[
e
.
id
,
e
]));
nextMap
.
set
(
mediaItem
.
id
,
mediaItem
);
return
Array
.
from
(
nextMap
.
values
());
});
}
}
catch
(
fileErr
)
{
console
.
error
(
"Error fetching file info for logo:"
,
fileErr
);
}
// Update config state with backend data
setConfig
((
prev
)
=>
{
if
(
!
prev
)
return
prev
;
const
updatedConfig
=
{
...
prev
,
logo
:
apiLogo
,
websiteName
:
activeLogo
.
logo_name
||
prev
.
websiteName
,
websiteLink
:
activeLogo
.
logo_url
||
prev
.
websiteLink
,
};
// Also persist to local storage as fallback for other pages (like dashboard)
persistBaseConfig
(
updatedConfig
);
return
updatedConfig
;
});
}
}
catch
(
err
)
{
console
.
error
(
"Error fetching active logo:"
,
err
);
}
};
void
init
();
},
[]);
},
[]);
const
mediaMap
=
React
.
useMemo
(
const
mediaMap
=
React
.
useMemo
(
...
@@ -377,7 +438,7 @@ export default function AdminBaseConfigPage() {
...
@@ -377,7 +438,7 @@ export default function AdminBaseConfigPage() {
setItemDialogOpen
(
true
);
setItemDialogOpen
(
true
);
};
};
const
handleSubmitItem
=
()
=>
{
const
handleSubmitItem
=
async
()
=>
{
if
(
!
config
)
return
;
if
(
!
config
)
return
;
const
trimmedName
=
itemForm
.
name
.
trim
();
const
trimmedName
=
itemForm
.
name
.
trim
();
...
@@ -393,16 +454,55 @@ export default function AdminBaseConfigPage() {
...
@@ -393,16 +454,55 @@ export default function AdminBaseConfigPage() {
setSavingItem
(
true
);
setSavingItem
(
true
);
const
nextConfig
=
cloneBaseConfigData
(
config
);
try
{
if
(
itemDialogMode
===
"logo"
)
{
if
(
itemDialogMode
===
"logo"
)
{
let
savedLogo
:
LogoItem
;
if
(
currentLogo
?.
id
)
{
savedLogo
=
await
updateCmsLogo
(
currentLogo
.
id
,
{
logo_name
:
trimmedName
,
file_id
:
itemForm
.
imageId
,
logo_url
:
config
.
websiteLink
||
""
,
});
}
else
{
savedLogo
=
await
createCmsLogo
({
logo_name
:
trimmedName
,
file_id
:
itemForm
.
imageId
,
logo_url
:
config
.
websiteLink
||
""
,
});
}
// Fetch file details for preview if needed
try
{
const
fileInfo
=
await
fetchCmsFileById
(
savedLogo
.
file_id
);
if
(
fileInfo
)
{
const
mediaItem
=
toAdminMediaItem
(
fileInfo
);
setMediaItems
((
prev
)
=>
{
const
nextMap
=
new
Map
(
prev
.
map
((
e
)
=>
[
e
.
id
,
e
]));
nextMap
.
set
(
mediaItem
.
id
,
mediaItem
);
return
Array
.
from
(
nextMap
.
values
());
});
}
}
catch
(
fileErr
)
{
console
.
error
(
"Error fetching file info after saving logo:"
,
fileErr
);
}
const
nextConfig
=
cloneBaseConfigData
(
config
);
nextConfig
.
logo
=
{
nextConfig
.
logo
=
{
id
:
editingItemId
||
currentLogo
?.
id
||
createBaseConfigItemId
(
"logo"
)
,
id
:
savedLogo
.
id
,
name
:
trimmedN
ame
,
name
:
savedLogo
.
logo_n
ame
,
imageId
:
itemForm
.
imageI
d
,
imageId
:
savedLogo
.
file_i
d
,
isActive
:
true
,
isActive
:
true
,
};
};
nextConfig
.
websiteName
=
savedLogo
.
logo_name
;
if
(
savedLogo
.
logo_url
)
{
nextConfig
.
websiteLink
=
savedLogo
.
logo_url
;
}
saveConfig
(
nextConfig
);
setItemDialogOpen
(
false
);
toast
.
success
(
"Đã lưu cấu hình logo"
);
}
else
{
}
else
{
const
nextConfig
=
cloneBaseConfigData
(
config
);
if
(
editingItemId
)
{
if
(
editingItemId
)
{
nextConfig
.
banners
=
nextConfig
.
banners
.
map
((
item
)
=>
nextConfig
.
banners
=
nextConfig
.
banners
.
map
((
item
)
=>
item
.
id
===
editingItemId
item
.
id
===
editingItemId
...
@@ -427,24 +527,25 @@ export default function AdminBaseConfigPage() {
...
@@ -427,24 +527,25 @@ export default function AdminBaseConfigPage() {
});
});
setCurrentBannerIndex
(
Math
.
max
(
nextConfig
.
banners
.
length
-
1
,
0
));
setCurrentBannerIndex
(
Math
.
max
(
nextConfig
.
banners
.
length
-
1
,
0
));
}
}
}
saveConfig
(
nextConfig
);
saveConfig
(
nextConfig
);
setSavingItem
(
false
);
setItemDialogOpen
(
false
);
setItemDialogOpen
(
false
);
toast
.
success
(
toast
.
success
(
"Đã lưu cấu hình banner"
);
itemDialogMode
===
"logo"
}
?
"Đã lưu cấu hình logo"
}
catch
(
err
)
{
:
"Đã lưu cấu hình banner"
,
toast
.
error
(
err
instanceof
Error
?
err
.
message
:
"Không thể lưu cấu hình"
);
);
}
finally
{
setSavingItem
(
false
);
}
};
};
const
handleDeleteItem
=
()
=>
{
const
handleDeleteItem
=
async
()
=>
{
if
(
!
config
||
!
deleteTarget
)
return
;
if
(
!
config
||
!
deleteTarget
)
return
;
try
{
const
nextConfig
=
cloneBaseConfigData
(
config
);
const
nextConfig
=
cloneBaseConfigData
(
config
);
if
(
deleteTarget
.
mode
===
"logo"
)
{
if
(
deleteTarget
.
mode
===
"logo"
)
{
await
deleteCmsLogo
(
deleteTarget
.
id
);
nextConfig
.
logo
=
null
;
nextConfig
.
logo
=
null
;
}
else
{
}
else
{
nextConfig
.
banners
=
nextConfig
.
banners
.
filter
((
item
)
=>
item
.
id
!==
deleteTarget
.
id
);
nextConfig
.
banners
=
nextConfig
.
banners
.
filter
((
item
)
=>
item
.
id
!==
deleteTarget
.
id
);
...
@@ -455,7 +556,11 @@ export default function AdminBaseConfigPage() {
...
@@ -455,7 +556,11 @@ export default function AdminBaseConfigPage() {
saveConfig
(
nextConfig
);
saveConfig
(
nextConfig
);
toast
.
success
(
"Đã xóa cấu hình"
);
toast
.
success
(
"Đã xóa cấu hình"
);
}
catch
(
err
)
{
toast
.
error
(
err
instanceof
Error
?
err
.
message
:
"Không thể xóa cấu hình"
);
}
finally
{
setDeleteTarget
(
null
);
setDeleteTarget
(
null
);
}
};
};
const
handleBranchChange
=
<
K
extends
keyof
BaseConfigBranchItem
>
(
const
handleBranchChange
=
<
K
extends
keyof
BaseConfigBranchItem
>
(
...
@@ -516,10 +621,39 @@ export default function AdminBaseConfigPage() {
...
@@ -516,10 +621,39 @@ export default function AdminBaseConfigPage() {
setConfig
((
previous
)
=>
(
previous
?
{
...
previous
,
[
key
]:
value
}
:
previous
));
setConfig
((
previous
)
=>
(
previous
?
{
...
previous
,
[
key
]:
value
}
:
previous
));
}
;
}
;
const handleSaveWebsiteInfo = () =
>
{
const handleSaveWebsiteInfo =
async
() =
>
{
if
(
!
config
)
return
;
if
(
!
config
)
return
;
setSavingItem
(
true
);
try
{
if
(
currentLogo
?.
id
)
{
const
updated
=
await
updateCmsLogo
(
currentLogo
.
id
,
{
logo_name
:
config
.
websiteName
,
file_id
:
currentLogo
.
imageId
,
logo_url
:
config
.
websiteLink
,
});
const
nextConfig
=
cloneBaseConfigData
(
config
);
nextConfig
.
logo
=
{
id
:
updated
.
id
,
name
:
updated
.
logo_name
,
imageId
:
updated
.
file_id
,
isActive
:
true
,
};
nextConfig
.
websiteName
=
updated
.
logo_name
;
if
(
updated
.
logo_url
)
{
nextConfig
.
websiteLink
=
updated
.
logo_url
;
}
saveConfig
(
nextConfig
);
}
else
{
saveConfig
(
config
);
saveConfig
(
config
);
}
toast
.
success
(
"Đã lưu thông tin website"
);
toast
.
success
(
"Đã lưu thông tin website"
);
}
catch
(
err
)
{
toast
.
error
(
err
instanceof
Error
?
err
.
message
:
"Không thể lưu thông tin website"
);
}
finally
{
setSavingItem
(
false
);
}
}
;
}
;
const handleSocialChange =
<
K
extends
keyof
BaseConfigSocialItem
>
(
const handleSocialChange =
<
K
extends
keyof
BaseConfigSocialItem
>
(
...
...
src/lib/api/logo.ts
0 → 100644
View file @
b49044df
"use client"
;
import
{
useCustomClient
}
from
"@/api/mutator/custom-client"
;
export
interface
LogoItem
{
id
:
string
;
logo_name
:
string
;
logo_url
:
string
|
null
;
file_id
:
string
;
created_at
:
string
;
created_by
?:
string
|
null
;
updated_at
:
string
;
updated_by
?:
string
|
null
;
}
export
interface
LogoListResult
{
rows
:
LogoItem
[];
count
:
number
;
page
:
number
;
pageSize
:
number
;
}
interface
LogoEnvelope
<
T
>
{
message
?:
string
;
message_en
?:
string
;
responseData
?:
T
;
data
?:
T
;
error
?:
string
;
status
?:
string
;
}
const
isObject
=
(
value
:
unknown
):
value
is
Record
<
string
,
unknown
>
=>
typeof
value
===
"object"
&&
value
!==
null
&&
!
Array
.
isArray
(
value
);
const
readMessage
=
(
payload
:
unknown
)
=>
{
if
(
!
isObject
(
payload
))
return
"Yêu cầu thất bại"
;
if
(
typeof
payload
.
message
===
"string"
&&
payload
.
message
.
trim
())
return
payload
.
message
;
if
(
typeof
payload
.
error
===
"string"
&&
payload
.
error
.
trim
())
return
payload
.
error
;
return
"Yêu cầu thất bại"
;
};
const
authHeaders
=
(
withJson
=
true
)
=>
{
const
headers
=
new
Headers
();
if
(
withJson
)
{
headers
.
set
(
"Content-Type"
,
"application/json"
);
}
return
headers
;
};
async
function
logoRequest
<
T
>
(
path
:
string
,
init
?:
RequestInit
):
Promise
<
T
>
{
const
payload
=
await
useCustomClient
<
LogoEnvelope
<
T
>
|
T
>
(
path
,
{
...
init
,
headers
:
init
?.
headers
??
authHeaders
(
init
?.
body
!==
undefined
),
});
if
(
isObject
(
payload
)
&&
"statusCode"
in
payload
)
{
const
statusCode
=
Number
(
payload
.
statusCode
);
if
(
statusCode
>=
400
)
{
throw
new
Error
(
readMessage
(
payload
));
}
}
if
(
isObject
(
payload
)
&&
(
"responseData"
in
payload
||
"data"
in
payload
))
{
return
((
payload
.
responseData
??
payload
.
data
)
as
T
)
??
({}
as
T
);
}
return
(
payload
??
{})
as
T
;
}
export
async
function
fetchCmsLogos
()
{
return
logoRequest
<
LogoListResult
>
(
"/logo?page=1&pageSize=10"
);
}
export
async
function
createCmsLogo
(
input
:
{
logo_name
:
string
;
logo_url
?:
string
|
null
;
file_id
:
string
;
})
{
return
logoRequest
<
LogoItem
>
(
"/logo"
,
{
method
:
"POST"
,
body
:
JSON
.
stringify
(
input
),
});
}
export
async
function
updateCmsLogo
(
id
:
string
,
input
:
{
logo_name
?:
string
;
logo_url
?:
string
|
null
;
file_id
?:
string
;
}
)
{
return
logoRequest
<
LogoItem
>
(
`/logo/
${
id
}
`
,
{
method
:
"PUT"
,
body
:
JSON
.
stringify
(
input
),
});
}
export
async
function
deleteCmsLogo
(
id
:
string
)
{
await
logoRequest
(
`/logo/
${
id
}
`
,
{
method
:
"DELETE"
,
});
}
src/store/useAuthStore.ts
View file @
b49044df
...
@@ -124,11 +124,8 @@ const normalizePersistedAuthState = (
...
@@ -124,11 +124,8 @@ const normalizePersistedAuthState = (
const
persistSession
=
persisted
.
appPersistSession
===
true
;
const
persistSession
=
persisted
.
appPersistSession
===
true
;
const
refreshTokenExpiredAt
=
getRefreshTokenExpiredAt
(
persisted
.
appSession
??
null
);
const
refreshTokenExpiredAt
=
getRefreshTokenExpiredAt
(
persisted
.
appSession
??
null
);
const
hasUsableSession
=
const
hasUsableSession
=
persistSession
&&
Boolean
(
persisted
.
appRefreshToken
)
&&
Boolean
(
persisted
.
appRefreshToken
)
&&
(
!
refreshTokenExpiredAt
||
refreshTokenExpiredAt
>
Date
.
now
())
&&
(
!
refreshTokenExpiredAt
||
refreshTokenExpiredAt
>
Date
.
now
());
(
!
persisted
.
appAccessTokenExpired
||
typeof
persisted
.
appAccessTokenExpired
===
"number"
);
if
(
!
hasUsableSession
)
{
if
(
!
hasUsableSession
)
{
return
{
return
{
...
@@ -141,25 +138,12 @@ const normalizePersistedAuthState = (
...
@@ -141,25 +138,12 @@ const normalizePersistedAuthState = (
return
{
return
{
...
currentState
,
...
currentState
,
...
persisted
,
...
persisted
,
appPersistSession
:
true
,
appPersistSession
:
persistSession
,
appUserRemember
:
rememberState
,
appUserRemember
:
rememberState
,
appIsRefreshing
:
false
,
appIsRefreshing
:
false
,
};
};
};
};
const
shouldPersistAuthStorage
=
(
value
:
string
)
=>
{
try
{
const
parsed
=
JSON
.
parse
(
value
)
as
{
state
?:
Partial
<
AuthStoreStateType
>
;
};
const
state
=
parsed
.
state
??
{};
return
state
.
appPersistSession
===
true
||
state
.
appUserRemember
?.
remember
===
true
;
}
catch
{
return
true
;
}
};
const
useAuthStore
=
create
<
AuthStoreStateType
>
()(
const
useAuthStore
=
create
<
AuthStoreStateType
>
()(
devtools
(
devtools
(
persist
(
persist
(
...
@@ -252,21 +236,37 @@ const useAuthStore = create<AuthStoreStateType>()(
...
@@ -252,21 +236,37 @@ const useAuthStore = create<AuthStoreStateType>()(
{
{
name
:
"app-auth-storage"
,
name
:
"app-auth-storage"
,
storage
:
createJSONStorage
(()
=>
({
storage
:
createJSONStorage
(()
=>
({
getItem
:
(
name
)
=>
localStorage
.
getItem
(
name
),
getItem
:
(
name
)
=>
{
if
(
typeof
window
===
"undefined"
)
return
null
;
return
localStorage
.
getItem
(
name
)
??
sessionStorage
.
getItem
(
name
);
},
setItem
:
(
name
,
value
)
=>
{
setItem
:
(
name
,
value
)
=>
{
if
(
shouldPersistAuthStorage
(
value
))
{
if
(
typeof
window
===
"undefined"
)
return
;
try
{
const
parsed
=
JSON
.
parse
(
value
)
as
{
state
?:
Partial
<
AuthStoreStateType
>
;
};
const
state
=
parsed
.
state
??
{};
if
(
state
.
appPersistSession
===
true
)
{
localStorage
.
setItem
(
name
,
value
);
localStorage
.
setItem
(
name
,
value
);
return
;
sessionStorage
.
removeItem
(
name
);
}
else
{
sessionStorage
.
setItem
(
name
,
value
);
localStorage
.
removeItem
(
name
);
}
}
}
catch
{
localStorage
.
setItem
(
name
,
value
);
}
},
removeItem
:
(
name
)
=>
{
if
(
typeof
window
===
"undefined"
)
return
;
localStorage
.
removeItem
(
name
);
localStorage
.
removeItem
(
name
);
sessionStorage
.
removeItem
(
name
);
},
},
removeItem
:
(
name
)
=>
localStorage
.
removeItem
(
name
),
})),
})),
partialize
:
(
state
)
=>
({
partialize
:
(
state
)
=>
({
appPersistSession
:
state
.
appPersistSession
,
appPersistSession
:
state
.
appPersistSession
,
...(
state
.
appPersistSession
?
{
appIsLoggedIn
:
state
.
appIsLoggedIn
,
appIsLoggedIn
:
state
.
appIsLoggedIn
,
appAccessToken
:
state
.
appAccessToken
,
appAccessToken
:
state
.
appAccessToken
,
appAccessTokenExpired
:
state
.
appAccessTokenExpired
,
appAccessTokenExpired
:
state
.
appAccessTokenExpired
,
...
@@ -274,8 +274,6 @@ const useAuthStore = create<AuthStoreStateType>()(
...
@@ -274,8 +274,6 @@ const useAuthStore = create<AuthStoreStateType>()(
appSession
:
state
.
appSession
,
appSession
:
state
.
appSession
,
appUser
:
state
.
appUser
,
appUser
:
state
.
appUser
,
appSessionExpiredNotified
:
state
.
appSessionExpiredNotified
,
appSessionExpiredNotified
:
state
.
appSessionExpiredNotified
,
}
:
{}),
appUserRemember
:
state
.
appUserRemember
,
appUserRemember
:
state
.
appUserRemember
,
}),
}),
merge
:
(
persistedState
,
currentState
)
=>
merge
:
(
persistedState
,
currentState
)
=>
...
...
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