Commit cd1a8b50 authored by Phạm Quang Bảo's avatar Phạm Quang Bảo

fix: post and permission

parent 17ad281c
This diff is collapsed.
/**
* 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;
/**
* 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;
};
/**
* 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;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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;
}
/**
* 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;
/**
* 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;
}
/**
* 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;
/**
* 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;
};
/**
* 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;
};
...@@ -60,6 +60,8 @@ export * from './deleteApiV10CategoryId200'; ...@@ -60,6 +60,8 @@ export * from './deleteApiV10CategoryId200';
export * from './deleteApiV10CategoryId200AllOf'; export * from './deleteApiV10CategoryId200AllOf';
export * from './deleteApiV10ContactId200'; export * from './deleteApiV10ContactId200';
export * from './deleteApiV10ContactId200AllOf'; export * from './deleteApiV10ContactId200AllOf';
export * from './deleteApiV10EventAddressId200';
export * from './deleteApiV10EventAddressId200AllOf';
export * from './deleteApiV10FileId200'; export * from './deleteApiV10FileId200';
export * from './deleteApiV10FileId200AllOf'; export * from './deleteApiV10FileId200AllOf';
export * from './deleteApiV10LogoId200'; export * from './deleteApiV10LogoId200';
...@@ -88,6 +90,12 @@ export * from './deleteApiV10UserIdRoleBody'; ...@@ -88,6 +90,12 @@ export * from './deleteApiV10UserIdRoleBody';
export * from './deleteApiV10UserRoleId200'; export * from './deleteApiV10UserRoleId200';
export * from './deleteApiV10UserRoleId200AllOf'; export * from './deleteApiV10UserRoleId200AllOf';
export * from './deleteApiV10UserRoleId200AllOfResponseData'; 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 './file';
export * from './fileCompressInfo'; export * from './fileCompressInfo';
export * from './fileImportInfo'; export * from './fileImportInfo';
...@@ -125,6 +133,9 @@ export * from './getApiV10CategoryTree200AllOf'; ...@@ -125,6 +133,9 @@ export * from './getApiV10CategoryTree200AllOf';
export * from './getApiV10ContactId200'; export * from './getApiV10ContactId200';
export * from './getApiV10ContactId200AllOf'; export * from './getApiV10ContactId200AllOf';
export * from './getApiV10ContactParams'; export * from './getApiV10ContactParams';
export * from './getApiV10EventAddressId200';
export * from './getApiV10EventAddressId200AllOf';
export * from './getApiV10EventAddressParams';
export * from './getApiV10FileId200'; export * from './getApiV10FileId200';
export * from './getApiV10FileId200AllOf'; export * from './getApiV10FileId200AllOf';
export * from './getApiV10FileParams'; export * from './getApiV10FileParams';
...@@ -263,6 +274,8 @@ export * from './postApiV10CategoryTagCategoryIdBulk200AllOf'; ...@@ -263,6 +274,8 @@ export * from './postApiV10CategoryTagCategoryIdBulk200AllOf';
export * from './postApiV10CategoryTagCategoryIdBulkBody'; export * from './postApiV10CategoryTagCategoryIdBulkBody';
export * from './postApiV10Contact200'; export * from './postApiV10Contact200';
export * from './postApiV10Contact200AllOf'; export * from './postApiV10Contact200AllOf';
export * from './postApiV10EventAddress200';
export * from './postApiV10EventAddress200AllOf';
export * from './postApiV10FileUpload200'; export * from './postApiV10FileUpload200';
export * from './postApiV10FileUpload200AllOf'; export * from './postApiV10FileUpload200AllOf';
export * from './postApiV10FileUploadBody'; export * from './postApiV10FileUploadBody';
...@@ -341,6 +354,8 @@ export * from './putApiV10CategoryId200'; ...@@ -341,6 +354,8 @@ export * from './putApiV10CategoryId200';
export * from './putApiV10CategoryId200AllOf'; export * from './putApiV10CategoryId200AllOf';
export * from './putApiV10ContactId200'; export * from './putApiV10ContactId200';
export * from './putApiV10ContactId200AllOf'; export * from './putApiV10ContactId200AllOf';
export * from './putApiV10EventAddressId200';
export * from './putApiV10EventAddressId200AllOf';
export * from './putApiV10LogoId200'; export * from './putApiV10LogoId200';
export * from './putApiV10LogoId200AllOf'; export * from './putApiV10LogoId200AllOf';
export * from './putApiV10PageConfigIdBody'; export * from './putApiV10PageConfigIdBody';
......
...@@ -16,7 +16,6 @@ export interface Post { ...@@ -16,7 +16,6 @@ export interface Post {
release_at?: string; release_at?: string;
is_active?: boolean; is_active?: boolean;
release_mode?: string; release_mode?: string;
page_config_id?: string;
created_at?: string; created_at?: string;
created_by?: string; created_by?: string;
updated_at?: string; updated_at?: string;
......
/**
* 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;
/**
* 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;
};
...@@ -15,7 +15,6 @@ export interface PostMutate { ...@@ -15,7 +15,6 @@ export interface PostMutate {
release_at?: string; release_at?: string;
is_active?: boolean; is_active?: boolean;
release_mode?: string; release_mode?: string;
page_config_id?: string;
is_featured?: boolean; is_featured?: boolean;
status?: string; status?: string;
type?: string; type?: string;
......
/**
* 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;
/**
* 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;
};
...@@ -6,6 +6,7 @@ import SidebarAdvertisements from "@/components/shared/sidebar-advertisements"; ...@@ -6,6 +6,7 @@ import SidebarAdvertisements from "@/components/shared/sidebar-advertisements";
import { findDisplayCategoryForPost } from "../data"; import { findDisplayCategoryForPost } from "../data";
import { renderPostSummary } from "./components/post-summary-content"; import { renderPostSummary } from "./components/post-summary-content";
import PostContent from "@/components/shared/post-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 EventInfoCard from "./components/event-info-card";
import type { DynamicCategoryRouteItem } from "../types"; import type { DynamicCategoryRouteItem } from "../types";
import type { DynamicPostItem } from "@/api/vcci-news/types/post"; import type { DynamicPostItem } from "@/api/vcci-news/types/post";
...@@ -46,8 +47,8 @@ export default function NewsDetailPage({ ...@@ -46,8 +47,8 @@ export default function NewsDetailPage({
{/* summary */} {/* summary */}
{post.summary ? ( {post.summary ? (
<div className="mt-5 max-w-4xl text-base font-semibold leading-7 text-[#374151] md:text-lg md:leading-8"> <div className="mt-5 max-w-4xl">
{renderPostSummary(post.summary)} <PostTextSection>{renderPostSummary(post.summary)}</PostTextSection>
</div> </div>
) : null} ) : null}
......
...@@ -9,7 +9,7 @@ import { ...@@ -9,7 +9,7 @@ import {
useState, useState,
} from "react"; } from "react";
import dayjs from "dayjs"; 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 Link from "next/link";
import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useParams, useRouter, useSearchParams } from "next/navigation";
import { toast } from "sonner"; import { toast } from "sonner";
...@@ -31,6 +31,8 @@ import { ...@@ -31,6 +31,8 @@ import {
SelectValue, SelectValue,
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Command, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverAnchor, PopoverContent } from "@/components/ui/popover";
import { import {
type TagItem, type TagItem,
} from "@/api/vcci-news/types/tag"; } from "@/api/vcci-news/types/tag";
...@@ -50,6 +52,7 @@ import { ...@@ -50,6 +52,7 @@ import {
getApiV10Tag, getApiV10Tag,
postApiV10TagIds, postApiV10TagIds,
} from "@/api/vcci-news/endpoints/tag"; } from "@/api/vcci-news/endpoints/tag";
import { useGetApiV10EventAddress } from "@/api/vcci-news/endpoints/event-address";
import { import {
ADMIN_NEWS_TYPE_OPTIONS, ADMIN_NEWS_TYPE_OPTIONS,
cloneAdminNewsFormValues, cloneAdminNewsFormValues,
...@@ -74,6 +77,7 @@ import { ...@@ -74,6 +77,7 @@ import {
} from "./constants"; } from "./constants";
import { DateTimePicker } from "@/components/shared/date-time-picker"; import { DateTimePicker } from "@/components/shared/date-time-picker";
import { EventDatesDatePicker } from "./event-dates-date-picker"; import { EventDatesDatePicker } from "./event-dates-date-picker";
import { EventAddressManagerDialog } from "./event-address-manager-dialog";
import { DraftRestoreDialog } from "./draft-restore-dialog"; import { DraftRestoreDialog } from "./draft-restore-dialog";
import { FormSection } from "./form-section"; import { FormSection } from "./form-section";
import { HeaderCategoryMultiPicker } from "./header-category-multi-picker"; import { HeaderCategoryMultiPicker } from "./header-category-multi-picker";
...@@ -106,6 +110,37 @@ export function AdminNewsFormContent() { ...@@ -106,6 +110,37 @@ export function AdminNewsFormContent() {
const [isLoadingInitialData, setIsLoadingInitialData] = useState(true); const [isLoadingInitialData, setIsLoadingInitialData] = useState(true);
const [isMissingPost, setIsMissingPost] = useState(false); const [isMissingPost, setIsMissingPost] = useState(false);
const [useEventDates, setUseEventDates] = 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) // Draft auto-save (create mode only)
const draftHasHydrated = useNewsDraftStore((s) => s._hasHydrated); const draftHasHydrated = useNewsDraftStore((s) => s._hasHydrated);
...@@ -1003,12 +1038,76 @@ export function AdminNewsFormContent() { ...@@ -1003,12 +1038,76 @@ export function AdminNewsFormContent() {
</div> </div>
<div> <div>
<Label className="mb-1.5 block text-gray-700">Địa điểm</Label> <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 <Input
value={form.location} value={form.location}
onChange={(event) => handleField("location", event.target.value)} onChange={(event) => {
placeholder="Nhập địa điểm" handleField("location", event.target.value);
setIsAddressSuggestOpen(true);
}}
onFocus={() => setIsAddressSuggestOpen(true)}
placeholder="Nhập hoặc chọn địa điểm"
className={fieldClassName} 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> </div>
......
"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>
);
}
import parse from "html-react-parser"; import parse from "html-react-parser";
import type { ReactNode } from "react";
import type { DynamicPostContentSection } from "@/api/vcci-news/types/post"; import type { DynamicPostContentSection } from "@/api/vcci-news/types/post";
function normalizeCaptionShortcodes(html: string) { function normalizeCaptionShortcodes(html: string) {
...@@ -118,13 +119,10 @@ function renderStructuredHtml(html: string) { ...@@ -118,13 +119,10 @@ function renderStructuredHtml(html: string) {
return parse(normalizeImportedLayout(normalizeCaptionShortcodes(html))); return parse(normalizeImportedLayout(normalizeCaptionShortcodes(html)));
} }
export function TextSection({ section }: { section: DynamicPostContentSection }) { export function PostTextSection({ children }: { children: ReactNode }) {
const content = section.content.trim();
if (!content) return null;
return ( return (
<div className="post-text-section"> <div className="post-text-section">
{renderStructuredHtml(content)} {children}
<style jsx global>{` <style jsx global>{`
.post-text-section { .post-text-section {
...@@ -215,3 +213,10 @@ export function TextSection({ section }: { section: DynamicPostContentSection }) ...@@ -215,3 +213,10 @@ export function TextSection({ section }: { section: DynamicPostContentSection })
</div> </div>
); );
} }
export function TextSection({ section }: { section: DynamicPostContentSection }) {
const content = section.content.trim();
if (!content) return null;
return <PostTextSection>{renderStructuredHtml(content)}</PostTextSection>;
}
...@@ -415,6 +415,11 @@ export function AdminRichTextEditor({ ...@@ -415,6 +415,11 @@ export function AdminRichTextEditor({
margin-bottom: 1em; 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,
.admin-rich-text-editor .jodit-wysiwyg .article-content_toc, .admin-rich-text-editor .jodit-wysiwyg .article-content_toc,
.admin-rich-text-editor .jodit-wysiwyg table, .admin-rich-text-editor .jodit-wysiwyg table,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment