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
1a6e3d53
Commit
1a6e3d53
authored
Aug 13, 2026
by
Lê Bảo Hồng Đức
☄
Browse files
Options
Browse Files
Download
Plain Diff
[tag]0.1-vcci
parents
3dc23e79
c152d34d
Pipeline
#52566
passed with stages
in 4 minutes and 4 seconds
Changes
49
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
49 changed files
with
13194 additions
and
138 deletions
+13194
-138
next.config.ts
next.config.ts
+7
-0
post-history.ts
src/api/endpoints/post-history.ts
+139
-0
post.ts
src/api/endpoints/post.ts
+253
-62
deleteApiV10PostId200.ts
src/api/models/deleteApiV10PostId200.ts
+11
-0
deleteApiV10PostId200AllOf.ts
src/api/models/deleteApiV10PostId200AllOf.ts
+11
-0
getApiV10PostId200AllOf.ts
src/api/models/getApiV10PostId200AllOf.ts
+2
-2
getApiV10PostIdHistory200.ts
src/api/models/getApiV10PostIdHistory200.ts
+11
-0
getApiV10PostIdHistory200AllOf.ts
src/api/models/getApiV10PostIdHistory200AllOf.ts
+12
-0
getApiV10PostIdHistory200AllOfResponseDataItem.ts
.../models/getApiV10PostIdHistory200AllOfResponseDataItem.ts
+26
-0
getApiV10PostIdHistory200AllOfResponseDataItemAction.ts
...s/getApiV10PostIdHistory200AllOfResponseDataItemAction.ts
+17
-0
getApiV10PostIdHistory200AllOfResponseDataItemActor.ts
...ls/getApiV10PostIdHistory200AllOfResponseDataItemActor.ts
+17
-0
getApiV10PostIdHistory200AllOfResponseDataItemChanges.ts
.../getApiV10PostIdHistory200AllOfResponseDataItemChanges.ts
+13
-0
getApiV10PostIdHistory200AllOfResponseDataItemSnapshot.ts
...getApiV10PostIdHistory200AllOfResponseDataItemSnapshot.ts
+12
-0
index.ts
src/api/models/index.ts
+18
-0
passwordResetRequest.ts
src/api/models/passwordResetRequest.ts
+19
-0
passwordResetRequestMutate.ts
src/api/models/passwordResetRequestMutate.ts
+16
-0
post.ts
src/api/models/post.ts
+1
-0
postApiV10Post200AllOf.ts
src/api/models/postApiV10Post200AllOf.ts
+2
-2
postHistory.ts
src/api/models/postHistory.ts
+19
-0
postHistoryChanges.ts
src/api/models/postHistoryChanges.ts
+9
-0
postHistoryMutate.ts
src/api/models/postHistoryMutate.ts
+17
-0
postHistoryMutateChanges.ts
src/api/models/postHistoryMutateChanges.ts
+9
-0
postHistoryMutateSnapshot.ts
src/api/models/postHistoryMutateSnapshot.ts
+9
-0
postHistorySnapshot.ts
src/api/models/postHistorySnapshot.ts
+9
-0
postMutate.ts
src/api/models/postMutate.ts
+1
-0
putApiV10PostId200AllOf.ts
src/api/models/putApiV10PostId200AllOf.ts
+2
-2
siteInformation.ts
src/api/models/siteInformation.ts
+8
-5
siteInformationMutate.ts
src/api/models/siteInformationMutate.ts
+8
-5
siteInformationSocial.ts
src/api/models/siteInformationSocial.ts
+19
-0
userAuth.ts
src/api/models/userAuth.ts
+1
-0
userAuthMutate.ts
src/api/models/userAuthMutate.ts
+1
-0
post-history.ts
src/api/types/post-history.ts
+37
-0
index.tsx
src/app/(main)/(home)/components/events-calendar/index.tsx
+5
-0
index.tsx
src/app/(main)/(home)/components/members/index.tsx
+60
-23
use-advertisements.ts
src/app/(main)/(home)/lib/use-advertisements.ts
+1
-0
use-home-posts.ts
src/app/(main)/(home)/lib/use-home-posts.ts
+19
-3
data.ts
src/app/(main)/[...slug]/templates/data.ts
+35
-0
types.ts
src/app/(main)/[...slug]/templates/types.ts
+12
-0
page.tsx
src/app/admin/news/page.tsx
+71
-17
page.tsx
src/app/admin/users/page.tsx
+55
-5
route.ts
src/app/api/featured-members/route.ts
+33
-4
route.ts
src/app/api/partners/route.ts
+33
-4
news-form.tsx
src/components/admin/news-form.tsx
+173
-2
post-history-viewer.tsx
src/components/admin/post-history-viewer.tsx
+238
-0
index.tsx
src/components/shared/sidebar-advertisements/index.tsx
+2
-2
cms-admin.ts
src/lib/api/cms-admin.ts
+66
-0
mock-home-posts.ts
src/lib/mock-home-posts.ts
+2
-0
admin-news.ts
src/mockdata/admin-news.ts
+16
-0
swagger-output.json
swagger-output.json
+11637
-0
No files found.
next.config.ts
View file @
1a6e3d53
...
...
@@ -14,6 +14,13 @@ const nextConfig: NextConfig = {
},
]
:
[]),
// Local development
{
protocol
:
"http"
,
hostname
:
"localhost"
,
port
:
"3001"
,
pathname
:
"/uploads/**"
,
},
{
protocol
:
"https"
,
hostname
:
"vcci-hcm.org.vn"
,
// WordPress / media host
...
...
src/api/endpoints/post-history.ts
0 → 100644
View file @
1a6e3d53
import
{
useQuery
}
from
"@tanstack/react-query"
;
import
type
{
QueryFunction
,
QueryKey
,
UseQueryOptions
,
UseQueryResult
,
}
from
"@tanstack/react-query"
;
import
{
useCustomClient
}
from
"../mutator/custom-client"
;
import
type
{
ErrorType
}
from
"../mutator/custom-client"
;
import
type
{
PostHistoryItem
}
from
"../types/post-history"
;
type
SecondParameter
<
T
extends
(...
args
:
never
)
=>
unknown
>
=
Parameters
<
T
>
[
1
];
interface
ApiResponse
<
T
>
{
data
:
T
;
status
:
string
;
message
:
string
;
message_en
:
string
;
timeStamp
:
string
;
violations
:
string
|
null
;
}
type
GetPostHistoryResponse
=
ApiResponse
<
PostHistoryItem
[]
>
;
export
type
getApiV10PostIdHistoryResponse200
=
{
data
:
GetPostHistoryResponse
;
status
:
200
;
};
export
type
getApiV10PostIdHistoryResponse404
=
{
data
:
void
;
status
:
404
;
};
export
type
getApiV10PostIdHistoryResponseSuccess
=
getApiV10PostIdHistoryResponse200
&
{
headers
:
Headers
;
};
export
type
getApiV10PostIdHistoryResponseError
=
getApiV10PostIdHistoryResponse404
&
{
headers
:
Headers
;
};
export
type
getApiV10PostIdHistoryResponse
=
|
getApiV10PostIdHistoryResponseSuccess
|
getApiV10PostIdHistoryResponseError
;
export
const
getGetApiV10PostIdHistoryUrl
=
(
id
:
string
)
=>
{
return
`/api/v1.0/post/
${
id
}
/history`
;
};
export
const
getApiV10PostIdHistory
=
async
(
id
:
string
,
options
?:
RequestInit
):
Promise
<
getApiV10PostIdHistoryResponse
>
=>
{
return
useCustomClient
<
getApiV10PostIdHistoryResponse
>
(
getGetApiV10PostIdHistoryUrl
(
id
),
{
...
options
,
method
:
"GET"
,
}
);
};
export
const
getGetApiV10PostIdHistoryQueryKey
=
(
id
:
string
)
=>
{
return
[
`/api/v1.0/post/
${
id
}
/history`
]
as
const
;
};
export
const
getGetApiV10PostIdHistoryQueryOptions
=
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>
>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>
>
;
request
?:
SecondParameter
<
typeof
useCustomClient
>
;
}
)
=>
{
const
{
query
:
queryOptions
,
request
:
requestOptions
}
=
options
??
{};
const
queryKey
=
queryOptions
?.
queryKey
??
getGetApiV10PostIdHistoryQueryKey
(
id
);
const
queryFn
:
QueryFunction
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
>
=
({
signal
})
=>
getApiV10PostIdHistory
(
id
,
{
signal
,
...
requestOptions
});
return
{
queryKey
,
queryFn
,
enabled
:
!!
id
,
retry
:
3
,
retryDelay
:
1000
,
...
queryOptions
,
}
as
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>
&
{
queryKey
:
QueryKey
};
};
export
type
GetApiV10PostIdHistoryQueryResult
=
NonNullable
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
>
;
export
type
GetApiV10PostIdHistoryQueryError
=
ErrorType
<
void
>
;
export
function
useGetApiV10PostIdHistory
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>
>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>
>
;
request
?:
SecondParameter
<
typeof
useCustomClient
>
;
}
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
QueryKey
}
{
const
queryOptions
=
getGetApiV10PostIdHistoryQueryOptions
(
id
,
options
);
const
query
=
useQuery
(
queryOptions
,
undefined
)
as
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
QueryKey
};
query
.
queryKey
=
queryOptions
.
queryKey
;
return
query
;
}
src/api/endpoints/post.ts
View file @
1a6e3d53
...
...
@@ -30,12 +30,14 @@ import type {
}
from
'@tanstack/react-query'
;
import
type
{
DeleteApiV10PostId200
,
GetApiV10PostId200
,
GetApiV10PostIdHistory200
,
GetApiV10PostParams
,
PostApiV10Post200
,
Post
ApiV10PostBody
,
Post
Mutate
,
PutApiV10PostId200
,
PutApiV10PostIdBody
ResponseGetAllData
}
from
'../models'
;
import
{
useCustomClient
}
from
'../mutator/custom-client'
;
...
...
@@ -48,7 +50,7 @@ type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* Retrieve a single post record by its ID
(includes categories)
* Retrieve a single post record by its ID
* @summary Get post by ID
*/
export
type
getApiV10PostIdResponse200
=
{
...
...
@@ -269,13 +271,7 @@ export const prefetchGetApiV10PostIdQuery = async <TData = Awaited<ReturnType<ty
/**
* Update a post by ID.
category_ids behavior:
- Not provided: do not change existing category mappings
- Provided as []: remove all category mappings
- Provided as [ids...]: replace all mappings by the provided ids
Rule: all category_ids must exist and must be same type (post | trade | industry | page).
* Update a single post record by its ID
* @summary Update post by ID
*/
export
type
putApiV10PostIdResponse200
=
{
...
...
@@ -283,11 +279,6 @@ export type putApiV10PostIdResponse200 = {
status
:
200
}
export
type
putApiV10PostIdResponse400
=
{
data
:
void
status
:
400
}
export
type
putApiV10PostIdResponse404
=
{
data
:
void
status
:
404
...
...
@@ -296,7 +287,7 @@ export type putApiV10PostIdResponse404 = {
export
type
putApiV10PostIdResponseSuccess
=
(
putApiV10PostIdResponse200
)
&
{
headers
:
Headers
;
};
export
type
putApiV10PostIdResponseError
=
(
putApiV10PostIdResponse40
0
|
putApiV10PostIdResponse40
4
)
&
{
export
type
putApiV10PostIdResponseError
=
(
putApiV10PostIdResponse404
)
&
{
headers
:
Headers
;
};
...
...
@@ -311,7 +302,7 @@ export const getPutApiV10PostIdUrl = (id: string,) => {
}
export
const
putApiV10PostId
=
async
(
id
:
string
,
p
utApiV10PostIdBody
:
PutApiV10PostIdBody
,
options
?:
RequestInit
):
Promise
<
putApiV10PostIdResponse
>
=>
{
p
ostMutate
:
PostMutate
,
options
?:
RequestInit
):
Promise
<
putApiV10PostIdResponse
>
=>
{
return
useCustomClient
<
putApiV10PostIdResponse
>
(
getPutApiV10PostIdUrl
(
id
),
{
...
...
@@ -319,7 +310,7 @@ export const putApiV10PostId = async (id: string,
method
:
'PUT'
,
headers
:
{
'Content-Type'
:
'application/json'
,
...
options
?.
headers
},
body
:
JSON
.
stringify
(
p
utApiV10PostIdBody
,)
p
ostMutate
,)
}
);}
...
...
@@ -327,8 +318,8 @@ export const putApiV10PostId = async (id: string,
export
const
getPutApiV10PostIdMutationOptions
=
<
TError
=
ErrorType
<
void
>
,
TContext
=
unknown
>
(
options
?:
{
mutation
?:
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
TError
,{
id
:
string
;
data
:
BodyType
<
P
utApiV10PostIdBody
>
},
TContext
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
):
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
TError
,{
id
:
string
;
data
:
BodyType
<
P
utApiV10PostIdBody
>
},
TContext
>
=>
{
TContext
=
unknown
>
(
options
?:
{
mutation
?:
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
TError
,{
id
:
string
;
data
:
BodyType
<
P
ostMutate
>
},
TContext
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
):
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
TError
,{
id
:
string
;
data
:
BodyType
<
P
ostMutate
>
},
TContext
>
=>
{
const
mutationKey
=
[
'putApiV10PostId'
];
const
{
mutation
:
mutationOptions
,
request
:
requestOptions
}
=
options
?
...
...
@@ -340,7 +331,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
const
mutationFn
:
MutationFunction
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
{
id
:
string
;
data
:
BodyType
<
P
utApiV10PostIdBody
>
}
>
=
(
props
)
=>
{
const
mutationFn
:
MutationFunction
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
{
id
:
string
;
data
:
BodyType
<
P
ostMutate
>
}
>
=
(
props
)
=>
{
const
{
id
,
data
}
=
props
??
{};
return
putApiV10PostId
(
id
,
data
,
requestOptions
)
...
...
@@ -352,18 +343,18 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
return
{
mutationFn
,
...
mutationOptions
}}
export
type
PutApiV10PostIdMutationResult
=
NonNullable
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>>
export
type
PutApiV10PostIdMutationBody
=
BodyType
<
P
utApiV10PostIdBody
>
export
type
PutApiV10PostIdMutationBody
=
BodyType
<
P
ostMutate
>
export
type
PutApiV10PostIdMutationError
=
ErrorType
<
void
>
/**
* @summary Update post by ID
*/
export
const
usePutApiV10PostId
=
<
TError
=
ErrorType
<
void
>
,
TContext
=
unknown
>
(
options
?:
{
mutation
?:
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
TError
,{
id
:
string
;
data
:
BodyType
<
P
utApiV10PostIdBody
>
},
TContext
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
TContext
=
unknown
>
(
options
?:
{
mutation
?:
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
TError
,{
id
:
string
;
data
:
BodyType
<
P
ostMutate
>
},
TContext
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
UseMutationResult
<
Awaited
<
ReturnType
<
typeof
putApiV10PostId
>>
,
TError
,
{
id
:
string
;
data
:
BodyType
<
P
utApiV10PostIdBody
>
},
{
id
:
string
;
data
:
BodyType
<
P
ostMutate
>
},
TContext
>
=>
{
...
...
@@ -376,7 +367,7 @@ export const usePutApiV10PostId = <TError = ErrorType<void>,
* @summary Delete post by ID
*/
export
type
deleteApiV10PostIdResponse200
=
{
data
:
void
data
:
DeleteApiV10PostId200
status
:
200
}
...
...
@@ -462,16 +453,11 @@ export const useDeleteApiV10PostId = <TError = ErrorType<void>,
return
useMutation
(
mutationOptions
,
queryClient
);
}
/**
* Retrieve a list of post with pagination, filtering and sorting (includes categories).
Extra filter supported:
- filters=category.type==trade (allowed: post, trade, industry, page)
- filters=category.type==(trade|post)
- filters=category.id=={categoryId}
* Retrieve a list of post with pagination, filtering and sorting
* @summary Get all post
*/
export
type
getApiV10PostResponse200
=
{
data
:
void
data
:
ResponseGetAllData
status
:
200
}
...
...
@@ -688,35 +674,20 @@ export const prefetchGetApiV10PostQuery = async <TData = Awaited<ReturnType<type
/**
* Create a new post record.
If category_ids is provided, it will link this post to those categories.
Rule: all category_ids must exist and must be same type (post | trade | industry | page).
* Create a new post record
* @summary Create a post
*/
export
type
postApiV10PostResponse200
=
{
data
:
PostApiV10Post200
status
:
200
}
export
type
postApiV10PostResponse400
=
{
data
:
void
status
:
400
}
export
type
postApiV10PostResponse404
=
{
data
:
void
status
:
404
}
export
type
postApiV10PostResponseSuccess
=
(
postApiV10PostResponse200
)
&
{
headers
:
Headers
;
};
export
type
postApiV10PostResponseError
=
(
postApiV10PostResponse400
|
postApiV10PostResponse404
)
&
{
headers
:
Headers
;
};
;
export
type
postApiV10PostResponse
=
(
postApiV10PostResponseSuccess
|
postApiV10PostResponseError
)
export
type
postApiV10PostResponse
=
(
postApiV10PostResponseSuccess
)
export
const
getPostApiV10PostUrl
=
()
=>
{
...
...
@@ -726,7 +697,7 @@ export const getPostApiV10PostUrl = () => {
return
`/api/v1.0/post`
}
export
const
postApiV10Post
=
async
(
post
ApiV10PostBody
:
PostApiV10PostBody
,
options
?:
RequestInit
):
Promise
<
postApiV10PostResponse
>
=>
{
export
const
postApiV10Post
=
async
(
post
Mutate
:
PostMutate
,
options
?:
RequestInit
):
Promise
<
postApiV10PostResponse
>
=>
{
return
useCustomClient
<
postApiV10PostResponse
>
(
getPostApiV10PostUrl
(),
{
...
...
@@ -734,16 +705,16 @@ export const postApiV10Post = async (postApiV10PostBody: PostApiV10PostBody, opt
method
:
'POST'
,
headers
:
{
'Content-Type'
:
'application/json'
,
...
options
?.
headers
},
body
:
JSON
.
stringify
(
post
ApiV10PostBody
,)
post
Mutate
,)
}
);}
export
const
getPostApiV10PostMutationOptions
=
<
TError
=
ErrorType
<
void
>
,
TContext
=
unknown
>
(
options
?:
{
mutation
?:
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
TError
,{
data
:
BodyType
<
Post
ApiV10PostBody
>
},
TContext
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
):
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
TError
,{
data
:
BodyType
<
Post
ApiV10PostBody
>
},
TContext
>
=>
{
export
const
getPostApiV10PostMutationOptions
=
<
TError
=
ErrorType
<
unknown
>
,
TContext
=
unknown
>
(
options
?:
{
mutation
?:
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
TError
,{
data
:
BodyType
<
Post
Mutate
>
},
TContext
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
):
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
TError
,{
data
:
BodyType
<
Post
Mutate
>
},
TContext
>
=>
{
const
mutationKey
=
[
'postApiV10Post'
];
const
{
mutation
:
mutationOptions
,
request
:
requestOptions
}
=
options
?
...
...
@@ -755,7 +726,7 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
const
mutationFn
:
MutationFunction
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
{
data
:
BodyType
<
Post
ApiV10PostBody
>
}
>
=
(
props
)
=>
{
const
mutationFn
:
MutationFunction
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
{
data
:
BodyType
<
Post
Mutate
>
}
>
=
(
props
)
=>
{
const
{
data
}
=
props
??
{};
return
postApiV10Post
(
data
,
requestOptions
)
...
...
@@ -767,18 +738,18 @@ const {mutation: mutationOptions, request: requestOptions} = options ?
return
{
mutationFn
,
...
mutationOptions
}}
export
type
PostApiV10PostMutationResult
=
NonNullable
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>>
export
type
PostApiV10PostMutationBody
=
BodyType
<
Post
ApiV10PostBody
>
export
type
PostApiV10PostMutationError
=
ErrorType
<
void
>
export
type
PostApiV10PostMutationBody
=
BodyType
<
Post
Mutate
>
export
type
PostApiV10PostMutationError
=
ErrorType
<
unknown
>
/**
* @summary Create a post
*/
export
const
usePostApiV10Post
=
<
TError
=
ErrorType
<
void
>
,
TContext
=
unknown
>
(
options
?:
{
mutation
?:
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
TError
,{
data
:
BodyType
<
Post
ApiV10PostBody
>
},
TContext
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
export
const
usePostApiV10Post
=
<
TError
=
ErrorType
<
unknown
>
,
TContext
=
unknown
>
(
options
?:
{
mutation
?:
UseMutationOptions
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
TError
,{
data
:
BodyType
<
Post
Mutate
>
},
TContext
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
UseMutationResult
<
Awaited
<
ReturnType
<
typeof
postApiV10Post
>>
,
TError
,
{
data
:
BodyType
<
Post
ApiV10PostBody
>
},
{
data
:
BodyType
<
Post
Mutate
>
},
TContext
>
=>
{
...
...
@@ -786,4 +757,224 @@ export const usePostApiV10Post = <TError = ErrorType<void>,
return
useMutation
(
mutationOptions
,
queryClient
);
}
\ No newline at end of file
/**
* Retrieve the change history of a specific post
* @summary Get post history
*/
export
type
getApiV10PostIdHistoryResponse200
=
{
data
:
GetApiV10PostIdHistory200
status
:
200
}
export
type
getApiV10PostIdHistoryResponse404
=
{
data
:
void
status
:
404
}
export
type
getApiV10PostIdHistoryResponseSuccess
=
(
getApiV10PostIdHistoryResponse200
)
&
{
headers
:
Headers
;
};
export
type
getApiV10PostIdHistoryResponseError
=
(
getApiV10PostIdHistoryResponse404
)
&
{
headers
:
Headers
;
};
export
type
getApiV10PostIdHistoryResponse
=
(
getApiV10PostIdHistoryResponseSuccess
|
getApiV10PostIdHistoryResponseError
)
export
const
getGetApiV10PostIdHistoryUrl
=
(
id
:
string
,)
=>
{
return
`/api/v1.0/post/
${
id
}
/history`
}
export
const
getApiV10PostIdHistory
=
async
(
id
:
string
,
options
?:
RequestInit
):
Promise
<
getApiV10PostIdHistoryResponse
>
=>
{
return
useCustomClient
<
getApiV10PostIdHistoryResponse
>
(
getGetApiV10PostIdHistoryUrl
(
id
),
{
...
options
,
method
:
'GET'
}
);}
export
const
getGetApiV10PostIdHistoryInfiniteQueryKey
=
(
id
?:
string
,)
=>
{
return
[
'infinite'
,
`/api/v1.0/post/
${
id
}
/history`
]
as
const
;
}
export
const
getGetApiV10PostIdHistoryQueryKey
=
(
id
?:
string
,)
=>
{
return
[
`/api/v1.0/post/
${
id
}
/history`
]
as
const
;
}
export
const
getGetApiV10PostIdHistoryInfiniteQueryOptions
=
<
TData
=
InfiniteData
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseInfiniteQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
)
=>
{
const
{
query
:
queryOptions
,
request
:
requestOptions
}
=
options
??
{};
const
queryKey
=
queryOptions
?.
queryKey
??
getGetApiV10PostIdHistoryInfiniteQueryKey
(
id
);
const
queryFn
:
QueryFunction
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
=
({
signal
})
=>
getApiV10PostIdHistory
(
id
,
{
signal
,
...
requestOptions
});
return
{
queryKey
,
queryFn
,
enabled
:
!!
(
id
),
retry
:
3
,
retryDelay
:
1000
,
...
queryOptions
}
as
UseInfiniteQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
}
export
type
GetApiV10PostIdHistoryInfiniteQueryResult
=
NonNullable
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
export
type
GetApiV10PostIdHistoryInfiniteQueryError
=
ErrorType
<
void
>
export
function
useGetApiV10PostIdHistoryInfinite
<
TData
=
InfiniteData
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
:
{
query
:
Partial
<
UseInfiniteQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
&
Pick
<
DefinedInitialDataOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
>
,
'initialData'
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
DefinedUseInfiniteQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
export
function
useGetApiV10PostIdHistoryInfinite
<
TData
=
InfiniteData
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseInfiniteQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
&
Pick
<
UndefinedInitialDataOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
>
,
'initialData'
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
UseInfiniteQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
export
function
useGetApiV10PostIdHistoryInfinite
<
TData
=
InfiniteData
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseInfiniteQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
UseInfiniteQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
/**
* @summary Get post history
*/
export
function
useGetApiV10PostIdHistoryInfinite
<
TData
=
InfiniteData
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseInfiniteQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
UseInfiniteQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
{
const
queryOptions
=
getGetApiV10PostIdHistoryInfiniteQueryOptions
(
id
,
options
)
const
query
=
useInfiniteQuery
(
queryOptions
,
queryClient
)
as
UseInfiniteQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
};
query
.
queryKey
=
queryOptions
.
queryKey
;
return
query
;
}
/**
* @summary Get post history
*/
export
const
prefetchGetApiV10PostIdHistoryInfiniteQuery
=
async
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>>
(
queryClient
:
QueryClient
,
id
:
string
,
options
?:
{
query
?:
Partial
<
UseInfiniteQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
):
Promise
<
QueryClient
>
=>
{
const
queryOptions
=
getGetApiV10PostIdHistoryInfiniteQueryOptions
(
id
,
options
)
await
queryClient
.
prefetchInfiniteQuery
(
queryOptions
);
return
queryClient
;
}
export
const
getGetApiV10PostIdHistoryQueryOptions
=
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
)
=>
{
const
{
query
:
queryOptions
,
request
:
requestOptions
}
=
options
??
{};
const
queryKey
=
queryOptions
?.
queryKey
??
getGetApiV10PostIdHistoryQueryKey
(
id
);
const
queryFn
:
QueryFunction
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
=
({
signal
})
=>
getApiV10PostIdHistory
(
id
,
{
signal
,
...
requestOptions
});
return
{
queryKey
,
queryFn
,
enabled
:
!!
(
id
),
retry
:
3
,
retryDelay
:
1000
,
...
queryOptions
}
as
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
}
export
type
GetApiV10PostIdHistoryQueryResult
=
NonNullable
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>>
export
type
GetApiV10PostIdHistoryQueryError
=
ErrorType
<
void
>
export
function
useGetApiV10PostIdHistory
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
:
{
query
:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
&
Pick
<
DefinedInitialDataOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
>
,
'initialData'
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
DefinedUseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
export
function
useGetApiV10PostIdHistory
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
&
Pick
<
UndefinedInitialDataOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
>
,
'initialData'
>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
export
function
useGetApiV10PostIdHistory
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
/**
* @summary Get post history
*/
export
function
useGetApiV10PostIdHistory
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>>
(
id
:
string
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
{
const
queryOptions
=
getGetApiV10PostIdHistoryQueryOptions
(
id
,
options
)
const
query
=
useQuery
(
queryOptions
,
queryClient
)
as
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
};
query
.
queryKey
=
queryOptions
.
queryKey
;
return
query
;
}
/**
* @summary Get post history
*/
export
const
prefetchGetApiV10PostIdHistoryQuery
=
async
<
TData
=
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
=
ErrorType
<
void
>>
(
queryClient
:
QueryClient
,
id
:
string
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
getApiV10PostIdHistory
>>
,
TError
,
TData
>>
,
request
?:
SecondParameter
<
typeof
useCustomClient
>
}
):
Promise
<
QueryClient
>
=>
{
const
queryOptions
=
getGetApiV10PostIdHistoryQueryOptions
(
id
,
options
)
await
queryClient
.
prefetchQuery
(
queryOptions
);
return
queryClient
;
}
src/api/models/deleteApiV10PostId200.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
ApiResponse
}
from
'./apiResponse'
;
import
type
{
DeleteApiV10PostId200AllOf
}
from
'./deleteApiV10PostId200AllOf'
;
export
type
DeleteApiV10PostId200
=
ApiResponse
&
DeleteApiV10PostId200AllOf
;
src/api/models/deleteApiV10PostId200AllOf.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
type
DeleteApiV10PostId200AllOf
=
{
responseData
?:
boolean
;
};
src/api/models/getApiV10PostId200AllOf.ts
View file @
1a6e3d53
...
...
@@ -5,8 +5,8 @@
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
GetApiV10PostId200AllOfResponseData
}
from
'./getApiV10PostId200AllOfResponseData
'
;
import
type
{
Post
}
from
'./post
'
;
export
type
GetApiV10PostId200AllOf
=
{
responseData
?:
GetApiV10PostId200AllOfResponseData
;
responseData
?:
Post
;
};
src/api/models/getApiV10PostIdHistory200.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
ApiResponse
}
from
'./apiResponse'
;
import
type
{
GetApiV10PostIdHistory200AllOf
}
from
'./getApiV10PostIdHistory200AllOf'
;
export
type
GetApiV10PostIdHistory200
=
ApiResponse
&
GetApiV10PostIdHistory200AllOf
;
src/api/models/getApiV10PostIdHistory200AllOf.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
GetApiV10PostIdHistory200AllOfResponseDataItem
}
from
'./getApiV10PostIdHistory200AllOfResponseDataItem'
;
export
type
GetApiV10PostIdHistory200AllOf
=
{
responseData
?:
GetApiV10PostIdHistory200AllOfResponseDataItem
[];
};
src/api/models/getApiV10PostIdHistory200AllOfResponseDataItem.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
GetApiV10PostIdHistory200AllOfResponseDataItemAction
}
from
'./getApiV10PostIdHistory200AllOfResponseDataItemAction'
;
import
type
{
GetApiV10PostIdHistory200AllOfResponseDataItemActor
}
from
'./getApiV10PostIdHistory200AllOfResponseDataItemActor'
;
import
type
{
GetApiV10PostIdHistory200AllOfResponseDataItemChanges
}
from
'./getApiV10PostIdHistory200AllOfResponseDataItemChanges'
;
import
type
{
GetApiV10PostIdHistory200AllOfResponseDataItemSnapshot
}
from
'./getApiV10PostIdHistory200AllOfResponseDataItemSnapshot'
;
export
type
GetApiV10PostIdHistory200AllOfResponseDataItem
=
{
id
?:
string
;
post_id
?:
string
;
action
?:
GetApiV10PostIdHistory200AllOfResponseDataItemAction
;
actor
?:
GetApiV10PostIdHistory200AllOfResponseDataItemActor
;
/**
* { field: { old: x, new: y } }
* @nullable
*/
changes
?:
GetApiV10PostIdHistory200AllOfResponseDataItemChanges
;
/** @nullable */
snapshot
?:
GetApiV10PostIdHistory200AllOfResponseDataItemSnapshot
;
created_at
?:
string
;
};
src/api/models/getApiV10PostIdHistory200AllOfResponseDataItemAction.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
type
GetApiV10PostIdHistory200AllOfResponseDataItemAction
=
typeof
GetApiV10PostIdHistory200AllOfResponseDataItemAction
[
keyof
typeof
GetApiV10PostIdHistory200AllOfResponseDataItemAction
];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export
const
GetApiV10PostIdHistory200AllOfResponseDataItemAction
=
{
CREATE
:
'CREATE'
,
UPDATE
:
'UPDATE'
,
DELETE
:
'DELETE'
,
}
as
const
;
src/api/models/getApiV10PostIdHistory200AllOfResponseDataItemActor.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
type
GetApiV10PostIdHistory200AllOfResponseDataItemActor
=
{
id
?:
string
;
email
?:
string
;
/** @nullable */
username
?:
string
|
null
;
full_name
?:
string
;
/** @nullable */
avatar_url
?:
string
|
null
;
};
src/api/models/getApiV10PostIdHistory200AllOfResponseDataItemChanges.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
/**
* { field: { old: x, new: y } }
* @nullable
*/
export
type
GetApiV10PostIdHistory200AllOfResponseDataItemChanges
=
{
[
key
:
string
]:
unknown
}
|
null
;
src/api/models/getApiV10PostIdHistory200AllOfResponseDataItemSnapshot.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
/**
* @nullable
*/
export
type
GetApiV10PostIdHistory200AllOfResponseDataItemSnapshot
=
{
[
key
:
string
]:
unknown
}
|
null
;
src/api/models/index.ts
View file @
1a6e3d53
...
...
@@ -70,6 +70,8 @@ export * from './deleteApiV10PermissionBulk200AllOfResponseData';
export
*
from
'./deleteApiV10PositionId200'
;
export
*
from
'./deleteApiV10PositionId200AllOf'
;
export
*
from
'./deleteApiV10PostCategoryPostIdParams'
;
export
*
from
'./deleteApiV10PostId200'
;
export
*
from
'./deleteApiV10PostId200AllOf'
;
export
*
from
'./deleteApiV10PostTagPostIdParams'
;
export
*
from
'./deleteApiV10RoleBulk200'
;
export
*
from
'./deleteApiV10RoleBulk200AllOf'
;
...
...
@@ -149,6 +151,13 @@ export * from './getApiV10PostId200AllOfResponseDataAllOfCategoryIdsItem';
export
*
from
'./getApiV10PostId200AllOfResponseDataAllOfCategoryIdsItemType'
;
export
*
from
'./getApiV10PostId200AllOfResponseDataAllOfStatus'
;
export
*
from
'./getApiV10PostId200AllOfResponseDataAllOfType'
;
export
*
from
'./getApiV10PostIdHistory200'
;
export
*
from
'./getApiV10PostIdHistory200AllOf'
;
export
*
from
'./getApiV10PostIdHistory200AllOfResponseDataItem'
;
export
*
from
'./getApiV10PostIdHistory200AllOfResponseDataItemAction'
;
export
*
from
'./getApiV10PostIdHistory200AllOfResponseDataItemActor'
;
export
*
from
'./getApiV10PostIdHistory200AllOfResponseDataItemChanges'
;
export
*
from
'./getApiV10PostIdHistory200AllOfResponseDataItemSnapshot'
;
export
*
from
'./getApiV10PostParams'
;
export
*
from
'./getApiV10PostTagParams'
;
export
*
from
'./getApiV10PostTagPostIdParams'
;
...
...
@@ -209,6 +218,8 @@ export * from './pageConfigTag';
export
*
from
'./pageConfigTagMutate'
;
export
*
from
'./pageParameter'
;
export
*
from
'./pageSizeParameter'
;
export
*
from
'./passwordResetRequest'
;
export
*
from
'./passwordResetRequestMutate'
;
export
*
from
'./patchApiV10NewsletterSubscriptionId200'
;
export
*
from
'./patchApiV10NewsletterSubscriptionId200AllOf'
;
export
*
from
'./patchApiV10TagId200'
;
...
...
@@ -316,6 +327,12 @@ export * from './postCategory';
export
*
from
'./postCategoryBulkCreate'
;
export
*
from
'./postCategoryMutate'
;
export
*
from
'./postContentStructure'
;
export
*
from
'./postHistory'
;
export
*
from
'./postHistoryChanges'
;
export
*
from
'./postHistoryMutate'
;
export
*
from
'./postHistoryMutateChanges'
;
export
*
from
'./postHistoryMutateSnapshot'
;
export
*
from
'./postHistorySnapshot'
;
export
*
from
'./postMutate'
;
export
*
from
'./postMutateContentStructure'
;
export
*
from
'./postTag'
;
...
...
@@ -388,6 +405,7 @@ export * from './siteInformationMutate';
export
*
from
'./siteInformationMutateLinkSocials'
;
export
*
from
'./siteInformationResponse'
;
export
*
from
'./siteInformationResponseAllOf'
;
export
*
from
'./siteInformationSocial'
;
export
*
from
'./siteInformationSocialLink'
;
export
*
from
'./siteInformationSocialMutate'
;
export
*
from
'./siteInformationUpdateBody'
;
...
...
src/api/models/passwordResetRequest.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
interface
PasswordResetRequest
{
id
?:
string
;
email
?:
string
;
note
?:
string
;
status
?:
string
;
resolved_by
?:
string
;
resolved_at
?:
string
;
resolve_note
?:
string
;
created_at
?:
string
;
updated_at
?:
string
;
}
src/api/models/passwordResetRequestMutate.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
interface
PasswordResetRequestMutate
{
email
?:
string
;
note
?:
string
;
status
?:
string
;
resolved_by
?:
string
;
resolved_at
?:
string
;
resolve_note
?:
string
;
}
src/api/models/post.ts
View file @
1a6e3d53
...
...
@@ -35,4 +35,5 @@ export interface Post {
location
?:
string
;
participation_fee
?:
string
;
content_structure
?:
PostContentStructure
;
event_dates
?:
string
;
}
src/api/models/postApiV10Post200AllOf.ts
View file @
1a6e3d53
...
...
@@ -5,8 +5,8 @@
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
Post
ApiV10Post200AllOfResponseData
}
from
'./postApiV10Post200AllOfResponseData
'
;
import
type
{
Post
}
from
'./post
'
;
export
type
PostApiV10Post200AllOf
=
{
responseData
?:
Post
ApiV10Post200AllOfResponseData
;
responseData
?:
Post
;
};
src/api/models/postHistory.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
PostHistoryChanges
}
from
'./postHistoryChanges'
;
import
type
{
PostHistorySnapshot
}
from
'./postHistorySnapshot'
;
export
interface
PostHistory
{
id
?:
string
;
post_id
?:
string
;
action
?:
string
;
actor_id
?:
string
;
changes
?:
PostHistoryChanges
;
snapshot
?:
PostHistorySnapshot
;
created_at
?:
string
;
}
src/api/models/postHistoryChanges.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
type
PostHistoryChanges
=
{
[
key
:
string
]:
unknown
};
src/api/models/postHistoryMutate.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
PostHistoryMutateChanges
}
from
'./postHistoryMutateChanges'
;
import
type
{
PostHistoryMutateSnapshot
}
from
'./postHistoryMutateSnapshot'
;
export
interface
PostHistoryMutate
{
post_id
?:
string
;
action
?:
string
;
actor_id
?:
string
;
changes
?:
PostHistoryMutateChanges
;
snapshot
?:
PostHistoryMutateSnapshot
;
}
src/api/models/postHistoryMutateChanges.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
type
PostHistoryMutateChanges
=
{
[
key
:
string
]:
unknown
};
src/api/models/postHistoryMutateSnapshot.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
type
PostHistoryMutateSnapshot
=
{
[
key
:
string
]:
unknown
};
src/api/models/postHistorySnapshot.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
type
PostHistorySnapshot
=
{
[
key
:
string
]:
unknown
};
src/api/models/postMutate.ts
View file @
1a6e3d53
...
...
@@ -30,4 +30,5 @@ export interface PostMutate {
location
?:
string
;
participation_fee
?:
string
;
content_structure
?:
PostMutateContentStructure
;
event_dates
?:
string
;
}
src/api/models/putApiV10PostId200AllOf.ts
View file @
1a6e3d53
...
...
@@ -5,8 +5,8 @@
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
P
utApiV10PostId200AllOfResponseData
}
from
'./putApiV10PostId200AllOfResponseData
'
;
import
type
{
P
ost
}
from
'./post
'
;
export
type
PutApiV10PostId200AllOf
=
{
responseData
?:
P
utApiV10PostId200AllOfResponseData
;
responseData
?:
P
ost
;
};
src/api/models/siteInformation.ts
View file @
1a6e3d53
...
...
@@ -5,18 +5,21 @@
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
SiteInformationLinkSocials
}
from
'./siteInformationLinkSocials'
;
export
interface
SiteInformation
{
id
?:
string
;
code
?:
string
;
telephone
?:
string
;
hotline
?:
string
;
email
?:
string
;
address
?:
string
;
working_hours
?:
string
;
link_socials
?:
SiteInformationLinkSocials
;
created_at
?:
string
;
created_by
?:
string
;
updated_at
?:
string
;
updated_by
?:
string
;
branch_name
?:
string
;
fax
?:
string
;
googlemap_link
?:
string
;
sort_order
?:
number
;
is_active
?:
boolean
;
website_name
?:
string
;
website_link
?:
string
;
}
src/api/models/siteInformationMutate.ts
View file @
1a6e3d53
...
...
@@ -5,13 +5,16 @@
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
SiteInformationMutateLinkSocials
}
from
'./siteInformationMutateLinkSocials'
;
export
interface
SiteInformationMutate
{
code
?:
string
;
telephone
?:
string
;
hotline
?:
string
;
email
?:
string
;
address
?:
string
;
working_hours
?:
string
;
link_socials
?:
SiteInformationMutateLinkSocials
;
branch_name
?:
string
;
fax
?:
string
;
googlemap_link
?:
string
;
sort_order
?:
number
;
is_active
?:
boolean
;
website_name
?:
string
;
website_link
?:
string
;
}
src/api/models/siteInformationSocial.ts
0 → 100644
View file @
1a6e3d53
/**
* Generated by orval v8.0.0-rc.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
interface
SiteInformationSocial
{
id
?:
string
;
platform
?:
string
;
label
?:
string
;
icon_key
?:
string
;
url
?:
string
;
sort_order
?:
number
;
is_active
?:
boolean
;
created_at
?:
string
;
updated_at
?:
string
;
}
src/api/models/userAuth.ts
View file @
1a6e3d53
...
...
@@ -26,4 +26,5 @@ export interface UserAuth {
reset_password_otp_expires_at
?:
string
;
reset_password_otp_attempts
?:
number
;
reset_password_otp_sent_at
?:
string
;
must_change_password
?:
boolean
;
}
src/api/models/userAuthMutate.ts
View file @
1a6e3d53
...
...
@@ -23,4 +23,5 @@ export interface UserAuthMutate {
reset_password_otp_expires_at
?:
string
;
reset_password_otp_attempts
?:
number
;
reset_password_otp_sent_at
?:
string
;
must_change_password
?:
boolean
;
}
src/api/types/post-history.ts
0 → 100644
View file @
1a6e3d53
export
interface
PostHistoryUser
{
id
:
string
email
:
string
username
:
string
|
null
first_name
:
string
|
null
last_name
:
string
|
null
full_name
:
string
avatar_url
:
string
|
null
}
export
type
PostHistoryAction
=
"CREATE"
|
"UPDATE"
|
"DELETE"
export
interface
PostHistoryChanges
{
[
field
:
string
]:
{
old
:
unknown
new
:
unknown
}
}
export
interface
PostHistoryItem
{
id
:
string
post_id
:
string
action
:
PostHistoryAction
actor
:
PostHistoryUser
|
null
changes
:
PostHistoryChanges
|
null
snapshot
:
Record
<
string
,
unknown
>
|
null
created_at
:
string
}
export
interface
PostHistoryResponse
{
data
:
PostHistoryItem
[]
status
:
string
message
:
string
message_en
:
string
timeStamp
:
string
violations
:
string
|
null
}
src/app/(main)/(home)/components/events-calendar/index.tsx
View file @
1a6e3d53
...
...
@@ -15,6 +15,11 @@ const formatDateTime = (value: string) =>
value
?
dayjs
(
value
).
format
(
"DD/MM/YYYY HH:mm"
)
:
"Đang cập nhật"
;
const
getEventDateRange
=
(
item
:
HomePostItem
)
=>
{
// Ưu tiên event_dates (ngày cụ thể)
if
(
item
.
eventDates
&&
item
.
eventDates
.
length
>
0
)
{
return
item
.
eventDates
.
map
((
d
)
=>
dayjs
(
d
).
format
(
"YYYY-MM-DD"
)).
filter
(
Boolean
);
}
const
startedAt
=
item
.
startedAt
?
dayjs
(
item
.
startedAt
)
:
null
;
const
endedAt
=
item
.
endedAt
?
dayjs
(
item
.
endedAt
)
:
null
;
...
...
src/app/(main)/(home)/components/members/index.tsx
View file @
1a6e3d53
...
...
@@ -21,6 +21,7 @@ type FeaturedMember = {
id
:
string
;
name
:
string
;
avatar
?:
string
|
null
;
org_link
?:
string
|
null
;
};
type
FeaturedMembersResponse
=
{
...
...
@@ -46,6 +47,13 @@ const resolveMemberImage = (avatar: string | null | undefined, index: number) =>
return
memberImages
[
index
%
memberImages
.
length
]
??
"/img-error.png"
;
};
const
getMemberDetailUrl
=
(
orgLink
:
string
|
null
|
undefined
)
=>
{
if
(
orgLink
)
{
return
`
${
VCCI_HCM_ORIGIN
}
/giao-thuong-b2b/doanh-nghiep/
${
orgLink
}
`
;
}
return
null
;
};
function
Members
()
{
const
{
memberConnectionPosts
,
categoryLinks
,
categoryNames
}
=
useHomePosts
();
const
[
featuredMembers
,
setFeaturedMembers
]
=
useState
<
FeaturedMemberState
>
({
...
...
@@ -123,29 +131,58 @@ function Members() {
spaceBetween=
{
16
}
className=
"w-full"
>
{
displayMembers
.
map
((
member
,
index
)
=>
(
<
SwiperSlide
key=
{
member
.
id
}
className=
"!h-auto !w-full md:!w-[calc(50%-8px)] xl:!w-[calc(33.333%-10.67px)]"
>
<
article
className=
"rounded-[20px] bg-white p-[7px] shadow-[0_10px_22px_rgba(158,114,0,0.16)]"
>
<
div
className=
"flex h-[210px] items-center justify-center overflow-hidden rounded-[14px] bg-white px-4 py-5"
>
<
div
className=
"flex h-full w-full max-w-[260px] items-center justify-center"
>
<
ImageNext
src=
{
resolveMemberImage
(
member
.
avatar
,
index
)
}
alt=
{
member
.
name
}
width=
{
260
}
height=
{
180
}
className=
"h-[180px] w-[260px] max-w-full object-contain"
/>
</
div
>
</
div
>
<
h3
className=
"mt-3 line-clamp-2 min-h-[40px] px-1 text-center text-sm font-semibold leading-5 text-[#1e2f5e]"
>
{
member
.
name
}
</
h3
>
</
article
>
</
SwiperSlide
>
))
}
{
displayMembers
.
map
((
member
,
index
)
=>
{
const
detailUrl
=
getMemberDetailUrl
(
member
.
org_link
);
return
(
<
SwiperSlide
key=
{
member
.
id
}
className=
"!h-auto !w-full md:!w-[calc(50%-8px)] xl:!w-[calc(33.333%-10.67px)]"
>
<
article
className=
"rounded-[20px] bg-white p-[7px] shadow-[0_10px_22px_rgba(158,114,0,0.16)]"
>
{
detailUrl
?
(
<
a
href=
{
detailUrl
}
target=
"_blank"
rel=
"noreferrer"
className=
"block"
>
<
div
className=
"flex h-[210px] items-center justify-center overflow-hidden rounded-[14px] bg-white px-4 py-5"
>
<
div
className=
"flex h-full w-full max-w-[260px] items-center justify-center"
>
<
ImageNext
src=
{
resolveMemberImage
(
member
.
avatar
,
index
)
}
alt=
{
member
.
name
}
width=
{
260
}
height=
{
180
}
className=
"h-[180px] w-[260px] max-w-full object-contain"
/>
</
div
>
</
div
>
<
h3
className=
"mt-3 line-clamp-2 min-h-[40px] px-1 text-center text-sm font-semibold leading-5 text-[#1e2f5e] transition-colors hover:text-[#20449a]"
>
{
member
.
name
}
</
h3
>
</
a
>
)
:
(
<>
<
div
className=
"flex h-[210px] items-center justify-center overflow-hidden rounded-[14px] bg-white px-4 py-5"
>
<
div
className=
"flex h-full w-full max-w-[260px] items-center justify-center"
>
<
ImageNext
src=
{
resolveMemberImage
(
member
.
avatar
,
index
)
}
alt=
{
member
.
name
}
width=
{
260
}
height=
{
180
}
className=
"h-[180px] w-[260px] max-w-full object-contain"
/>
</
div
>
</
div
>
<
h3
className=
"mt-3 line-clamp-2 min-h-[40px] px-1 text-center text-sm font-semibold leading-5 text-[#1e2f5e]"
>
{
member
.
name
}
</
h3
>
</>
)
}
</
article
>
</
SwiperSlide
>
);
})
}
</
Swiper
>
);
};
...
...
src/app/(main)/(home)/lib/use-advertisements.ts
View file @
1a6e3d53
...
...
@@ -23,6 +23,7 @@ export function useAdvertisements(
limit
:
effectiveLimit
,
});
// Backend trả về { responseData: [...] }
return
(
(
data
as
unknown
as
{
responseData
?:
Advertisement
[]
}
|
undefined
)?.
responseData
??
[]
);
...
...
src/app/(main)/(home)/lib/use-home-posts.ts
View file @
1a6e3d53
...
...
@@ -40,6 +40,7 @@ type RawHomePost = {
is_active
?:
boolean
|
null
;
status
?:
string
|
null
;
type
?:
string
|
null
;
event_dates
?:
string
[]
|
null
;
categories
?:
RawHomeCategory
[]
|
null
;
thumbnail
?:
RawHomeThumbnail
|
null
;
content_structure
?:
{
...
...
@@ -84,6 +85,7 @@ export type HomePostItem = {
isActive
:
boolean
;
status
:
string
;
type
:
string
;
eventDates
?:
string
[];
categories
:
HomePostCategory
[];
thumbnail
:
{
url
:
string
;
...
...
@@ -416,6 +418,9 @@ async function fetchHomePostsFromApi() {
isActive
:
item
.
is_active
!==
false
,
status
:
String
(
item
.
status
??
""
),
type
:
String
(
item
.
type
??
""
),
eventDates
:
Array
.
isArray
(
item
.
event_dates
)
?
item
.
event_dates
.
filter
((
d
):
d
is
string
=>
typeof
d
===
"string"
)
:
[],
categories
,
thumbnail
:
thumbnailPath
?
{
...
...
@@ -642,6 +647,9 @@ export function useEventCalendarPosts(currentMonth: Date) {
isActive
:
item
.
is_active
!==
false
,
status
:
String
(
item
.
status
??
""
),
type
:
String
(
item
.
type
??
""
),
eventDates
:
Array
.
isArray
(
item
.
event_dates
)
?
item
.
event_dates
.
filter
((
d
):
d
is
string
=>
typeof
d
===
"string"
)
:
[],
categories
,
thumbnail
:
thumbnailPath
?
{
...
...
@@ -652,15 +660,16 @@ export function useEventCalendarPosts(currentMonth: Date) {
}
satisfies
HomePostItem
;
});
// Filter posts that have at least one date (
startedAt, endedAt, or registrationDeadline)
// falling within the current month
// Filter posts that have at least one date (
eventDates, startedAt,
//
endedAt, or registrationDeadline)
falling within the current month
return
mappedPosts
.
filter
((
item
)
=>
{
const
startedAt
=
item
.
startedAt
?
dayjs
(
item
.
startedAt
)
:
null
;
const
endedAt
=
item
.
endedAt
?
dayjs
(
item
.
endedAt
)
:
null
;
const
registrationDeadline
=
item
.
registrationDeadline
?
dayjs
(
item
.
registrationDeadline
)
:
null
;
const
eventDates
=
(
item
.
eventDates
??
[]).
map
((
d
)
=>
dayjs
(
d
)).
filter
((
d
)
=>
d
.
isValid
());
// If no dates at all, exclude
if
(
!
startedAt
&&
!
endedAt
&&
!
registrationDeadline
)
return
false
;
if
(
!
startedAt
&&
!
endedAt
&&
!
registrationDeadline
&&
eventDates
.
length
===
0
)
return
false
;
const
monthStartDay
=
dayjs
(
monthStart
);
const
monthEndDay
=
dayjs
(
monthEnd
);
...
...
@@ -670,6 +679,13 @@ export function useEventCalendarPosts(currentMonth: Date) {
return
date
!==
null
&&
!
date
.
isBefore
(
monthStartDay
,
"day"
)
&&
!
date
.
isAfter
(
monthEndDay
,
"day"
);
};
// If specific event_dates are set, only check those (they take
// priority over the started_at/ended_at range — same rule the
// calendar rendering uses in getEventDateRange).
if
(
eventDates
.
length
>
0
)
{
return
eventDates
.
some
((
d
)
=>
hasDateInMonth
(
d
));
}
// Check if event overlaps with current month (spans across the month)
const
eventStartDate
=
startedAt
||
registrationDeadline
;
const
eventEndDate
=
endedAt
||
registrationDeadline
||
startedAt
;
...
...
src/app/(main)/[...slug]/templates/data.ts
View file @
1a6e3d53
...
...
@@ -9,6 +9,7 @@ import type {
DynamicPostContentSection
,
DynamicPostItem
,
DynamicPostThumbnail
,
DynamicPostUser
,
}
from
"./types"
;
type
CategoryListResponse
=
{
...
...
@@ -31,6 +32,16 @@ type RawPostThumbnail = {
url
?:
string
|
null
;
};
type
RawPostUser
=
{
id
?:
string
|
null
;
email
?:
string
|
null
;
username
?:
string
|
null
;
first_name
?:
string
|
null
;
last_name
?:
string
|
null
;
full_name
?:
string
|
null
;
avatar_url
?:
string
|
null
;
}
|
null
;
type
RawPostSectionImage
=
{
position
?:
number
|
null
;
image
?:
{
...
...
@@ -66,6 +77,8 @@ type RawPostItem = {
type
?:
string
|
null
;
thumbnail
?:
RawPostThumbnail
|
null
;
categories
?:
RawPostCategory
[]
|
null
;
creator
?:
RawPostUser
;
editor
?:
RawPostUser
;
content_structure
?:
{
post_content
?:
Array
<
{
id
?:
string
|
null
;
...
...
@@ -205,6 +218,26 @@ const mapPostContentSections = (item: RawPostItem): DynamicPostContentSection[]
}));
};
const
mapPostUser
=
(
user
:
RawPostUser
|
undefined
):
DynamicPostUser
=>
{
if
(
!
user
||
typeof
user
!==
"object"
)
return
null
;
const
firstName
=
String
(
user
.
first_name
??
""
).
trim
();
const
lastName
=
String
(
user
.
last_name
??
""
).
trim
();
const
fullName
=
String
(
user
.
full_name
??
""
).
trim
()
||
[
firstName
,
lastName
].
filter
(
Boolean
).
join
(
" "
).
trim
()
||
String
(
user
.
username
??
""
).
trim
()
||
String
(
user
.
email
??
""
).
trim
();
return
{
id
:
String
(
user
.
id
??
""
),
email
:
String
(
user
.
email
??
""
),
username
:
user
.
username
?
String
(
user
.
username
)
:
null
,
first_name
:
firstName
||
null
,
last_name
:
lastName
||
null
,
full_name
:
fullName
,
avatar_url
:
user
.
avatar_url
?
String
(
user
.
avatar_url
)
:
null
,
};
};
const
mapPost
=
(
item
:
RawPostItem
):
DynamicPostItem
=>
({
id
:
String
(
item
.
id
??
""
),
title
:
String
(
item
.
title
??
""
).
trim
(),
...
...
@@ -239,6 +272,8 @@ const mapPost = (item: RawPostItem): DynamicPostItem => ({
content_structure
:
{
post_content
:
mapPostContentSections
(
item
),
},
creator
:
mapPostUser
(
item
.
creator
),
editor
:
mapPostUser
(
item
.
editor
),
});
const
buildPostFilters
=
(
filters
:
Array
<
string
|
null
|
undefined
>
)
=>
...
...
src/app/(main)/[...slug]/templates/types.ts
View file @
1a6e3d53
...
...
@@ -30,6 +30,16 @@ export type DynamicPostThumbnail = {
url
?:
string
|
null
;
}
|
null
;
export
type
DynamicPostUser
=
{
id
:
string
;
email
:
string
;
username
:
string
|
null
;
first_name
:
string
|
null
;
last_name
:
string
|
null
;
full_name
:
string
;
avatar_url
:
string
|
null
;
}
|
null
;
export
type
DynamicPostContentSection
=
{
id
:
string
;
type
:
string
;
...
...
@@ -76,4 +86,6 @@ export type DynamicPostItem = {
content_structure
:
{
post_content
:
DynamicPostContentSection
[];
}
|
null
;
creator
:
DynamicPostUser
;
editor
:
DynamicPostUser
;
};
src/app/admin/news/page.tsx
View file @
1a6e3d53
...
...
@@ -47,6 +47,7 @@ import {
fetchCmsNewsItems
,
fetchCmsPostCount
,
fetchHeaderConfigItems
,
toggleCmsNewsVisibility
,
}
from
"@/lib/api/cms-admin"
;
import
{
ChevronLeft
,
ChevronRight
}
from
"lucide-react"
;
import
{
...
...
@@ -239,6 +240,7 @@ export default function AdminNewsPage() {
const
[
deleteTarget
,
setDeleteTarget
]
=
React
.
useState
<
AdminNewsItem
|
null
>
(
null
);
const
[
ready
,
setReady
]
=
React
.
useState
(
false
);
const
[
isDeleting
,
setIsDeleting
]
=
React
.
useState
(
false
);
const
[
togglingVisibilityId
,
setTogglingVisibilityId
]
=
React
.
useState
<
string
|
null
>
(
null
);
const
[
page
,
setPage
]
=
React
.
useState
(()
=>
{
const
parsedPage
=
Number
(
searchParams
.
get
(
"page"
)
??
1
);
return
Number
.
isFinite
(
parsedPage
)
&&
parsedPage
>
0
?
Math
.
floor
(
parsedPage
)
:
1
;
...
...
@@ -450,6 +452,25 @@ export default function AdminNewsPage() {
}
};
const
handleToggleVisibility
=
async
(
item
:
AdminNewsItem
)
=>
{
if
(
togglingVisibilityId
)
return
;
const
nextIsHidden
=
!
item
.
is_hidden
;
setTogglingVisibilityId
(
item
.
id
);
try
{
await
toggleCmsNewsVisibility
(
item
.
id
,
nextIsHidden
);
toast
.
success
(
nextIsHidden
?
"Đã ẩn bài viết"
:
"Đã hiển thị bài viết"
);
await
Promise
.
all
([
load
(),
loadStats
()]);
}
catch
(
error
)
{
toast
.
error
(
error
instanceof
Error
?
error
.
message
:
"Không thể thay đổi trạng thái hiển thị"
,
);
}
finally
{
setTogglingVisibilityId
(
null
);
}
};
const
totalPages
=
Math
.
ceil
(
total
/
pageSize
);
const
handlePageChange
=
(
newPage
:
number
)
=>
{
...
...
@@ -528,17 +549,17 @@ export default function AdminNewsPage() {
<
TableHead
className=
"w-[140px] py-4 text-center text-white"
>
Hình ảnh đại diện
</
TableHead
>
<
TableHead
className=
"w-40 py-4 text-center text-white"
>
Loại bài viết
</
TableHead
>
<
TableHead
className=
"w-[190px] py-4 text-center text-white"
>
Danh mục hiển thị
<
TableHead
className=
"w-[220px] py-4 text-center text-white"
>
Loại / Danh mục
</
TableHead
>
<
TableHead
className=
"w-[170px] py-4 text-center text-white"
>
Ngày xuất bản
Ngày xuất bản
/ Hết hạn
</
TableHead
>
<
TableHead
className=
"w-[170px] py-4 text-center text-white"
>
Ngày hết hạn
<
TableHead
className=
"w-[150px] py-4 text-center text-white"
>
Người tạo
</
TableHead
>
<
TableHead
className=
"w-[150px] py-4 text-center text-white"
>
Cập nhật bởi
</
TableHead
>
<
TableHead
className=
"w-[130px] py-4 text-center text-white"
>
Thao tác
...
...
@@ -598,31 +619,64 @@ export default function AdminNewsPage() {
</
TableCell
>
<
TableCell
className=
"text-center"
>
<
Badge
variant=
"outline"
className=
"border-[#063e8e]/25 text-[#063e8e]"
>
{
ADMIN_NEWS_TYPE_LABELS
[
item
.
type
]
}
</
Badge
>
<
div
className=
"flex flex-col items-center gap-1"
>
<
Badge
variant=
"outline"
className=
"border-[#063e8e]/25 text-[#063e8e]"
>
{
ADMIN_NEWS_TYPE_LABELS
[
item
.
type
]
}
</
Badge
>
<
span
className=
"text-sm text-gray-700"
>
{
primaryCategoryName
}
{
extraCategoryCount
>
0
?
` (+${extraCategoryCount})`
:
""
}
</
span
>
</
div
>
</
TableCell
>
<
TableCell
className=
"text-center text-sm text-gray-700"
>
{
primaryCategoryName
}
{
extraCategoryCount
>
0
?
` (+${extraCategoryCount})`
:
""
}
<
div
className=
"flex flex-col gap-0.5"
>
<
span
>
{
formatDateTime
(
item
.
published_at
)
||
"—"
}
</
span
>
<
span
className=
"text-gray-500"
>
{
formatDateTime
(
item
.
expired_at
)
||
"—"
}
</
span
>
</
div
>
</
TableCell
>
<
TableCell
className=
"text-center text-sm text-gray-700"
>
{
formatDateTime
(
item
.
published_at
)
}
{
item
.
creator
?
(
<
div
className=
"flex flex-col gap-0.5"
>
<
span
className=
"font-medium text-[#1f3768]"
>
{
item
.
creator
.
full_name
}
</
span
>
<
span
className=
"text-gray-500"
>
{
formatDateTime
(
item
.
created_at
)
||
"—"
}
</
span
>
</
div
>
)
:
(
<
span
className=
"text-gray-400"
>
—
</
span
>
)
}
</
TableCell
>
<
TableCell
className=
"text-center text-sm text-gray-700"
>
{
formatDateTime
(
item
.
expired_at
)
}
{
item
.
editor
&&
item
.
editor
.
id
?
(
<
div
className=
"flex flex-col gap-0.5"
>
<
span
className=
"font-medium text-[#1f3768]"
>
{
item
.
editor
.
full_name
}
</
span
>
<
span
className=
"text-gray-500"
>
{
formatDateTime
(
item
.
updated_at
)
||
"—"
}
</
span
>
</
div
>
)
:
(
<
span
className=
"text-gray-400"
>
—
</
span
>
)
}
</
TableCell
>
<
TableCell
className=
"text-center"
>
<
AdminRowActions
actions=
{
[
{
kind
:
item
.
is_hidden
?
"hidden"
:
"visible"
,
label
:
item
.
is_hidden
?
"B
\
u00e0i vi
\
u1ebft
\
u0111ang
\
u1ea9n"
:
"B
\
u00e0i vi
\
u1ebft
\
u0111ang hi
\
u1ec3n th
\
u1ecb"
,
?
"B
\
u00e0i vi
\
u1ebft
\
u0111ang
\
u1ea9n, b
\
u1ea5m
\
u0111
\
u1ec3 hi
\
u1ec3n th
\
u1ecb"
:
"B
\
u00e0i vi
\
u1ebft
\
u0111ang hi
\
u1ec3n th
\
u1ecb, b
\
u1ea5m
\
u0111
\
u1ec3
\
u1ea9n"
,
disabled
:
togglingVisibilityId
===
item
.
id
,
onClick
:
()
=>
void
handleToggleVisibility
(
item
),
},
{
kind
:
"edit"
,
...
...
src/app/admin/users/page.tsx
View file @
1a6e3d53
...
...
@@ -71,6 +71,7 @@ import {
usePatchApiV10UserIdStatus
,
usePostApiV10UserIdResetPassword
,
usePostApiV10UserIdRole
,
useDeleteApiV10UserIdRole
,
useGetApiV10UserId
,
usePutApiV10UserId
,
putApiV10UserId
,
...
...
@@ -234,6 +235,7 @@ export default function UsersPage() {
const
toggleStatusMutation
=
usePatchApiV10UserIdStatus
();
const
resetPasswordMutation
=
usePostApiV10UserIdResetPassword
();
const
assignRoleMutation
=
usePostApiV10UserIdRole
();
const
removeRoleMutation
=
useDeleteApiV10UserIdRole
();
// Data
const
roles
:
Role
[]
=
((
rolesData
as
unknown
as
{
responseData
?:
{
rows
?:
Role
[]
}
})?.
responseData
?.
rows
)
||
[];
...
...
@@ -373,16 +375,64 @@ export default function UsersPage() {
}
try
{
const
primaryRole
=
roles
.
find
((
r
)
=>
r
.
name
===
userRoles
[
0
]);
if
(
primaryRole
)
{
// Compute diff between current roles (from selectedUser) and new
// selection (userRoles). Both arrays hold role *names*.
const
currentRoleNames
=
selectedUser
.
roles
??
[];
const
currentRoleSet
=
new
Set
(
currentRoleNames
);
const
newRoleSet
=
new
Set
(
userRoles
);
// Roles to remove: in current but not in new
const
rolesToRemove
=
currentRoleNames
.
filter
(
(
name
)
=>
!
newRoleSet
.
has
(
name
),
);
// Roles to add: in new but not in current
const
rolesToAdd
=
userRoles
.
filter
(
(
name
)
=>
!
currentRoleSet
.
has
(
name
),
);
// Resolve role IDs from name
const
roleByName
=
new
Map
(
roles
.
map
((
r
)
=>
[
r
.
name
,
r
]));
// 1. Remove roles no longer selected
for
(
const
roleName
of
rolesToRemove
)
{
const
role
=
roleByName
.
get
(
roleName
);
if
(
!
role
)
continue
;
await
removeRoleMutation
.
mutateAsync
({
id
:
selectedUser
.
id
,
data
:
{
role_id
:
role
.
id
},
});
}
// 2. Add new roles. userRoles[0] is treated as primary.
// Non-primary roles get is_primary=false.
for
(
let
i
=
0
;
i
<
rolesToAdd
.
length
;
i
++
)
{
const
role
=
roleByName
.
get
(
rolesToAdd
[
i
]);
if
(
!
role
)
continue
;
await
assignRoleMutation
.
mutateAsync
({
id
:
selectedUser
.
id
,
data
:
{
role_id
:
primaryR
ole
.
id
,
is_primary
:
true
,
role_id
:
r
ole
.
id
,
is_primary
:
i
===
0
,
},
});
}
// 3. If no roles were added but primary changed (e.g. user reordered
// existing roles), still POST the first selected role with
// is_primary=true. Backend will update is_primary without 409.
if
(
rolesToAdd
.
length
===
0
&&
rolesToRemove
.
length
===
0
)
{
const
primaryRole
=
roleByName
.
get
(
userRoles
[
0
]);
if
(
primaryRole
)
{
await
assignRoleMutation
.
mutateAsync
({
id
:
selectedUser
.
id
,
data
:
{
role_id
:
primaryRole
.
id
,
is_primary
:
true
,
},
});
}
}
toast
.
success
(
"Cập nhật vai trò thành công!"
);
setIsRoleDialogOpen
(
false
);
queryClient
.
invalidateQueries
({
queryKey
:
[
"/api/v1.0/user"
],
exact
:
false
});
...
...
@@ -978,7 +1028,7 @@ export default function UsersPage() {
</
Button
>
<
Button
onClick=
{
handleSaveRoles
}
disabled=
{
userRoles
.
length
===
0
||
assignRoleMutation
.
isPending
}
disabled=
{
userRoles
.
length
===
0
||
assignRoleMutation
.
isPending
||
removeRoleMutation
.
isPending
}
className=
"h-10 rounded-xl bg-[#063e8e] text-white hover:bg-[#063e8e]/90"
>
{
assignRoleMutation
.
isPending
&&
<
Loader2
className=
"mr-2 h-4 w-4 animate-spin"
/>
}
...
...
src/app/api/featured-members/route.ts
View file @
1a6e3d53
...
...
@@ -4,6 +4,12 @@ import { MOCK_FEATURED_MEMBERS_RESPONSE } from "@/app/api/mock-data";
const
BACKEND_HOST
=
process
.
env
.
NEXT_PUBLIC_BACKEND_HOST
||
"https://news.vccihcm.vn"
;
const
FEATURED_MEMBER_API_URL
=
`
${
BACKEND_HOST
.
replace
(
/
\/
+$/
,
""
)}
/api/v1.0/vcci/featured-members`
;
// Fallback: gọi trực tiếp VCCI HCM API
const
FALLBACK_FEATURED_MEMBER_API_URL
=
"https://vccihcm.vn/api/v1.0/organizations"
+
"?filters=users.status_id+%3D%3D+36ca1cc5-7b6e-4f9f-b973-69c5207deb62"
+
"&pageSize=12&sortField=created_at&sortOrder=ASC"
;
export
async
function
GET
()
{
try
{
const
response
=
await
fetch
(
FEATURED_MEMBER_API_URL
,
{
...
...
@@ -12,17 +18,40 @@ export async function GET() {
});
if
(
!
response
.
ok
)
{
// Khi upstream lỗi, vẫn trả mock để FE không hiển thị "Chưa có thông tin".
console
.
warn
(
`[api/featured-members] upstream returned
${
response
.
status
}
,
serving mock data
`
,
`[api/featured-members] upstream returned
${
response
.
status
}
,
trying fallback
`
,
);
return
NextResponse
.
json
(
MOCK_FEATURED_MEMBERS_RESPONSE
,
{
status
:
200
});
// Thử fallback - gọi trực tiếp VCCI HCM API
return
tryFallback
();
}
const
data
=
await
response
.
json
();
return
NextResponse
.
json
(
data
);
}
catch
(
error
)
{
console
.
error
(
"[api/featured-members] upstream failed, serving mock data"
,
error
);
console
.
error
(
"[api/featured-members] upstream failed, trying fallback"
,
error
);
return
tryFallback
();
}
}
async
function
tryFallback
()
{
try
{
const
fallbackResponse
=
await
fetch
(
FALLBACK_FEATURED_MEMBER_API_URL
,
{
headers
:
{
Accept
:
"application/json"
},
next
:
{
revalidate
:
300
},
});
if
(
!
fallbackResponse
.
ok
)
{
console
.
warn
(
`[api/featured-members] fallback returned
${
fallbackResponse
.
status
}
, serving mock data`
,
);
return
NextResponse
.
json
(
MOCK_FEATURED_MEMBERS_RESPONSE
,
{
status
:
200
});
}
const
data
=
await
fallbackResponse
.
json
();
console
.
log
(
"[api/featured-members] served from fallback"
);
return
NextResponse
.
json
(
data
);
}
catch
(
error
)
{
console
.
error
(
"[api/featured-members] fallback failed, serving mock data"
,
error
);
return
NextResponse
.
json
(
MOCK_FEATURED_MEMBERS_RESPONSE
,
{
status
:
200
});
}
}
src/app/api/partners/route.ts
View file @
1a6e3d53
...
...
@@ -4,6 +4,12 @@ import { MOCK_PARTNERS_RESPONSE } from "@/app/api/mock-data";
const
BACKEND_HOST
=
process
.
env
.
NEXT_PUBLIC_BACKEND_HOST
||
"https://news.vccihcm.vn"
;
const
PARTNER_API_URL
=
`
${
BACKEND_HOST
.
replace
(
/
\/
+$/
,
""
)}
/api/v1.0/vcci/partners`
;
// Fallback: gọi trực tiếp VCCI HCM API
const
FALLBACK_PARTNER_API_URL
=
"https://vccihcm.vn/api/v1.0/organizations"
+
"?filters=type%3D%3DSPONSOR&pageSize=12"
+
"&sortField=sort_order&sortOrder=ASC"
;
export
async
function
GET
()
{
try
{
const
response
=
await
fetch
(
PARTNER_API_URL
,
{
...
...
@@ -12,17 +18,40 @@ export async function GET() {
});
if
(
!
response
.
ok
)
{
// Khi upstream lỗi, vẫn trả mock để FE không hiển thị "Chưa có thông tin".
console
.
warn
(
`[api/partners] upstream returned
${
response
.
status
}
,
serving mock data
`
,
`[api/partners] upstream returned
${
response
.
status
}
,
trying fallback
`
,
);
return
NextResponse
.
json
(
MOCK_PARTNERS_RESPONSE
,
{
status
:
200
});
// Thử fallback - gọi trực tiếp VCCI HCM API
return
tryFallback
();
}
const
data
=
await
response
.
json
();
return
NextResponse
.
json
(
data
);
}
catch
(
error
)
{
console
.
error
(
"[api/partners] upstream failed, serving mock data"
,
error
);
console
.
error
(
"[api/partners] upstream failed, trying fallback"
,
error
);
return
tryFallback
();
}
}
async
function
tryFallback
()
{
try
{
const
fallbackResponse
=
await
fetch
(
FALLBACK_PARTNER_API_URL
,
{
headers
:
{
Accept
:
"application/json"
},
next
:
{
revalidate
:
300
},
});
if
(
!
fallbackResponse
.
ok
)
{
console
.
warn
(
`[api/partners] fallback returned
${
fallbackResponse
.
status
}
, serving mock data`
,
);
return
NextResponse
.
json
(
MOCK_PARTNERS_RESPONSE
,
{
status
:
200
});
}
const
data
=
await
fallbackResponse
.
json
();
console
.
log
(
"[api/partners] served from fallback"
);
return
NextResponse
.
json
(
data
);
}
catch
(
error
)
{
console
.
error
(
"[api/partners] fallback failed, serving mock data"
,
error
);
return
NextResponse
.
json
(
MOCK_PARTNERS_RESPONSE
,
{
status
:
200
});
}
}
src/components/admin/news-form.tsx
View file @
1a6e3d53
...
...
@@ -2,15 +2,18 @@
import
*
as
React
from
"react"
;
import
dayjs
from
"dayjs"
;
import
{
ArrowLeft
,
Check
,
ChevronsUpDown
,
Save
,
Upload
,
X
}
from
"lucide-react"
;
import
{
ArrowLeft
,
Check
,
ChevronsUpDown
,
Save
,
Upload
,
X
,
Calendar
as
CalendarIcon
,
Plus
}
from
"lucide-react"
;
import
Link
from
"next/link"
;
import
{
useRouter
}
from
"next/navigation"
;
import
{
toast
}
from
"sonner"
;
import
{
AdminImagePicker
}
from
"@/components/admin/image-picker"
;
import
{
AdminPostContentEditor
}
from
"@/components/admin/post-content-editor"
;
import
{
PostHistoryViewer
}
from
"@/components/admin/post-history-viewer"
;
import
{
AdminRichTextEditor
}
from
"@/components/admin/rich-text-editor"
;
import
{
SafeNextImage
}
from
"@/components/admin/safe-next-image"
;
import
{
PermissionGate
}
from
"@/components/shared/permission-gate"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Calendar
}
from
"@/components/ui/calendar"
;
import
{
Checkbox
}
from
"@/components/ui/checkbox"
;
import
{
Command
,
...
...
@@ -399,6 +402,10 @@ export function AdminNewsForm({
const
[
isSubmitting
,
setIsSubmitting
]
=
React
.
useState
(
false
);
const
[
isLoadingInitialData
,
setIsLoadingInitialData
]
=
React
.
useState
(
true
);
const
[
isMissingPost
,
setIsMissingPost
]
=
React
.
useState
(
false
);
// Toggle state for "Hiển thị các ngày cụ thể trên lịch" — independent of
// whether event_dates array is empty, so the date picker section stays
// visible after enabling (even before any dates are added).
const
[
useEventDates
,
setUseEventDates
]
=
React
.
useState
(
false
);
React
.
useEffect
(()
=>
{
let
cancelled
=
false
;
...
...
@@ -440,6 +447,7 @@ export function AdminNewsForm({
created_at
:
now
,
updated_at
:
now
,
});
setUseEventDates
(
false
);
return
;
}
...
...
@@ -483,6 +491,8 @@ export function AdminNewsForm({
nextForm
.
header_category_id
=
nextForm
.
category_ids
[
0
]
??
""
;
}
setForm
(
nextForm
);
// Enable the "specific dates" toggle if the post already has event_dates
setUseEventDates
((
nextForm
.
event_dates
??
[]).
length
>
0
);
}
catch
(
error
)
{
if
(
cancelled
)
return
;
toast
.
error
(
error
instanceof
Error
?
error
.
message
:
"Không thể tải bài viết"
);
...
...
@@ -735,6 +745,7 @@ export function AdminNewsForm({
registration_deadline
:
form
.
registration_deadline
||
null
,
location
:
form
.
location
.
trim
(),
participation_fee
:
form
.
participation_fee
.
trim
(),
event_dates
:
(
form
.
event_dates
??
[]).
length
>
0
?
form
.
event_dates
:
null
,
post_content
:
form
.
post_content
.
map
((
section
,
index
)
=>
({
...
section
,
position
:
index
+
1
,
...
...
@@ -1089,7 +1100,65 @@ export function AdminNewsForm({
title=
"Thông tin sự kiện (tùy chọn)"
description=
"Nhóm các trường dành cho bài viết có tính chất sự kiện hoặc chương trình."
>
<
div
className=
"rounded-xl border border-[#063e8e]/15 p-4"
>
<
div
className=
"rounded-xl border border-[#063e8e]/15 p-4 space-y-4"
>
{
/* Toggle: Sử dụng ngày cụ thể */
}
<
div
className=
"flex items-center gap-3 rounded-lg border border-[#063e8e]/10 bg-[#063e8e]/5 p-3"
>
<
Checkbox
id=
"use-event-dates"
checked=
{
useEventDates
}
onCheckedChange=
{
(
checked
)
=>
{
setUseEventDates
(
checked
===
true
);
if
(
!
checked
)
{
// Tắt: xóa hết event_dates
handleField
(
"event_dates"
,
[]);
}
}
}
className=
"border-[#063e8e]/30 data-[state=checked]:border-[#063e8e] data-[state=checked]:bg-[#063e8e]"
/>
<
div
className=
"flex-1"
>
<
Label
htmlFor=
"use-event-dates"
className=
"cursor-pointer text-sm font-medium text-gray-700"
>
Hiển thị các ngày cụ thể trên lịch
</
Label
>
<
p
className=
"text-xs text-gray-500"
>
Thay vì hiển thị tất cả ngày từ bắt đầu đến kết thúc, chỉ hiển thị những ngày bạn chọn bên dưới
</
p
>
</
div
>
</
div
>
{
/* Ngày cụ thể - chỉ hiển thị khi được bật */
}
{
useEventDates
&&
(
<
div
className=
"space-y-2"
>
<
Label
className=
"block text-sm font-medium text-gray-700"
>
Các ngày cụ thể (
{
(
form
.
event_dates
??
[]).
length
}
ngày)
</
Label
>
<
div
className=
"flex flex-wrap gap-2"
>
{
(
form
.
event_dates
??
[]).
map
((
date
,
index
)
=>
(
<
div
key=
{
date
}
className=
"flex items-center gap-1 rounded-lg bg-[#063e8e]/10 px-3 py-1.5 text-sm text-[#063e8e]"
>
<
CalendarIcon
className=
"h-3.5 w-3.5"
/>
<
span
>
{
dayjs
(
date
).
format
(
"DD/MM/YYYY"
)
}
</
span
>
<
button
type=
"button"
onClick=
{
()
=>
{
const
newDates
=
(
form
.
event_dates
??
[]).
filter
((
_
,
i
)
=>
i
!==
index
);
handleField
(
"event_dates"
,
newDates
);
}
}
className=
"ml-1 rounded-full p-0.5 hover:bg-[#063e8e]/20"
>
<
X
className=
"h-3 w-3"
/>
</
button
>
</
div
>
))
}
</
div
>
<
EventDatesDatePicker
value=
{
form
.
event_dates
??
[]
}
onChange=
{
(
dates
)
=>
handleField
(
"event_dates"
,
dates
)
}
/>
</
div
>
)
}
<
div
className=
"grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3"
>
<
div
>
<
Label
className=
"mb-1.5 block text-gray-700"
>
Ngày bắt đầu
</
Label
>
...
...
@@ -1199,6 +1268,108 @@ export function AdminNewsForm({
onOpenChange=
{
setPickerOpen
}
onSelect=
{
handleThumbnailSelect
}
/>
{
!
isCreate
&&
newsId
&&
(
<
PermissionGate
required=
"posts:read"
>
<
PostHistoryViewer
postId=
{
newsId
}
/>
</
PermissionGate
>
)
}
</
div
>
);
}
function EventDatesDatePicker(
{
value
,
onChange
,
}
:
{
value
:
string
[];
onChange
:
(
dates
:
string
[])
=>
void
;
}
)
{
const
[
popoverOpen
,
setPopoverOpen
]
=
React
.
useState
(
false
);
// Convert stored "YYYY-MM-DD" strings to Date objects for react-day-picker
const
selectedDates
=
React
.
useMemo
(
()
=>
value
.
map
((
d
)
=>
dayjs
(
d
).
toDate
()).
filter
((
d
)
=>
!
Number
.
isNaN
(
d
.
getTime
())),
[
value
],
);
const
handleMultipleSelect
=
(
dates
:
Date
[]
|
undefined
)
=>
{
if
(
!
dates
)
{
onChange
([]);
return
;
}
const
newDates
=
Array
.
from
(
new
Set
(
dates
.
map
((
d
)
=>
dayjs
(
d
).
format
(
"YYYY-MM-DD"
))),
).
sort
();
onChange
(
newDates
);
};
return
(
<
Popover
open=
{
popoverOpen
}
onOpenChange=
{
setPopoverOpen
}
>
<
PopoverTrigger
asChild
>
<
Button
type=
"button"
variant=
"outline"
className=
"border-[#063e8e]/15 bg-white text-gray-700 hover:bg-[#063e8e]/10 hover:text-[#063e8e]"
>
<
Plus
className=
"mr-2 h-4 w-4"
/>
Thêm ngày
{
value
.
length
>
0
?
` (${value.length})`
:
""
}
</
Button
>
</
PopoverTrigger
>
<
PopoverContent
className=
"w-fit p-0"
align=
"start"
>
<
div
className=
"p-4"
>
<
div
className=
"mb-3 flex items-center justify-between gap-2 border-b border-gray-100 pb-3"
>
<
span
className=
"text-base font-semibold text-[#063e8e]"
>
{
value
.
length
>
0
?
`Đã chọn ${value.length} ngày`
:
"Chọn các ngày cụ thể"
}
</
span
>
{
value
.
length
>
0
&&
(
<
Button
type=
"button"
variant=
"ghost"
size=
"sm"
className=
"h-8 px-3 text-sm text-red-600 hover:bg-red-50 hover:text-red-700"
onClick=
{
()
=>
onChange
([])
}
>
Xóa tất cả
</
Button
>
)
}
</
div
>
<
Calendar
mode=
"multiple"
selected=
{
selectedDates
}
onSelect=
{
handleMultipleSelect
}
className=
"w-full [--cell-size:3.5rem]"
classNames=
{
{
root
:
"w-full"
,
month
:
"flex w-full flex-col gap-4"
,
month_caption
:
"flex h-12 w-full items-center justify-center px-2 text-xl font-bold text-[#063e8e]"
,
nav
:
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1"
,
button_previous
:
"h-12 w-12 select-none p-0 text-[#063e8e] hover:bg-[#063e8e]/10 aria-disabled:opacity-50 [&>svg]:size-6"
,
button_next
:
"h-12 w-12 select-none p-0 text-[#063e8e] hover:bg-[#063e8e]/10 aria-disabled:opacity-50 [&>svg]:size-6"
,
weekday
:
"flex-1 select-none rounded-md text-sm font-semibold uppercase text-gray-400"
,
day
:
"group/day relative aspect-square h-full w-full select-none p-0 text-center text-lg"
,
today
:
"ring-2 ring-[#063e8e]/40 rounded-full bg-[#063e8e]/5 text-[#063e8e] font-semibold"
,
outside
:
"text-gray-300"
,
}
}
/>
<
div
className=
"mt-3 flex items-center justify-between gap-4 border-t border-gray-100 pt-3"
>
<
span
className=
"text-sm text-gray-400"
>
Click ngày để chọn / bỏ chọn
</
span
>
<
Button
type=
"button"
variant=
"default"
size=
"default"
className=
"bg-[#063e8e] hover:bg-[#063e8e]/90"
onClick=
{
()
=>
setPopoverOpen
(
false
)
}
>
Xong
</
Button
>
</
div
>
</
div
>
</
PopoverContent
>
</
Popover
>
);
}
src/components/admin/post-history-viewer.tsx
0 → 100644
View file @
1a6e3d53
"use client"
;
import
*
as
React
from
"react"
;
import
{
Clock
,
ChevronDown
,
ChevronUp
,
User
,
Plus
,
Pencil
,
Trash2
,
Loader2
}
from
"lucide-react"
;
import
{
useGetApiV10PostIdHistory
}
from
"@/api/endpoints/post-history"
;
import
type
{
PostHistoryItem
,
PostHistoryAction
}
from
"@/api/types/post-history"
;
import
{
cn
}
from
"@/lib/utils"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Collapsible
,
CollapsibleContent
,
CollapsibleTrigger
}
from
"@/components/ui/collapsible"
;
const
ACTION_CONFIG
:
Record
<
PostHistoryAction
,
{
label
:
string
;
icon
:
React
.
ComponentType
<
{
className
?:
string
}
>
;
className
:
string
}
>
=
{
CREATE
:
{
label
:
"Tạo bài viết"
,
icon
:
Plus
,
className
:
"bg-green-100 text-green-700 border-green-200"
,
},
UPDATE
:
{
label
:
"Cập nhật"
,
icon
:
Pencil
,
className
:
"bg-blue-100 text-blue-700 border-blue-200"
,
},
DELETE
:
{
label
:
"Xóa bài viết"
,
icon
:
Trash2
,
className
:
"bg-red-100 text-red-700 border-red-200"
,
},
};
function
formatDate
(
dateString
:
string
)
{
const
date
=
new
Date
(
dateString
);
return
date
.
toLocaleString
(
"vi-VN"
,
{
day
:
"2-digit"
,
month
:
"2-digit"
,
year
:
"numeric"
,
hour
:
"2-digit"
,
minute
:
"2-digit"
,
});
}
function
formatFieldName
(
field
:
string
):
string
{
const
fieldMap
:
Record
<
string
,
string
>
=
{
title
:
"Tiêu đề"
,
content
:
"Nội dung"
,
summary
:
"Tóm tắt"
,
status
:
"Trạng thái"
,
is_hidden
:
"Ẩn/Hiện"
,
is_featured
:
"Tin nổi bật"
,
thumbnail_id
:
"Hình đại diện"
,
slug
:
"Slug"
,
published_at
:
"Ngày xuất bản"
,
expired_at
:
"Ngày hết hạn"
,
external_link
:
"Liên kết ngoài"
,
};
return
fieldMap
[
field
]
||
field
;
}
function
formatValue
(
value
:
unknown
):
string
{
if
(
value
===
null
||
value
===
undefined
)
return
"(trống)"
;
if
(
typeof
value
===
"boolean"
)
return
value
?
"Có"
:
"Không"
;
if
(
typeof
value
===
"object"
)
{
try
{
return
JSON
.
stringify
(
value
);
}
catch
{
return
String
(
value
);
}
}
return
String
(
value
);
}
function
HistoryItem
({
item
}:
{
item
:
PostHistoryItem
})
{
const
config
=
ACTION_CONFIG
[
item
.
action
];
const
Icon
=
config
.
icon
;
const
[
isOpen
,
setIsOpen
]
=
React
.
useState
(
item
.
action
===
"CREATE"
);
return
(
<
div
className=
"rounded-lg border border-gray-200 bg-white"
>
<
div
className=
"flex cursor-pointer items-center justify-between p-3 hover:bg-gray-50"
onClick=
{
()
=>
setIsOpen
(
!
isOpen
)
}
>
<
div
className=
"flex items-center gap-3"
>
<
div
className=
{
cn
(
"flex h-8 w-8 items-center justify-center rounded-full"
,
config
.
className
)
}
>
<
Icon
className=
"h-4 w-4"
/>
</
div
>
<
div
>
<
div
className=
"flex items-center gap-2"
>
<
span
className=
"font-medium text-gray-900"
>
{
config
.
label
}
</
span
>
{
item
.
actor
&&
(
<
span
className=
"text-sm text-gray-500"
>
bởi
<
span
className=
"font-medium"
>
{
item
.
actor
.
full_name
||
item
.
actor
.
email
}
</
span
>
</
span
>
)
}
</
div
>
<
p
className=
"text-sm text-gray-500"
>
{
formatDate
(
item
.
created_at
)
}
</
p
>
</
div
>
</
div
>
{
isOpen
?
(
<
ChevronUp
className=
"h-5 w-5 text-gray-400"
/>
)
:
(
<
ChevronDown
className=
"h-5 w-5 text-gray-400"
/>
)
}
</
div
>
{
isOpen
&&
(
<
div
className=
"border-t border-gray-100 p-3"
>
{
item
.
action
===
"UPDATE"
&&
item
.
changes
&&
Object
.
keys
(
item
.
changes
).
length
>
0
?
(
<
div
className=
"space-y-2"
>
<
p
className=
"text-sm font-medium text-gray-700"
>
Các thay đổi:
</
p
>
{
Object
.
entries
(
item
.
changes
).
map
(([
field
,
{
old
:
oldVal
,
new
:
newVal
}])
=>
(
<
div
key=
{
field
}
className=
"grid grid-cols-[120px_1fr_1fr] items-center gap-2 text-sm"
>
<
span
className=
"font-medium text-gray-600"
>
{
formatFieldName
(
field
)
}
:
</
span
>
<
div
className=
"rounded bg-red-50 px-2 py-1 text-red-700 line-through"
>
{
formatValue
(
oldVal
)
}
</
div
>
<
div
className=
"rounded bg-green-50 px-2 py-1 text-green-700"
>
{
formatValue
(
newVal
)
}
</
div
>
</
div
>
))
}
</
div
>
)
:
item
.
action
===
"DELETE"
&&
item
.
snapshot
?
(
<
div
className=
"space-y-1 text-sm text-gray-600"
>
<
p
className=
"font-medium text-gray-700"
>
Dữ liệu bài viết trước khi xóa:
</
p
>
<
div
className=
"grid grid-cols-2 gap-x-4 gap-y-1"
>
<
span
>
Tiêu đề:
</
span
>
<
span
className=
"font-medium"
>
{
String
(
item
.
snapshot
.
title
||
""
)
}
</
span
>
<
span
>
Trạng thái:
</
span
>
<
span
className=
"font-medium"
>
{
String
(
item
.
snapshot
.
status
||
""
)
}
</
span
>
</
div
>
</
div
>
)
:
item
.
snapshot
?
(
<
div
className=
"space-y-1 text-sm text-gray-600"
>
<
p
className=
"font-medium text-gray-700"
>
Trạng thái ban đầu:
</
p
>
<
div
className=
"grid grid-cols-2 gap-x-4 gap-y-1"
>
<
span
>
Tiêu đề:
</
span
>
<
span
className=
"font-medium"
>
{
String
(
item
.
snapshot
.
title
||
item
.
snapshot
.
name
||
""
)
}
</
span
>
<
span
>
Trạng thái:
</
span
>
<
span
className=
"font-medium"
>
{
String
(
item
.
snapshot
.
status
||
""
)
}
</
span
>
</
div
>
</
div
>
)
:
(
<
p
className=
"text-sm text-gray-500 italic"
>
Không có chi tiết
</
p
>
)
}
</
div
>
)
}
</
div
>
);
}
function
HistoryLoadingState
()
{
return
(
<
div
className=
"flex items-center justify-center py-8"
>
<
Loader2
className=
"h-6 w-6 animate-spin text-[#063e8e]"
/>
<
span
className=
"ml-2 text-gray-600"
>
Đang tải lịch sử...
</
span
>
</
div
>
);
}
function
HistoryEmptyState
()
{
return
(
<
div
className=
"py-8 text-center text-gray-500"
>
<
Clock
className=
"mx-auto mb-2 h-8 w-8 text-gray-300"
/>
<
p
>
Chưa có lịch sử chỉnh sửa
</
p
>
</
div
>
);
}
function
HistoryErrorState
({
error
}:
{
error
:
string
})
{
return
(
<
div
className=
"rounded-lg border border-red-200 bg-red-50 p-4 text-red-700"
>
<
p
>
Không thể tải lịch sử:
{
error
}
</
p
>
</
div
>
);
}
interface
PostHistoryViewerProps
{
postId
:
string
;
}
export
function
PostHistoryViewer
({
postId
}:
PostHistoryViewerProps
)
{
const
[
isOpen
,
setIsOpen
]
=
React
.
useState
(
false
);
const
{
data
,
isLoading
,
isError
,
error
}
=
useGetApiV10PostIdHistory
(
postId
,
{
query
:
{
enabled
:
isOpen
,
},
});
const
historyItems
=
React
.
useMemo
(()
=>
{
// useQuery's `data` IS the API response body: { responseData: [...], ... }
const
raw
=
data
as
unknown
as
{
responseData
?:
PostHistoryItem
[];
data
?:
PostHistoryItem
[]
}
|
undefined
;
if
(
!
raw
)
return
[];
return
raw
.
responseData
??
raw
.
data
??
[];
},
[
data
]);
return
(
<
Collapsible
open=
{
isOpen
}
onOpenChange=
{
setIsOpen
}
>
<
CollapsibleTrigger
asChild
>
<
button
type=
"button"
className=
"flex w-full items-center justify-between rounded-xl border border-[#063e8e]/15 bg-white p-4 text-left transition hover:bg-[#063e8e]/5"
>
<
div
className=
"flex items-center gap-3"
>
<
div
className=
"flex h-10 w-10 items-center justify-center rounded-full bg-[#063e8e]/10"
>
<
Clock
className=
"h-5 w-5 text-[#063e8e]"
/>
</
div
>
<
div
>
<
p
className=
"font-semibold text-[#063e8e]"
>
Lịch sử chỉnh sửa
</
p
>
<
p
className=
"text-sm text-gray-500"
>
{
isLoading
?
"Đang tải..."
:
`${historyItems.length} thay đổi`
}
</
p
>
</
div
>
</
div
>
{
isOpen
?
(
<
ChevronUp
className=
"h-5 w-5 text-[#063e8e]"
/>
)
:
(
<
ChevronDown
className=
"h-5 w-5 text-[#063e8e]"
/>
)
}
</
button
>
</
CollapsibleTrigger
>
<
CollapsibleContent
>
<
div
className=
"mt-2 space-y-2"
>
{
isLoading
?
(
<
HistoryLoadingState
/>
)
:
isError
?
(
<
HistoryErrorState
error=
{
error
?.
message
||
"Lỗi không xác định"
}
/>
)
:
historyItems
.
length
===
0
?
(
<
HistoryEmptyState
/>
)
:
(
historyItems
.
map
((
item
)
=>
<
HistoryItem
key=
{
item
.
id
}
item=
{
item
}
/>)
)
}
</
div
>
</
CollapsibleContent
>
</
Collapsible
>
);
}
src/components/shared/sidebar-advertisements/index.tsx
View file @
1a6e3d53
...
...
@@ -13,7 +13,6 @@ const FALLBACK_HREF = "https://vcci-hcm.org.vn";
function
SidebarAdItem
({
item
}:
{
item
:
Advertisement
})
{
const
initialSrc
=
item
.
file
?.
path
?
resolveUploadUrl
(
item
.
file
.
path
)
:
FALLBACK_SRC
;
const
[
src
,
setSrc
]
=
useState
(
initialSrc
);
const
isGif
=
src
.
toLowerCase
().
endsWith
(
".gif"
);
return
(
<
Link
...
...
@@ -29,7 +28,7 @@ function SidebarAdItem({ item }: { item: Advertisement }) {
alt=
{
item
.
alt
||
item
.
name
}
fill
className=
"h-full w-full object-cover"
unoptimized
=
{
isGif
}
unoptimized
onError=
{
()
=>
{
if
(
src
!==
FALLBACK_SRC
)
setSrc
(
FALLBACK_SRC
);
}
}
...
...
@@ -54,6 +53,7 @@ function FallbackSidebarAdItem() {
alt=
"Quảng cáo VCCI HCM"
fill
className=
"h-full w-full object-cover"
unoptimized
/>
</
div
>
</
Link
>
...
...
src/lib/api/cms-admin.ts
View file @
1a6e3d53
...
...
@@ -55,6 +55,16 @@ export interface CmsPostContentSection {
images
:
CmsPostContentImage
[];
}
export
interface
CmsUserSummary
{
id
:
string
;
email
:
string
;
username
:
string
|
null
;
first_name
:
string
|
null
;
last_name
:
string
|
null
;
full_name
:
string
;
avatar_url
:
string
|
null
;
}
export
interface
CmsNewsItem
{
id
:
string
;
title
:
string
;
...
...
@@ -82,7 +92,10 @@ export interface CmsNewsItem {
registration_deadline
:
string
;
location
:
string
;
participation_fee
:
string
;
event_dates
:
string
[];
post_content
:
CmsPostContentSection
[];
creator
:
CmsUserSummary
|
null
;
editor
:
CmsUserSummary
|
null
;
}
export
interface
CmsHeaderCategoryItem
{
...
...
@@ -169,7 +182,20 @@ interface CmsRawPostItem {
registration_deadline
?:
string
|
null
;
location
?:
string
|
null
;
participation_fee
?:
string
|
null
;
event_dates
?:
string
[]
|
null
;
content_structure
?:
Record
<
string
,
unknown
>
|
null
;
creator
?:
CmsRawUser
|
null
;
editor
?:
CmsRawUser
|
null
;
}
interface
CmsRawUser
{
id
?:
string
|
null
;
email
?:
string
|
null
;
username
?:
string
|
null
;
first_name
?:
string
|
null
;
last_name
?:
string
|
null
;
full_name
?:
string
|
null
;
avatar_url
?:
string
|
null
;
}
interface
CmsPivotItem
{
...
...
@@ -182,6 +208,26 @@ interface CmsPivotItem {
const
isObject
=
(
value
:
unknown
):
value
is
Record
<
string
,
unknown
>
=>
typeof
value
===
"object"
&&
value
!==
null
&&
!
Array
.
isArray
(
value
);
const
normalizeUser
=
(
user
:
CmsRawUser
|
null
|
undefined
):
CmsUserSummary
|
null
=>
{
if
(
!
user
||
typeof
user
!==
"object"
||
!
user
.
id
)
return
null
;
const
firstName
=
String
(
user
.
first_name
??
""
).
trim
();
const
lastName
=
String
(
user
.
last_name
??
""
).
trim
();
const
fullName
=
String
(
user
.
full_name
??
""
).
trim
()
||
[
firstName
,
lastName
].
filter
(
Boolean
).
join
(
" "
).
trim
()
||
String
(
user
.
username
??
""
).
trim
()
||
String
(
user
.
email
??
""
).
trim
();
return
{
id
:
String
(
user
.
id
),
email
:
String
(
user
.
email
??
""
),
username
:
user
.
username
?
String
(
user
.
username
)
:
null
,
first_name
:
firstName
||
null
,
last_name
:
lastName
||
null
,
full_name
:
fullName
,
avatar_url
:
user
.
avatar_url
?
String
(
user
.
avatar_url
)
:
null
,
};
};
const
readMessage
=
(
payload
:
unknown
)
=>
{
if
(
!
isObject
(
payload
))
return
"Request failed"
;
if
(
typeof
payload
.
message
===
"string"
&&
payload
.
message
.
trim
())
return
payload
.
message
;
...
...
@@ -367,7 +413,12 @@ const transformPost = (
registration_deadline
:
normalizeDateTimeInput
(
post
.
registration_deadline
),
location
:
post
.
location
??
""
,
participation_fee
:
post
.
participation_fee
??
""
,
event_dates
:
Array
.
isArray
(
post
.
event_dates
)
?
post
.
event_dates
.
filter
((
d
):
d
is
string
=>
typeof
d
===
"string"
)
:
[],
post_content
:
fallbackContent
,
creator
:
normalizeUser
(
post
.
creator
),
editor
:
normalizeUser
(
post
.
editor
),
};
};
...
...
@@ -802,6 +853,7 @@ export async function createCmsNewsItem(input: {
registration_deadline
?:
string
|
null
;
location
?:
string
;
participation_fee
?:
string
;
event_dates
?:
string
[]
|
null
;
post_content
:
CmsPostContentSection
[];
})
{
const
payload
=
{
...
...
@@ -823,6 +875,7 @@ export async function createCmsNewsItem(input: {
registration_deadline
:
input
.
registration_deadline
||
null
,
location
:
input
.
location
?.
trim
()
||
null
,
participation_fee
:
input
.
participation_fee
?.
trim
()
||
null
,
event_dates
:
input
.
event_dates
??
null
,
release_mode
:
input
.
published_at
?
"SCHEDULED"
:
"NOW"
,
release_at
:
input
.
published_at
||
null
,
content_structure
:
{
...
...
@@ -863,6 +916,7 @@ export async function updateCmsNewsItem(
registration_deadline
?:
string
|
null
;
location
?:
string
;
participation_fee
?:
string
;
event_dates
?:
string
[]
|
null
;
post_content
:
CmsPostContentSection
[];
},
)
{
...
...
@@ -885,6 +939,7 @@ export async function updateCmsNewsItem(
registration_deadline
:
input
.
registration_deadline
||
null
,
location
:
input
.
location
?.
trim
()
||
null
,
participation_fee
:
input
.
participation_fee
?.
trim
()
||
null
,
event_dates
:
input
.
event_dates
??
null
,
release_mode
:
input
.
published_at
?
"SCHEDULED"
:
"NOW"
,
release_at
:
input
.
published_at
||
null
,
content_structure
:
{
...
...
@@ -909,3 +964,14 @@ export async function deleteCmsNewsItem(id: string) {
headers
:
authHeaders
(
false
),
});
}
export
async
function
toggleCmsNewsVisibility
(
id
:
string
,
isHidden
:
boolean
)
{
return
cmsRequest
<
CmsRawPostItem
>
(
`/post/
${
id
}
`
,
{
method
:
"PUT"
,
headers
:
authHeaders
(),
body
:
JSON
.
stringify
({
is_hidden
:
isHidden
,
is_active
:
!
isHidden
,
}),
});
}
src/lib/mock-home-posts.ts
View file @
1a6e3d53
...
...
@@ -30,6 +30,7 @@ export type HomePostItem = {
isActive
:
boolean
;
status
:
string
;
type
:
string
;
eventDates
?:
string
[];
categories
:
HomePostCategory
[];
thumbnail
:
{
url
:
string
;
alt
:
string
}
|
null
;
};
...
...
@@ -126,6 +127,7 @@ const buildPost = (params: BuildPostParams): HomePostItem => ({
isActive
:
true
,
status
:
"published"
,
type
:
"news"
,
eventDates
:
[],
categories
:
[
buildCategory
(
params
.
categoryId
,
...
...
src/mockdata/admin-news.ts
View file @
1a6e3d53
...
...
@@ -56,6 +56,16 @@ export interface AdminNewsContentSection {
images
:
AdminNewsContentImage
[];
}
export
interface
AdminNewsUser
{
id
:
string
;
email
:
string
;
username
:
string
|
null
;
first_name
:
string
|
null
;
last_name
:
string
|
null
;
full_name
:
string
;
avatar_url
:
string
|
null
;
}
export
interface
AdminNewsItem
{
id
:
string
;
title
:
string
;
...
...
@@ -77,7 +87,10 @@ export interface AdminNewsItem {
registration_deadline
:
string
;
location
:
string
;
participation_fee
:
string
;
event_dates
?:
string
[];
post_content
:
AdminNewsContentSection
[];
creator
?:
AdminNewsUser
|
null
;
editor
?:
AdminNewsUser
|
null
;
}
export
interface
AdminNewsFormValues
{
...
...
@@ -100,6 +113,7 @@ export interface AdminNewsFormValues {
registration_deadline
:
string
;
location
:
string
;
participation_fee
:
string
;
event_dates
?:
string
[];
post_content
:
AdminNewsContentSection
[];
}
...
...
@@ -123,6 +137,7 @@ export const EMPTY_ADMIN_NEWS_FORM: AdminNewsFormValues = {
registration_deadline
:
""
,
location
:
""
,
participation_fee
:
""
,
event_dates
:
[],
post_content
:
[],
};
...
...
@@ -1324,6 +1339,7 @@ export function cloneAdminNewsFormValues(item?: AdminNewsItem | null): AdminNews
registration_deadline
:
item
.
registration_deadline
,
location
:
item
.
location
,
participation_fee
:
item
.
participation_fee
,
event_dates
:
[...(
item
.
event_dates
??
[])],
post_content
:
item
.
post_content
.
map
((
section
)
=>
({
...
section
,
images
:
section
.
images
.
map
((
image
)
=>
({
...
...
swagger-output.json
0 → 100644
View file @
1a6e3d53
This source diff could not be displayed because it is too large. You can
view the blob
instead.
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