Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
V
vcci-news-frontend
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-frontend
Commits
cd1a8b50
Commit
cd1a8b50
authored
Sep 20, 2026
by
Phạm Quang Bảo
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: post and permission
parent
17ad281c
Changes
24
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
24 changed files
with
1249 additions
and
16 deletions
+1249
-16
event-address.ts
src/api/vcci-news/endpoints/event-address.ts
+632
-0
deleteApiV10EventAddressId200.ts
src/api/vcci-news/models/deleteApiV10EventAddressId200.ts
+11
-0
deleteApiV10EventAddressId200AllOf.ts
...pi/vcci-news/models/deleteApiV10EventAddressId200AllOf.ts
+11
-0
eventAddres.ts
src/api/vcci-news/models/eventAddres.ts
+12
-0
eventAddresMutate.ts
src/api/vcci-news/models/eventAddresMutate.ts
+11
-0
eventAddress.ts
src/api/vcci-news/models/eventAddress.ts
+12
-0
eventAddressCreate.ts
src/api/vcci-news/models/eventAddressCreate.ts
+11
-0
eventAddressMutate.ts
src/api/vcci-news/models/eventAddressMutate.ts
+10
-0
eventAddressUpdate.ts
src/api/vcci-news/models/eventAddressUpdate.ts
+11
-0
getApiV10EventAddressId200.ts
src/api/vcci-news/models/getApiV10EventAddressId200.ts
+11
-0
getApiV10EventAddressId200AllOf.ts
src/api/vcci-news/models/getApiV10EventAddressId200AllOf.ts
+12
-0
getApiV10EventAddressParams.ts
src/api/vcci-news/models/getApiV10EventAddressParams.ts
+37
-0
index.ts
src/api/vcci-news/models/index.ts
+15
-0
post.ts
src/api/vcci-news/models/post.ts
+0
-1
postApiV10EventAddress200.ts
src/api/vcci-news/models/postApiV10EventAddress200.ts
+11
-0
postApiV10EventAddress200AllOf.ts
src/api/vcci-news/models/postApiV10EventAddress200AllOf.ts
+12
-0
postMutate.ts
src/api/vcci-news/models/postMutate.ts
+0
-1
putApiV10EventAddressId200.ts
src/api/vcci-news/models/putApiV10EventAddressId200.ts
+11
-0
putApiV10EventAddressId200AllOf.ts
src/api/vcci-news/models/putApiV10EventAddressId200AllOf.ts
+12
-0
index.tsx
...app/(main)/[...slug]/templates/news-detail-page/index.tsx
+3
-2
admin-news-form-content.tsx
...p/admin/news/[id]/_components/admin-news-form-content.tsx
+106
-7
event-address-manager-dialog.tsx
...in/news/[id]/_components/event-address-manager-dialog.tsx
+283
-0
text-section.tsx
...omponents/shared/post-content/components/text-section.tsx
+10
-5
rich-text-editor.tsx
src/components/shared/rich-text-editor.tsx
+5
-0
No files found.
src/api/vcci-news/endpoints/event-address.ts
0 → 100644
View file @
cd1a8b50
This diff is collapsed.
Click to expand it.
src/api/vcci-news/models/deleteApiV10EventAddressId200.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.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
{
DeleteApiV10EventAddressId200AllOf
}
from
'./deleteApiV10EventAddressId200AllOf'
;
export
type
DeleteApiV10EventAddressId200
=
ApiResponse
&
DeleteApiV10EventAddressId200AllOf
;
src/api/vcci-news/models/deleteApiV10EventAddressId200AllOf.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
type
DeleteApiV10EventAddressId200AllOf
=
{
responseData
?:
boolean
;
};
src/api/vcci-news/models/eventAddres.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
interface
EventAddres
{
id
?:
string
;
information
?:
string
;
}
src/api/vcci-news/models/eventAddresMutate.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
interface
EventAddresMutate
{
information
?:
string
;
}
src/api/vcci-news/models/eventAddress.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
interface
EventAddress
{
id
:
string
;
information
:
string
;
}
src/api/vcci-news/models/eventAddressCreate.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
interface
EventAddressCreate
{
information
:
string
;
}
src/api/vcci-news/models/eventAddressMutate.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
EventAddressCreate
}
from
'./eventAddressCreate'
;
export
type
EventAddressMutate
=
EventAddressCreate
;
src/api/vcci-news/models/eventAddressUpdate.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
export
interface
EventAddressUpdate
{
information
?:
string
;
}
src/api/vcci-news/models/getApiV10EventAddressId200.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.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
{
GetApiV10EventAddressId200AllOf
}
from
'./getApiV10EventAddressId200AllOf'
;
export
type
GetApiV10EventAddressId200
=
ApiResponse
&
GetApiV10EventAddressId200AllOf
;
src/api/vcci-news/models/getApiV10EventAddressId200AllOf.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
EventAddress
}
from
'./eventAddress'
;
export
type
GetApiV10EventAddressId200AllOf
=
{
responseData
?:
EventAddress
;
};
src/api/vcci-news/models/getApiV10EventAddressParams.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
FiltersParameter
}
from
'./filtersParameter'
;
import
type
{
SortFieldParameter
}
from
'./sortFieldParameter'
;
import
type
{
SortOrderParameter
}
from
'./sortOrderParameter'
;
import
type
{
PageParameter
}
from
'./pageParameter'
;
import
type
{
PageSizeParameter
}
from
'./pageSizeParameter'
;
export
type
GetApiV10EventAddressParams
=
{
/**
* filter, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
filters
?:
FiltersParameter
;
/**
* sortField, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortField
?:
SortFieldParameter
;
/**
* sort order, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
*/
sortOrder
?:
SortOrderParameter
;
/**
* page, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
page
?:
PageParameter
;
/**
* pageSize, visit https://www.npmjs.com/package/sequelize-api-paginate for syntax
* @minimum 1
*/
pageSize
?:
PageSizeParameter
;
};
src/api/vcci-news/models/index.ts
View file @
cd1a8b50
...
...
@@ -60,6 +60,8 @@ export * from './deleteApiV10CategoryId200';
export
*
from
'./deleteApiV10CategoryId200AllOf'
;
export
*
from
'./deleteApiV10ContactId200'
;
export
*
from
'./deleteApiV10ContactId200AllOf'
;
export
*
from
'./deleteApiV10EventAddressId200'
;
export
*
from
'./deleteApiV10EventAddressId200AllOf'
;
export
*
from
'./deleteApiV10FileId200'
;
export
*
from
'./deleteApiV10FileId200AllOf'
;
export
*
from
'./deleteApiV10LogoId200'
;
...
...
@@ -88,6 +90,12 @@ export * from './deleteApiV10UserIdRoleBody';
export
*
from
'./deleteApiV10UserRoleId200'
;
export
*
from
'./deleteApiV10UserRoleId200AllOf'
;
export
*
from
'./deleteApiV10UserRoleId200AllOfResponseData'
;
export
*
from
'./eventAddres'
;
export
*
from
'./eventAddresMutate'
;
export
*
from
'./eventAddress'
;
export
*
from
'./eventAddressCreate'
;
export
*
from
'./eventAddressMutate'
;
export
*
from
'./eventAddressUpdate'
;
export
*
from
'./file'
;
export
*
from
'./fileCompressInfo'
;
export
*
from
'./fileImportInfo'
;
...
...
@@ -125,6 +133,9 @@ export * from './getApiV10CategoryTree200AllOf';
export
*
from
'./getApiV10ContactId200'
;
export
*
from
'./getApiV10ContactId200AllOf'
;
export
*
from
'./getApiV10ContactParams'
;
export
*
from
'./getApiV10EventAddressId200'
;
export
*
from
'./getApiV10EventAddressId200AllOf'
;
export
*
from
'./getApiV10EventAddressParams'
;
export
*
from
'./getApiV10FileId200'
;
export
*
from
'./getApiV10FileId200AllOf'
;
export
*
from
'./getApiV10FileParams'
;
...
...
@@ -263,6 +274,8 @@ export * from './postApiV10CategoryTagCategoryIdBulk200AllOf';
export
*
from
'./postApiV10CategoryTagCategoryIdBulkBody'
;
export
*
from
'./postApiV10Contact200'
;
export
*
from
'./postApiV10Contact200AllOf'
;
export
*
from
'./postApiV10EventAddress200'
;
export
*
from
'./postApiV10EventAddress200AllOf'
;
export
*
from
'./postApiV10FileUpload200'
;
export
*
from
'./postApiV10FileUpload200AllOf'
;
export
*
from
'./postApiV10FileUploadBody'
;
...
...
@@ -341,6 +354,8 @@ export * from './putApiV10CategoryId200';
export
*
from
'./putApiV10CategoryId200AllOf'
;
export
*
from
'./putApiV10ContactId200'
;
export
*
from
'./putApiV10ContactId200AllOf'
;
export
*
from
'./putApiV10EventAddressId200'
;
export
*
from
'./putApiV10EventAddressId200AllOf'
;
export
*
from
'./putApiV10LogoId200'
;
export
*
from
'./putApiV10LogoId200AllOf'
;
export
*
from
'./putApiV10PageConfigIdBody'
;
...
...
src/api/vcci-news/models/post.ts
View file @
cd1a8b50
...
...
@@ -16,7 +16,6 @@ export interface Post {
release_at
?:
string
;
is_active
?:
boolean
;
release_mode
?:
string
;
page_config_id
?:
string
;
created_at
?:
string
;
created_by
?:
string
;
updated_at
?:
string
;
...
...
src/api/vcci-news/models/postApiV10EventAddress200.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.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
{
PostApiV10EventAddress200AllOf
}
from
'./postApiV10EventAddress200AllOf'
;
export
type
PostApiV10EventAddress200
=
ApiResponse
&
PostApiV10EventAddress200AllOf
;
src/api/vcci-news/models/postApiV10EventAddress200AllOf.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
EventAddress
}
from
'./eventAddress'
;
export
type
PostApiV10EventAddress200AllOf
=
{
responseData
?:
EventAddress
;
};
src/api/vcci-news/models/postMutate.ts
View file @
cd1a8b50
...
...
@@ -15,7 +15,6 @@ export interface PostMutate {
release_at
?:
string
;
is_active
?:
boolean
;
release_mode
?:
string
;
page_config_id
?:
string
;
is_featured
?:
boolean
;
status
?:
string
;
type
?:
string
;
...
...
src/api/vcci-news/models/putApiV10EventAddressId200.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.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
{
PutApiV10EventAddressId200AllOf
}
from
'./putApiV10EventAddressId200AllOf'
;
export
type
PutApiV10EventAddressId200
=
ApiResponse
&
PutApiV10EventAddressId200AllOf
;
src/api/vcci-news/models/putApiV10EventAddressId200AllOf.ts
0 → 100644
View file @
cd1a8b50
/**
* Generated by orval v7.17.0 🍺
* Do not edit manually.
* VietProDev CMS Backend API
* Generated API documentation
* OpenAPI spec version: 1.0.0
*/
import
type
{
EventAddress
}
from
'./eventAddress'
;
export
type
PutApiV10EventAddressId200AllOf
=
{
responseData
?:
EventAddress
;
};
src/app/(main)/[...slug]/templates/news-detail-page/index.tsx
View file @
cd1a8b50
...
...
@@ -6,6 +6,7 @@ import SidebarAdvertisements from "@/components/shared/sidebar-advertisements";
import
{
findDisplayCategoryForPost
}
from
"../data"
;
import
{
renderPostSummary
}
from
"./components/post-summary-content"
;
import
PostContent
from
"@/components/shared/post-content"
;
import
{
PostTextSection
}
from
"@/components/shared/post-content/components/text-section"
;
import
EventInfoCard
from
"./components/event-info-card"
;
import
type
{
DynamicCategoryRouteItem
}
from
"../types"
;
import
type
{
DynamicPostItem
}
from
"@/api/vcci-news/types/post"
;
...
...
@@ -46,8 +47,8 @@ export default function NewsDetailPage({
{
/* summary */
}
{
post
.
summary
?
(
<
div
className=
"mt-5 max-w-4xl
text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8
"
>
{
renderPostSummary
(
post
.
summary
)
}
<
div
className=
"mt-5 max-w-4xl"
>
<
PostTextSection
>
{
renderPostSummary
(
post
.
summary
)
}
</
PostTextSection
>
</
div
>
)
:
null
}
...
...
src/app/admin/news/[id]/_components/admin-news-form-content.tsx
View file @
cd1a8b50
...
...
@@ -9,7 +9,7 @@ import {
useState
,
}
from
"react"
;
import
dayjs
from
"dayjs"
;
import
{
ArrowLeft
,
Save
,
Upload
,
X
,
Calendar
as
CalendarIcon
}
from
"lucide-react"
;
import
{
ArrowLeft
,
Save
,
Upload
,
X
,
Calendar
as
CalendarIcon
,
Plus
,
Check
,
ChevronsUpDown
}
from
"lucide-react"
;
import
Link
from
"next/link"
;
import
{
useParams
,
useRouter
,
useSearchParams
}
from
"next/navigation"
;
import
{
toast
}
from
"sonner"
;
...
...
@@ -31,6 +31,8 @@ import {
SelectValue
,
}
from
"@/components/ui/select"
;
import
{
Switch
}
from
"@/components/ui/switch"
;
import
{
Command
,
CommandItem
,
CommandList
}
from
"@/components/ui/command"
;
import
{
Popover
,
PopoverAnchor
,
PopoverContent
}
from
"@/components/ui/popover"
;
import
{
type
TagItem
,
}
from
"@/api/vcci-news/types/tag"
;
...
...
@@ -50,6 +52,7 @@ import {
getApiV10Tag
,
postApiV10TagIds
,
}
from
"@/api/vcci-news/endpoints/tag"
;
import
{
useGetApiV10EventAddress
}
from
"@/api/vcci-news/endpoints/event-address"
;
import
{
ADMIN_NEWS_TYPE_OPTIONS
,
cloneAdminNewsFormValues
,
...
...
@@ -74,6 +77,7 @@ import {
}
from
"./constants"
;
import
{
DateTimePicker
}
from
"@/components/shared/date-time-picker"
;
import
{
EventDatesDatePicker
}
from
"./event-dates-date-picker"
;
import
{
EventAddressManagerDialog
}
from
"./event-address-manager-dialog"
;
import
{
DraftRestoreDialog
}
from
"./draft-restore-dialog"
;
import
{
FormSection
}
from
"./form-section"
;
import
{
HeaderCategoryMultiPicker
}
from
"./header-category-multi-picker"
;
...
...
@@ -106,6 +110,37 @@ export function AdminNewsFormContent() {
const
[
isLoadingInitialData
,
setIsLoadingInitialData
]
=
useState
(
true
);
const
[
isMissingPost
,
setIsMissingPost
]
=
useState
(
false
);
const
[
useEventDates
,
setUseEventDates
]
=
useState
(
false
);
const
[
isAddressManagerOpen
,
setIsAddressManagerOpen
]
=
useState
(
false
);
const
[
isAddressSuggestOpen
,
setIsAddressSuggestOpen
]
=
useState
(
false
);
const
eventAddressesQuery
=
useGetApiV10EventAddress
({
page
:
1
,
pageSize
:
100
,
sortField
:
"information"
,
sortOrder
:
"asc"
,
});
const
addressOptions
=
useMemo
(()
=>
{
const
rows
=
eventAddressesQuery
.
data
?.
responseData
?.
rows
??
[];
const
values
=
Array
.
from
(
new
Set
(
rows
.
map
((
row
)
=>
String
((
row
as
Record
<
string
,
unknown
>
)?.
information
??
""
).
trim
())
.
filter
(
Boolean
),
),
);
const
currentLocation
=
form
?.
location
?.
trim
();
if
(
currentLocation
&&
!
values
.
includes
(
currentLocation
))
{
values
.
unshift
(
currentLocation
);
}
return
values
;
},
[
eventAddressesQuery
.
data
,
form
?.
location
]);
const
matchingAddressOptions
=
useMemo
(()
=>
{
const
keyword
=
form
?.
location
?.
trim
().
toLowerCase
()
??
""
;
if
(
!
keyword
)
return
addressOptions
;
return
addressOptions
.
filter
((
option
)
=>
option
.
toLowerCase
().
includes
(
keyword
));
},
[
addressOptions
,
form
?.
location
]);
// Draft auto-save (create mode only)
const
draftHasHydrated
=
useNewsDraftStore
((
s
)
=>
s
.
_hasHydrated
);
...
...
@@ -1003,12 +1038,76 @@ export function AdminNewsFormContent() {
</
div
>
<
div
>
<
Label
className=
"mb-1.5 block text-gray-700"
>
Địa điểm
</
Label
>
<
Input
value=
{
form
.
location
}
onChange=
{
(
event
)
=>
handleField
(
"location"
,
event
.
target
.
value
)
}
placeholder=
"Nhập địa điểm"
className=
{
fieldClassName
}
<
div
className=
"mb-1.5 flex items-center justify-between gap-2"
>
<
Label
className=
"block text-gray-700"
>
Địa điểm
</
Label
>
<
PermissionGate
required=
"settings:write"
>
<
button
type=
"button"
onClick=
{
()
=>
setIsAddressManagerOpen
(
true
)
}
className=
"flex items-center gap-1 text-xs font-medium text-[#063e8e] hover:underline"
>
<
Plus
className=
"h-3.5 w-3.5"
/>
Tạo địa điểm mới
</
button
>
</
PermissionGate
>
</
div
>
<
Popover
open=
{
isAddressSuggestOpen
}
onOpenChange=
{
setIsAddressSuggestOpen
}
>
<
PopoverAnchor
asChild
>
<
div
className=
"relative"
>
<
Input
value=
{
form
.
location
}
onChange=
{
(
event
)
=>
{
handleField
(
"location"
,
event
.
target
.
value
);
setIsAddressSuggestOpen
(
true
);
}
}
onFocus=
{
()
=>
setIsAddressSuggestOpen
(
true
)
}
placeholder=
"Nhập hoặc chọn địa điểm"
className=
{
fieldClassName
}
autoComplete=
"off"
/>
<
ChevronsUpDown
className=
"pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"
/>
</
div
>
</
PopoverAnchor
>
<
PopoverContent
align=
"start"
onOpenAutoFocus=
{
(
event
)
=>
event
.
preventDefault
()
}
className=
"w-[var(--radix-popover-trigger-width)] min-w-[var(--radix-popover-trigger-width)] rounded-xl border-[#063e8e]/15 bg-white p-0 text-gray-700"
>
<
Command
className=
"bg-white text-gray-700"
>
<
CommandList
className=
"max-h-60"
>
{
matchingAddressOptions
.
length
===
0
?
(
<
div
className=
"px-3 py-2 text-sm text-gray-500"
>
Không có gợi ý — địa điểm sẽ dùng như đã nhập
</
div
>
)
:
(
matchingAddressOptions
.
map
((
option
)
=>
(
<
CommandItem
key=
{
option
}
value=
{
option
}
onSelect=
{
()
=>
{
handleField
(
"location"
,
option
);
setIsAddressSuggestOpen
(
false
);
}
}
className=
"gap-2 px-3 py-2 text-gray-700 data-[selected=true]:bg-[#063e8e]/10 data-[selected=true]:text-[#063e8e]"
>
<
Check
className=
{
`h-4 w-4 shrink-0 text-[#063e8e] ${form.location === option ? "opacity-100" : "opacity-0"
}`
}
/>
<
span
className=
"truncate"
>
{
option
}
</
span
>
</
CommandItem
>
))
)
}
</
CommandList
>
</
Command
>
</
PopoverContent
>
</
Popover
>
<
EventAddressManagerDialog
open=
{
isAddressManagerOpen
}
onOpenChange=
{
setIsAddressManagerOpen
}
/>
</
div
>
...
...
src/app/admin/news/[id]/_components/event-address-manager-dialog.tsx
0 → 100644
View file @
cd1a8b50
"use client"
;
import
{
useMemo
,
useState
}
from
"react"
;
import
{
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
Loader2
,
Pencil
,
Plus
,
Trash2
}
from
"lucide-react"
;
import
{
toast
}
from
"sonner"
;
import
{
useDeleteApiV10EventAddressId
,
useGetApiV10EventAddress
,
usePostApiV10EventAddress
,
usePutApiV10EventAddressId
,
}
from
"@/api/vcci-news/endpoints/event-address"
;
import
{
Button
}
from
"@/components/ui/button"
;
import
{
Dialog
,
DialogContent
,
DialogDescription
,
DialogHeader
,
DialogTitle
,
}
from
"@/components/ui/dialog"
;
import
{
Input
}
from
"@/components/ui/input"
;
import
{
PermissionGate
}
from
"@/components/shared/permission-gate"
;
const
EVENT_ADDRESS_QUERY_KEY
=
"/api/v1.0/eventAddress"
;
interface
EventAddressItem
{
id
:
string
;
information
:
string
;
}
interface
EventAddressManagerDialogProps
{
open
:
boolean
;
onOpenChange
:
(
open
:
boolean
)
=>
void
;
}
export
function
EventAddressManagerDialog
({
open
,
onOpenChange
,
}:
EventAddressManagerDialogProps
)
{
const
queryClient
=
useQueryClient
();
const
[
newInformation
,
setNewInformation
]
=
useState
(
""
);
const
[
editingId
,
setEditingId
]
=
useState
<
string
|
null
>
(
null
);
const
[
editingInformation
,
setEditingInformation
]
=
useState
(
""
);
const
[
deletingId
,
setDeletingId
]
=
useState
<
string
|
null
>
(
null
);
const
{
data
,
isLoading
}
=
useGetApiV10EventAddress
(
open
?
{
page
:
1
,
pageSize
:
100
,
sortField
:
"information"
,
sortOrder
:
"asc"
}
:
undefined
,
);
const
addresses
=
useMemo
(()
=>
{
const
rows
=
data
?.
responseData
?.
rows
??
[];
return
rows
.
map
((
row
)
=>
{
const
record
=
(
row
??
{})
as
Record
<
string
,
unknown
>
;
return
{
id
:
String
(
record
.
id
??
""
),
information
:
String
(
record
.
information
??
""
).
trim
(),
};
})
.
filter
((
item
)
=>
item
.
id
&&
item
.
information
);
},
[
data
]);
const
invalidateAddresses
=
()
=>
{
queryClient
.
invalidateQueries
({
queryKey
:
[
EVENT_ADDRESS_QUERY_KEY
],
exact
:
false
,
});
};
const
createMutation
=
usePostApiV10EventAddress
({
mutation
:
{
onSettled
:
invalidateAddresses
},
});
const
updateMutation
=
usePutApiV10EventAddressId
({
mutation
:
{
onSettled
:
invalidateAddresses
},
});
const
deleteMutation
=
useDeleteApiV10EventAddressId
({
mutation
:
{
onSettled
:
invalidateAddresses
},
});
const
handleCreate
=
async
()
=>
{
const
value
=
newInformation
.
trim
();
if
(
!
value
)
{
toast
.
error
(
"Vui lòng nhập địa chỉ"
);
return
;
}
try
{
await
createMutation
.
mutateAsync
({
data
:
{
information
:
value
}
});
toast
.
success
(
"Tạo địa chỉ thành công"
);
setNewInformation
(
""
);
}
catch
(
error
)
{
const
err
=
error
as
{
message
?:
string
};
toast
.
error
(
err
?.
message
||
"Tạo địa chỉ thất bại"
);
}
};
const
handleStartEdit
=
(
item
:
EventAddressItem
)
=>
{
setEditingId
(
item
.
id
);
setEditingInformation
(
item
.
information
);
};
const
handleCancelEdit
=
()
=>
{
setEditingId
(
null
);
setEditingInformation
(
""
);
};
const
handleUpdate
=
async
()
=>
{
if
(
!
editingId
)
return
;
const
value
=
editingInformation
.
trim
();
if
(
!
value
)
{
toast
.
error
(
"Vui lòng nhập địa chỉ"
);
return
;
}
try
{
await
updateMutation
.
mutateAsync
({
id
:
editingId
,
data
:
{
information
:
value
}
});
toast
.
success
(
"Cập nhật địa chỉ thành công"
);
setEditingId
(
null
);
setEditingInformation
(
""
);
}
catch
(
error
)
{
const
err
=
error
as
{
message
?:
string
};
toast
.
error
(
err
?.
message
||
"Cập nhật địa chỉ thất bại"
);
}
};
const
handleConfirmDelete
=
async
()
=>
{
if
(
!
deletingId
)
return
;
try
{
await
deleteMutation
.
mutateAsync
({
id
:
deletingId
});
toast
.
success
(
"Xóa địa chỉ thành công"
);
setDeletingId
(
null
);
}
catch
(
error
)
{
const
err
=
error
as
{
message
?:
string
};
toast
.
error
(
err
?.
message
||
"Xóa địa chỉ thất bại"
);
}
};
const
handleOpenChange
=
(
nextOpen
:
boolean
)
=>
{
if
(
!
nextOpen
)
{
setEditingId
(
null
);
setEditingInformation
(
""
);
setDeletingId
(
null
);
}
onOpenChange
(
nextOpen
);
};
return
(
<
Dialog
open=
{
open
}
onOpenChange=
{
handleOpenChange
}
>
<
DialogContent
className=
"max-w-lg"
>
<
DialogHeader
>
<
DialogTitle
>
Quản lý địa điểm
</
DialogTitle
>
<
DialogDescription
>
Danh sách địa điểm dùng cho các sự kiện trong hệ thống.
</
DialogDescription
>
</
DialogHeader
>
<
PermissionGate
required=
"settings:write"
>
<
div
className=
"flex gap-2"
>
<
Input
value=
{
newInformation
}
onChange=
{
(
event
)
=>
setNewInformation
(
event
.
target
.
value
)
}
placeholder=
"Nhập địa chỉ mới"
className=
"rounded-xl border-[#063e8e]/15 bg-white text-gray-700 placeholder:text-gray-700 focus-visible:ring-[#063e8e]/30"
onKeyDown=
{
(
event
)
=>
{
if
(
event
.
key
===
"Enter"
)
{
event
.
preventDefault
();
handleCreate
();
}
}
}
/>
<
Button
type=
"button"
onClick=
{
handleCreate
}
disabled=
{
createMutation
.
isPending
||
!
newInformation
.
trim
()
}
className=
"shrink-0 rounded-xl bg-[#063e8e] text-white hover:bg-[#063e8e]/90"
>
{
createMutation
.
isPending
?
(
<
Loader2
className=
"h-4 w-4 animate-spin"
/>
)
:
(
<
Plus
className=
"h-4 w-4"
/>
)
}
Thêm
</
Button
>
</
div
>
</
PermissionGate
>
<
div
className=
"mt-3 max-h-80 space-y-2 overflow-y-auto pr-1"
>
{
isLoading
?
(
<
div
className=
"flex items-center justify-center gap-2 py-8 text-sm text-gray-500"
>
<
Loader2
className=
"h-4 w-4 animate-spin"
/>
Đang tải danh sách địa chỉ...
</
div
>
)
:
addresses
.
length
===
0
?
(
<
p
className=
"py-8 text-center text-sm text-gray-500"
>
Chưa có địa chỉ nào
</
p
>
)
:
(
addresses
.
map
((
item
)
=>
(
<
div
key=
{
item
.
id
}
className=
"flex items-center justify-between gap-2 rounded-xl border border-[#063e8e]/10 px-3 py-2"
>
{
editingId
===
item
.
id
?
(
<
div
className=
"flex flex-1 items-center gap-2"
>
<
Input
value=
{
editingInformation
}
onChange=
{
(
event
)
=>
setEditingInformation
(
event
.
target
.
value
)
}
className=
"h-8 rounded-lg border-[#063e8e]/15 bg-white text-sm text-gray-700"
onKeyDown=
{
(
event
)
=>
{
if
(
event
.
key
===
"Enter"
)
handleUpdate
();
}
}
/>
<
Button
type=
"button"
size=
"sm"
onClick=
{
handleUpdate
}
disabled=
{
updateMutation
.
isPending
||
!
editingInformation
.
trim
()
}
>
Lưu
</
Button
>
<
Button
type=
"button"
variant=
"outline"
size=
"sm"
onClick=
{
handleCancelEdit
}
>
Hủy
</
Button
>
</
div
>
)
:
deletingId
===
item
.
id
?
(
<>
<
span
className=
"text-sm text-gray-700"
>
Xóa địa chỉ này?
</
span
>
<
div
className=
"flex gap-1"
>
<
Button
type=
"button"
size=
"sm"
variant=
"destructive"
onClick=
{
handleConfirmDelete
}
disabled=
{
deleteMutation
.
isPending
}
>
Xóa
</
Button
>
<
Button
type=
"button"
size=
"sm"
variant=
"outline"
onClick=
{
()
=>
setDeletingId
(
null
)
}
>
Hủy
</
Button
>
</
div
>
</>
)
:
(
<>
<
span
className=
"text-sm text-gray-700"
>
{
item
.
information
}
</
span
>
<
PermissionGate
required=
"settings:write"
>
<
div
className=
"flex gap-1"
>
<
button
type=
"button"
onClick=
{
()
=>
handleStartEdit
(
item
)
}
className=
"rounded-full p-1.5 text-gray-500 hover:bg-[#063e8e]/10 hover:text-[#063e8e]"
aria
-
label=
"Sửa địa chỉ"
>
<
Pencil
className=
"h-4 w-4"
/>
</
button
>
<
button
type=
"button"
onClick=
{
()
=>
setDeletingId
(
item
.
id
)
}
className=
"rounded-full p-1 text-gray-500 hover:bg-red-50 hover:text-red-600"
aria
-
label=
"Xóa địa chỉ"
>
<
Trash2
className=
"h-4 w-4"
/>
</
button
>
</
div
>
</
PermissionGate
>
</>
)
}
</
div
>
))
)
}
</
div
>
</
DialogContent
>
</
Dialog
>
);
}
src/components/shared/post-content/components/text-section.tsx
View file @
cd1a8b50
import
parse
from
"html-react-parser"
;
import
type
{
ReactNode
}
from
"react"
;
import
type
{
DynamicPostContentSection
}
from
"@/api/vcci-news/types/post"
;
function
normalizeCaptionShortcodes
(
html
:
string
)
{
...
...
@@ -118,13 +119,10 @@ function renderStructuredHtml(html: string) {
return
parse
(
normalizeImportedLayout
(
normalizeCaptionShortcodes
(
html
)));
}
export
function
TextSection
({
section
}:
{
section
:
DynamicPostContentSection
})
{
const
content
=
section
.
content
.
trim
();
if
(
!
content
)
return
null
;
export
function
PostTextSection
({
children
}:
{
children
:
ReactNode
})
{
return
(
<
div
className=
"post-text-section"
>
{
renderStructuredHtml
(
content
)
}
{
children
}
<
style
jsx
global
>
{
`
.post-text-section {
...
...
@@ -215,3 +213,10 @@ export function TextSection({ section }: { section: DynamicPostContentSection })
</
div
>
);
}
export
function
TextSection
({
section
}:
{
section
:
DynamicPostContentSection
})
{
const
content
=
section
.
content
.
trim
();
if
(
!
content
)
return
null
;
return
<
PostTextSection
>
{
renderStructuredHtml
(
content
)
}
</
PostTextSection
>;
}
src/components/shared/rich-text-editor.tsx
View file @
cd1a8b50
...
...
@@ -415,6 +415,11 @@ export function AdminRichTextEditor({
margin-bottom: 1em;
}
.admin-rich-text-editor .jodit-wysiwyg a {
color: #14519f;
text-decoration: underline;
}
.admin-rich-text-editor .jodit-wysiwyg .article-content,
.admin-rich-text-editor .jodit-wysiwyg .article-content_toc,
.admin-rich-text-editor .jodit-wysiwyg table,
...
...
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