Commit f818e2b5 authored by Lê Bảo Hồng Đức's avatar Lê Bảo Hồng Đức

Merge branch 'feat/video-newsletter' into 'develop-news'

fix

See merge request !62
parents 5257ce93 adfed5ce
import Axios, { AxiosError, AxiosHeaders, AxiosRequestConfig, InternalAxiosRequestConfig } from "axios"; import Axios, { AxiosError, AxiosHeaders, AxiosRequestConfig, InternalAxiosRequestConfig } from "axios";
import links from "@/links";
import { import {
ensureValidAdminAccessToken, ensureValidAdminAccessToken,
refreshAdminAccessToken, refreshAdminAccessToken,
...@@ -10,7 +11,7 @@ interface RetriableAxiosRequestConfig extends InternalAxiosRequestConfig { ...@@ -10,7 +11,7 @@ interface RetriableAxiosRequestConfig extends InternalAxiosRequestConfig {
const createAxiosInstance = () => { const createAxiosInstance = () => {
const instance = Axios.create({ const instance = Axios.create({
baseURL: `${process.env.NEXT_PUBLIC_BACKEND_HOST}/api/v1.0`, baseURL: links.apiEndpoint,
withCredentials: true, withCredentials: true,
}); });
......
import { NewsItem } from "@/api/types/news"; import { NewsItem } from "@/api/types/news";
import BASE_URL from "@/links"; import { resolveUploadUrl } from "@/links";
import dayjs from "dayjs"; import dayjs from "dayjs";
import AppEditorContent from "@/components/shared/editor-content"; import AppEditorContent from "@/components/shared/editor-content";
import Link from "next/link"; import Link from "next/link";
...@@ -12,7 +12,7 @@ function CardNews({ news }: { news: NewsItem }) { ...@@ -12,7 +12,7 @@ function CardNews({ news }: { news: NewsItem }) {
className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3" className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3"
> >
<ImageNext <ImageNext
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`} src={resolveUploadUrl(news.thumbnail)}
alt={news.title} alt={news.title}
className="aspect-3/2 object-cover" className="aspect-3/2 object-cover"
width={130} width={130}
......
import { EventItem } from '@/api/types/event' import { EventItem } from '@/api/types/event'
import BASE_URL from '@/links' import { resolveUploadUrl } from '@/links'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import AppEditorContent from '@/components/shared/editor-content'; import AppEditorContent from '@/components/shared/editor-content';
import Link from "next/link"; import Link from "next/link";
...@@ -12,7 +12,7 @@ function CardEvent({ event }: { event: EventItem }) { ...@@ -12,7 +12,7 @@ function CardEvent({ event }: { event: EventItem }) {
className='flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3 p-2 sm:p-3 border border-gray-200 bg-white rounded-md' className='flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3 p-2 sm:p-3 border border-gray-200 bg-white rounded-md'
> >
<ImageNext <ImageNext
src={`${BASE_URL.imageEndpoint}${event.image}`} src={resolveUploadUrl(event.image)}
alt={event.name} alt={event.name}
className='aspect-3/2 object-cover' className='aspect-3/2 object-cover'
width={130} width={130}
......
import { NewsItem } from "@/api/types/news"; import { NewsItem } from "@/api/types/news";
import BASE_URL from "@/links"; import { resolveUploadUrl } from "@/links";
import dayjs from "dayjs"; import dayjs from "dayjs";
import AppEditorContent from "@/components/shared/editor-content"; import AppEditorContent from "@/components/shared/editor-content";
import Link from "next/link"; import Link from "next/link";
...@@ -12,7 +12,7 @@ function CardNews({ news }: { news: NewsItem }) { ...@@ -12,7 +12,7 @@ function CardNews({ news }: { news: NewsItem }) {
className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3" className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3"
> >
<ImageNext <ImageNext
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`} src={resolveUploadUrl(news.thumbnail)}
alt={news.title} alt={news.title}
className="aspect-3/2 object-cover" className="aspect-3/2 object-cover"
width={130} width={130}
......
import { NewsItem } from "@/api/types/news"; import { NewsItem } from "@/api/types/news";
import BASE_URL from "@/links"; import { resolveUploadUrl } from "@/links";
import dayjs from "dayjs"; import dayjs from "dayjs";
import Link from "next/link"; import Link from "next/link";
import ImageNext from "@/components/shared/image-next"; import ImageNext from "@/components/shared/image-next";
...@@ -11,7 +11,7 @@ function CardNews({ news }: { news: NewsItem }) { ...@@ -11,7 +11,7 @@ function CardNews({ news }: { news: NewsItem }) {
className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3" className="flex flex-row gap-2 mb-2 sm:gap-3 sm:mb-3"
> >
<ImageNext <ImageNext
src={`${BASE_URL.imageEndpoint}${news.thumbnail}`} src={resolveUploadUrl(news.thumbnail)}
alt={news.title} alt={news.title}
className="aspect-3/2 object-cover" className="aspect-3/2 object-cover"
width={130} width={130}
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
import * as React from "react"; import * as React from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useCustomClient } from "@/api/mutator/custom-client"; import { useCustomClient } from "@/api/mutator/custom-client";
import Links from "@/links"; import Links, { resolveUploadUrl } from "@/links";
type RawHomeCategory = { type RawHomeCategory = {
id?: string | null; id?: string | null;
...@@ -153,12 +153,8 @@ const resolveAssetUrl = (value?: string | null) => { ...@@ -153,12 +153,8 @@ const resolveAssetUrl = (value?: string | null) => {
const trimmed = value?.trim(); const trimmed = value?.trim();
if (!trimmed) return "/thumbnail.png"; if (!trimmed) return "/thumbnail.png";
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
if (trimmed.startsWith("/")) {
return `${Links.imageEndpoint.replace(/\/+$/, "")}${trimmed}`;
}
return `${Links.imageEndpoint}${trimmed.replace(/^\/+/, "")}`; return resolveUploadUrl(trimmed);
}; };
const sortByPublishedDesc = (items: HomePostItem[]) => const sortByPublishedDesc = (items: HomePostItem[]) =>
......
...@@ -6,7 +6,7 @@ import { notFound, useParams } from "next/navigation"; ...@@ -6,7 +6,7 @@ import { notFound, useParams } from "next/navigation";
import dayjs from "dayjs"; import dayjs from "dayjs";
import parse from "html-react-parser"; import parse from "html-react-parser";
import BASE_URL from "@/links"; import { resolveUploadUrl } from "@/links";
import { useGetEvents } from "@/api/endpoints/event"; import { useGetEvents } from "@/api/endpoints/event";
import { EventApiResponse } from "@/api/types/event"; import { EventApiResponse } from "@/api/types/event";
...@@ -58,7 +58,7 @@ export default function EventDetailPage() { ...@@ -58,7 +58,7 @@ export default function EventDetailPage() {
{eventsDetail?.responseData?.rows[0].image ? ( {eventsDetail?.responseData?.rows[0].image ? (
<div className="w-full h-52 relative "> <div className="w-full h-52 relative ">
<EventImage <EventImage
src={`${BASE_URL.imageEndpoint}${eventsDetail?.responseData?.rows[0].image}`} src={resolveUploadUrl(eventsDetail?.responseData?.rows[0].image)}
alt={eventsDetail?.responseData?.rows[0]?.name || "image"} alt={eventsDetail?.responseData?.rows[0]?.name || "image"}
/> />
</div> </div>
......
import type { Category } from "@/api/models/category"; import type { Category } from "@/api/models/category";
import { useCustomClient } from "@/api/mutator/custom-client"; import { useCustomClient } from "@/api/mutator/custom-client";
import Links from "@/links"; import Links, { resolveUploadUrl } from "@/links";
import { getCategoryFallbackResponse } from "@/mockdata/categories"; import { getCategoryFallbackResponse } from "@/mockdata/categories";
import type { import type {
DynamicCategoryMenuItem, DynamicCategoryMenuItem,
...@@ -293,10 +293,8 @@ export function resolveDynamicPostImage(thumbnail?: DynamicPostThumbnail) { ...@@ -293,10 +293,8 @@ export function resolveDynamicPostImage(thumbnail?: DynamicPostThumbnail) {
const value = thumbnail?.path ?? thumbnail?.original ?? thumbnail?.url ?? ""; const value = thumbnail?.path ?? thumbnail?.original ?? thumbnail?.url ?? "";
if (!value) return "/thumbnail.png"; if (!value) return "/thumbnail.png";
if (value.startsWith("http://") || value.startsWith("https://")) return value;
if (value.startsWith("/")) return `${Links.imageEndpoint.replace(/\/+$/, "")}${value}`;
return `${Links.imageEndpoint}${value.replace(/^\/+/, "")}`; return resolveUploadUrl(value);
} }
export function stripHtml(value?: string | null) { export function stripHtml(value?: string | null) {
......
...@@ -19,6 +19,7 @@ import { Checkbox } from "@/components/ui/checkbox"; ...@@ -19,6 +19,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import logo from "@/assets/VCCI-HCM-logo-VN-2025.png"; import logo from "@/assets/VCCI-HCM-logo-VN-2025.png";
import links from "@/links";
import { loginAdmin } from "@/lib/auth/admin-auth"; import { loginAdmin } from "@/lib/auth/admin-auth";
import useAuthStore from "@/store/useAuthStore"; import useAuthStore from "@/store/useAuthStore";
...@@ -85,7 +86,7 @@ function getAuthErrorMessage(error: unknown, fallback: string) { ...@@ -85,7 +86,7 @@ function getAuthErrorMessage(error: unknown, fallback: string) {
} }
async function postAuthJson<TResponse, TBody>(path: string, body: TBody) { async function postAuthJson<TResponse, TBody>(path: string, body: TBody) {
const response = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_HOST}/api/v1.0${path}`, { const response = await fetch(`${links.apiEndpoint}${path}`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
......
import { EventItem } from '@/api/types/event'; import { EventItem } from '@/api/types/event';
import Links from '@links/index' import { resolveUploadUrl } from '@links/index'
import dayjs from 'dayjs'; import dayjs from 'dayjs';
// Helper: remove <img> tags and extract plain text from HTML // Helper: remove <img> tags and extract plain text from HTML
...@@ -27,7 +27,7 @@ const CardEvents = ({ event, link }: { event: EventItem, link: string }) => { ...@@ -27,7 +27,7 @@ const CardEvents = ({ event, link }: { event: EventItem, link: string }) => {
className="flex flex-col hover:no-underline sm:flex-row gap-2 mb-6 bg-white rounded-lg shadow-sm p-4 border items-start min-w-0" className="flex flex-col hover:no-underline sm:flex-row gap-2 mb-6 bg-white rounded-lg shadow-sm p-4 border items-start min-w-0"
> >
<img <img
src={`${Links.imageEndpoint}${event.image}`} src={resolveUploadUrl(event.image)}
alt={event.name} alt={event.name}
className="w-full sm:w-56 md:w-64 h-40 md:h-36 object-cover shrink-0" className="w-full sm:w-56 md:w-64 h-40 md:h-36 object-cover shrink-0"
onError={(e) => { onError={(e) => {
......
import dayjs from "dayjs"; import dayjs from "dayjs";
import Links from "@links/index"; import { resolveUploadUrl } from "@links/index";
import { NewsItem } from "@/api/types/news"; import { NewsItem } from "@/api/types/news";
const stripImagesAndHtml = (html?: string) => { const stripImagesAndHtml = (html?: string) => {
...@@ -21,9 +21,7 @@ const stripImagesAndHtml = (html?: string) => { ...@@ -21,9 +21,7 @@ const stripImagesAndHtml = (html?: string) => {
const resolveThumbnail = (thumbnail?: string) => { const resolveThumbnail = (thumbnail?: string) => {
if (!thumbnail) return "/img-error.png"; if (!thumbnail) return "/img-error.png";
if (thumbnail.startsWith("http://") || thumbnail.startsWith("https://")) return thumbnail; return resolveUploadUrl(thumbnail);
if (thumbnail.startsWith("/")) return `${Links.imageEndpoint.replace(/\/+$/, "")}${thumbnail}`;
return `${Links.imageEndpoint}${thumbnail}`;
}; };
const CardNews = ({ news, link }: { news: NewsItem; link: string }) => { const CardNews = ({ news, link }: { news: NewsItem; link: string }) => {
......
"use client"; "use client";
import { useCustomClient } from "@/api/mutator/custom-client"; import { useCustomClient } from "@/api/mutator/custom-client";
import { resolveUploadUrl } from "@/links";
import { categoryFallbackRows } from "@/mockdata/categories"; import { categoryFallbackRows } from "@/mockdata/categories";
export type CmsHeaderCategoryType = "category" | "page" | "news"; export type CmsHeaderCategoryType = "category" | "page" | "news";
...@@ -350,7 +351,7 @@ const transformPost = ( ...@@ -350,7 +351,7 @@ const transformPost = (
id: post.thumbnail.id, id: post.thumbnail.id,
name: post.thumbnail.original ?? post.thumbnail.path ?? "thumbnail", name: post.thumbnail.original ?? post.thumbnail.path ?? "thumbnail",
alt: post.thumbnail.original ?? post.thumbnail.path ?? "thumbnail", alt: post.thumbnail.original ?? post.thumbnail.path ?? "thumbnail",
url: post.thumbnail.path ?? "", url: resolveUploadUrl(post.thumbnail.path),
} }
: null, : null,
is_hidden: Boolean(post.is_hidden), is_hidden: Boolean(post.is_hidden),
......
"use client"; "use client";
import { useCustomClient } from "@/api/mutator/custom-client"; import { useCustomClient } from "@/api/mutator/custom-client";
import Links from "@/links"; import { resolveUploadUrl } from "@/links";
import type { AdminMediaItem } from "@/mockdata/admin-news"; import type { AdminMediaItem } from "@/mockdata/admin-news";
export type CmsFileItem = { export type CmsFileItem = {
...@@ -44,10 +44,7 @@ export const resolveCmsFileUrl = (path?: string | null) => { ...@@ -44,10 +44,7 @@ export const resolveCmsFileUrl = (path?: string | null) => {
const value = path?.trim(); const value = path?.trim();
if (!value) return "/img-error.png"; if (!value) return "/img-error.png";
if (value.startsWith("http://") || value.startsWith("https://")) return value; return resolveUploadUrl(value);
if (value.startsWith("/")) return `${Links.imageEndpoint.replace(/\/+$/, "")}${value}`;
return `${Links.imageEndpoint}${value.replace(/^\/+/, "")}`;
}; };
export const toAdminMediaItem = (item: CmsFileItem): AdminMediaItem => ({ export const toAdminMediaItem = (item: CmsFileItem): AdminMediaItem => ({
......
...@@ -5,8 +5,9 @@ import useAuthStore, { ...@@ -5,8 +5,9 @@ import useAuthStore, {
type AuthenticatedAdminSession, type AuthenticatedAdminSession,
type AuthenticatedAdminUser, type AuthenticatedAdminUser,
} from "@/store/useAuthStore"; } from "@/store/useAuthStore";
import links from "@/links";
const AUTH_BASE_URL = `${process.env.NEXT_PUBLIC_BACKEND_HOST}/api/v1.0/auth`; const AUTH_BASE_URL = `${links.apiEndpoint}/auth`;
const SESSION_EXPIRED_MESSAGE = "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại."; const SESSION_EXPIRED_MESSAGE = "Phiên đăng nhập đã hết hạn. Vui lòng đăng nhập lại.";
interface AuthEnvelope<T> { interface AuthEnvelope<T> {
......
...@@ -2,6 +2,7 @@ declare const links: { ...@@ -2,6 +2,7 @@ declare const links: {
analyticsGoogle: string analyticsGoogle: string
apiEndpoint: string apiEndpoint: string
imageEndpoint: string imageEndpoint: string
resolveUploadUrl: (value?: string | null) => string
backendHost: string backendHost: string
backendProtocol: string backendProtocol: string
backendPathname: string backendPathname: string
......
const DEFAULT_BACKEND_ORIGIN = "https://vietprodev.duckdns.org/gateway/vcci-news-backend";
const normalizeOrigin = (value?: string | null) => value?.trim().replace(/\/+$/, "") || ""; const normalizeOrigin = (value?: string | null) => value?.trim().replace(/\/+$/, "") || "";
const readOrigin = (key: "NEXT_PUBLIC_BACKEND_HOST" | "NEXT_PUBLIC_FRONTEND_HOST") => { const readOrigin = (key: "NEXT_PUBLIC_BACKEND_HOST" | "NEXT_PUBLIC_FRONTEND_HOST") => {
const envOrigin = normalizeOrigin(process.env[key]); const envOrigin = normalizeOrigin(process.env[key]);
if (envOrigin) return envOrigin; if (envOrigin) return envOrigin;
if (key === "NEXT_PUBLIC_BACKEND_HOST" && process.env.NODE_ENV === "production") {
return DEFAULT_BACKEND_ORIGIN;
}
if (typeof window !== "undefined" && key === "NEXT_PUBLIC_FRONTEND_HOST") { if (typeof window !== "undefined" && key === "NEXT_PUBLIC_FRONTEND_HOST") {
return normalizeOrigin(window.location.origin); return normalizeOrigin(window.location.origin);
...@@ -33,11 +38,27 @@ const frontendOrigin = readOrigin("NEXT_PUBLIC_FRONTEND_HOST"); ...@@ -33,11 +38,27 @@ const frontendOrigin = readOrigin("NEXT_PUBLIC_FRONTEND_HOST");
const backendUrl = toUrl(backendOrigin); const backendUrl = toUrl(backendOrigin);
const frontendUrl = toUrl(frontendOrigin); const frontendUrl = toUrl(frontendOrigin);
const uploadsEndpoint = backendOrigin ? `${backendOrigin}/uploads/` : "/uploads/";
export const resolveUploadUrl = (value?: string | null) => {
const trimmed = value?.trim();
if (!trimmed) return "";
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) return trimmed;
const cleanPath = trimmed.replace(/^\/+/, "").replace(/^api\/uploads\//, "uploads/");
if (cleanPath.startsWith("uploads/")) {
return backendOrigin ? `${backendOrigin}/${cleanPath}` : `/${cleanPath}`;
}
return `${uploadsEndpoint}${cleanPath}`;
};
const links = { const links = {
analyticsGoogle: "G-C9TEK9BS4C", analyticsGoogle: "G-C9TEK9BS4C",
apiEndpoint: backendOrigin ? `${backendOrigin}/api/v1.0` : "/api/v1.0", apiEndpoint: backendOrigin ? `${backendOrigin}/api/v1.0` : "/api/v1.0",
imageEndpoint: backendOrigin ? `${backendOrigin}/` : "/", imageEndpoint: uploadsEndpoint,
resolveUploadUrl,
backendHost: backendUrl?.hostname || "", backendHost: backendUrl?.hostname || "",
backendProtocol: backendUrl?.protocol.replace(":", "") || "", backendProtocol: backendUrl?.protocol.replace(":", "") || "",
backendPathname: backendUrl?.pathname.replace(/\/+$/, "") || "/", backendPathname: backendUrl?.pathname.replace(/\/+$/, "") || "/",
......
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