Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
"organization.des": "Organizer introduction",
"organization.defaultDes": "This organizer is lazy and hasn't written any introduction.",
"organization.passedEvent": "Past conventions",
"pageview.label": "Views",
"pageview.aria": "{{count}} page views",
"years.title": "Overview",
"years.des": "FCC has documented {{totalAmount}} furry cons across {{totalYear}} years:",
"years.unknown": "Not yet scheduled",
Expand Down
2 changes: 2 additions & 0 deletions public/locales/ru/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@
"organization.des": "Об организаторе",
"organization.defaultDes": "Организатор ленив и не написал описание",
"organization.passedEvent": "Прошедшие фурконы",
"pageview.label": "Интерес",
"pageview.aria": "{{count}} просмотров страницы",
"years.title": "Итоги",
"years.des": "За {{totalYear}} лет в календаре фурри-мероприятий собрано {{totalAmount}} фурконов/встреч. Среди них:",
"years.unknown": "Дата уточняется",
Expand Down
2 changes: 2 additions & 0 deletions public/locales/zh-Hans/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
"organization.des": "展方简介",
"organization.defaultDes": "这个主办方很懒,什么介绍也没写过。",
"organization.passedEvent": "历届展会",
"pageview.label": "热度",
"pageview.aria": "本页累计浏览 {{count}} 次",
"years.title": "总结",
"years.des": "兽展日历共在 {{totalYear}} 年里收录到 {{totalAmount}} 场兽展/兽聚。 其中:",
"years.unknown": "暂未定档",
Expand Down
2 changes: 2 additions & 0 deletions public/locales/zh-Hant/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
"organization.des": "主辦單位簡介",
"organization.defaultDes": "這個主辦單位尚未提供簡介。",
"organization.passedEvent": "歷年活動",
"pageview.label": "熱度",
"pageview.aria": "本頁累計瀏覽 {{count}} 次",
"years.title": "年度統計",
"years.des": "獸展日曆共收錄 {{totalYear}} 個年度的 {{totalAmount}} 場展會/獸聚。其中:",
"years.unknown": "日期未定",
Expand Down
28 changes: 28 additions & 0 deletions src/api/infra.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import API from "@/api";

type PageviewMetrics = {
pageviews: number;
visitors: number;
visits: number;
bounces: number;
totaltime: number;
};

type PageviewReport = PageviewMetrics & {
comparison: PageviewMetrics;
};

type GetPageviewResponse = {
success: boolean;
pageCount?: PageviewReport;
};

export class InfraAPI {
static async getPageview(path: string) {
const response = await API.get<GetPageviewResponse>("internal/infra/pageview", {
params: { path },
});

return response.data;
}
}
24 changes: 24 additions & 0 deletions src/components/PageviewHeatTag/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import clsx from "clsx";
import { useTranslation } from "next-i18next/pages";
import { FaFire } from "react-icons/fa6";

function formatCount(count: number, locale: string): string {
const intlLocale = locale === "en" ? "en-US" : locale === "zh-Hant" ? "zh-Hant" : "zh-Hans";
return new Intl.NumberFormat(intlLocale, {
notation: count >= 10_000 ? "compact" : "standard",
maximumFractionDigits: 1,
}).format(count);
Comment on lines +6 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

修复 locale 映射默认分支,避免俄语页面被格式化成中文数字样式。

当前分支把所有非 en/zh-Hant 语言都强制映射到 zh-Hans;新增 ru 后会出现错误本地化展示。

💡 建议修改
 function formatCount(count: number, locale: string): string {
-  const intlLocale = locale === "en" ? "en-US" : locale === "zh-Hant" ? "zh-Hant" : "zh-Hans";
+  const intlLocaleMap: Record<string, string> = {
+    en: "en-US",
+    "zh-Hant": "zh-Hant",
+    "zh-Hans": "zh-Hans",
+    ru: "ru-RU",
+  };
+  const intlLocale = intlLocaleMap[locale] ?? locale;
   return new Intl.NumberFormat(intlLocale, {
     notation: count >= 10_000 ? "compact" : "standard",
     maximumFractionDigits: 1,
   }).format(count);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PageviewHeatTag/index.tsx` around lines 6 - 10, The locale
mapping for number formatting is too broad: intlLocale currently maps any
non-"en"/"zh-Hant" to "zh-Hans", causing languages like "ru" to be formatted
with Chinese rules; update the intlLocale logic (the locale variable and
intlLocale constant used with new Intl.NumberFormat) to only map explicit known
overrides (e.g., map "en" -> "en-US", "zh-Hant" -> "zh-Hant", "zh-Hans" ->
"zh-Hans") and otherwise pass through the incoming locale (or a safe fallback
like "en-US") so Russian ("ru") and other locales use their correct formatting.

}

export default function PageviewTag({ className, count }: { className?: string; count: number | null }) {
const { t, i18n } = useTranslation();

if (count == null) return null;

return (
<span className={clsx("inline-flex items-center", className)} aria-label={t("pageview.aria", { count })}>
<FaFire className="text-sm shrink-0" aria-hidden />
<span className="tabular-nums leading-none ml-1">{formatCount(count, i18n.language)}</span>
</span>
);
}
62 changes: 41 additions & 21 deletions src/pages/[organization]/[slug].tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { InfraAPI } from "@/api/infra";
import { EventsAPI } from "@/api/events";
import EventMapCard from "@/components/event/EventMapCard";
import EventOrganizationCard from "@/components/event/EventOrganizationCard";
import EventSourceButton from "@/components/event/EventSourceButton";
import { EventDate } from "@/components/eventCard";
import NextImage from "@/components/image";
import PageviewTag from "@/components/PageviewHeatTag";
import { EventStatus } from "@/constants/event";
import type { EventItem } from "@/types/event";
import { getEventCoverImgPath, imageUrl } from "@/utils/imageLoader";
Expand All @@ -20,8 +22,9 @@ import { FaHotel, FaPeoplePulling } from "react-icons/fa6";
import { IoLocation } from "react-icons/io5";
import { RiErrorWarningLine } from "react-icons/ri";
import * as z from "zod/v4";
import axios from "axios";

export default function EventDetail({ event }: { event: EventItem }) {
export default function EventDetail({ event, pageviewCount }: { event: EventItem; pageviewCount: number | null }) {
const { t, i18n } = useTranslation();

const finalEventCoverImage = getEventCoverImgPath(event);
Expand Down Expand Up @@ -67,19 +70,20 @@ export default function EventDetail({ event }: { event: EventItem }) {
</p>
)}

<h2 aria-label={t("event.aria.name")} className="font-bold text-3xl text-gray-700">
{event.name}
</h2>
<h2 className="text-gray-600 text-sm flex">
<div className="flex flex-wrap items-baseline gap-2 gap-y-1">
<h2 aria-label={t("event.aria.name")} className="font-bold text-3xl text-gray-700">
{event.name}
</h2>
</div>
<h2 className="text-gray-600 text-sm flex items-center gap-2">
{t("event.hostBy", {
hostName: event.organizations.length
? [
event.organization?.name ?? "",
...event.organizations.map((organization) => organization.name),
].filter(Boolean).join("、")
? [event.organization?.name ?? "", ...event.organizations.map((organization) => organization.name)]
.filter(Boolean)
.join("、")
: event.organization?.name,
})}
{/* <EventStatusBar className="ml-2" pageviews="0" fav="2" /> */}
<PageviewTag count={pageviewCount} />
</h2>

<p aria-label={t("event.aria.location")} className="flex items-center text-gray-500 mt-4">
Expand Down Expand Up @@ -164,7 +168,8 @@ export default function EventDetail({ event }: { event: EventItem }) {

export async function getServerSideProps(context: GetServerSidePropsContext) {
try {
const { locale } = context;
const { locale: localeParam } = context;
const appLocale = formatLocale(localeParam);

const eventParamsSchema = z.object({
slug: z
Expand All @@ -182,7 +187,17 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
organization: context.params?.organization,
});

const event = await EventsAPI.getEventDetail(reqParamsParseResult.slug, reqParamsParseResult.organization);
const pageviewPath = getEventDetailUrl({
eventSlug: reqParamsParseResult.slug,
organizationSlug: reqParamsParseResult.organization,
locale: appLocale,
fullUrl: false,
});

const [event, pv] = await Promise.all([
EventsAPI.getEventDetail(reqParamsParseResult.slug, reqParamsParseResult.organization),
InfraAPI.getPageview(pageviewPath),
]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!event) {
return {
Expand All @@ -193,37 +208,42 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
event: event,
pageviewCount: pv?.pageCount?.pageviews ?? null,
headMetas: {
title: `${event?.name}-${event?.organization?.name}`,
keywords: keywordGenerator({
page: "event",
locale: formatLocale(locale),
locale: appLocale,
event: {
name: event?.name,
startDate: event?.startAt,
city: event.region?.localName || undefined,
},
}),
des: eventDescriptionGenerator(formatLocale(locale), event),
des: eventDescriptionGenerator(appLocale, event),
url: getEventDetailUrl({
eventSlug: event.slug,
organizationSlug: event.organization.slug,
locale: formatLocale(locale),
locale: appLocale,
fullUrl: false,
}),
cover: imageUrl(getEventCoverImgPath(event)),
},
structuredData: generateEventDetailStructuredData({
event,
locale: formatLocale(locale),
locale: appLocale,
}),
...(locale ? await serverSideTranslations(locale, ["common"]) : {}),
...(localeParam ? await serverSideTranslations(localeParam, ["common"]) : {}),
},
};
} catch (error) {
console.error(error);
return {
notFound: true,
};
if (axios.isAxiosError(error)) {
if (error.response?.status === 404) {
return {
notFound: true,
};
}
}
throw error;
}
}
47 changes: 33 additions & 14 deletions src/pages/[organization]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import OrganizationStatus from "@/components/organizationStatus";
import styles from "@/styles/Organization.module.css";
import clsx from "clsx";
import Image from "@/components/image";
import PageviewTag from "@/components/PageviewHeatTag";
import { GetServerSidePropsContext } from "next/types";
import { useMemo } from "react";
import toast from "react-hot-toast";
Expand All @@ -16,12 +17,14 @@ import "dayjs/locale/zh-tw";
import { serverSideTranslations } from "next-i18next/pages/serverSideTranslations";
import { useTranslation } from "next-i18next/pages";
import { OrganizationsAPI } from "@/api/organizations";
import { InfraAPI } from "@/api/infra";
import * as z from "zod/v4";
import type { EventCardItem } from "@/types/event";
import type { Organization } from "@/types/organization";
import { keywordGenerator, organizationDetailDescriptionGenerator, OrganizationPageMeta } from "@/utils/meta";
import { getOrganizationDetailUrl } from "@/utils/url";
import { breadcrumbGenerator } from "@/utils/structuredData";
import { currentSupportLocale, getDayjsLocale } from "@/utils/locale";
import { formatLocale, getDayjsLocale } from "@/utils/locale";
import axios from "axios";
// import {
// WebsiteButton,
Expand All @@ -35,9 +38,13 @@ import axios from "axios";

dayjs.extend(relativeTime);

export default function OrganizationDetail(props: { events: EventCardItem[]; organization: Organization }) {
export default function OrganizationDetail(props: {
events: EventCardItem[];
organization: Organization;
pageviewCount: number | null;
}) {
const { t, i18n } = useTranslation();
const { organization, events } = props;
const { organization, events, pageviewCount } = props;
const dayjsLocale = getDayjsLocale(i18n.language);

const formattedFirstEventTime = useMemo(() => {
Expand Down Expand Up @@ -88,10 +95,13 @@ export default function OrganizationDetail(props: { events: EventCardItem[]; org
</div>
)}
<div className="mt-4 md:mt-0 md:ml-4 ">
<h2 className="text-2xl font-bold mb-2">{organization.name}</h2>
<div className="flex flex-wrap items-center gap-2 mb-2">
<h2 className="text-2xl font-bold">{organization.name}</h2>
</div>

<div className="flex items-center mb-2 text-gray-500">
<div className="flex items-center gap-2 mb-2 text-gray-500">
<OrganizationStatus status={organization.status} />
<PageviewTag count={pageviewCount} />
</div>

<div className={clsx("mb-2 text-gray-500", styles["intro-bar"])}>
Expand Down Expand Up @@ -298,7 +308,23 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
});

try {
const data = await OrganizationsAPI.getOrganizationDetail(reqParamsParseResult.organization);
const locale = formatLocale(context.locale);
const pageviewPath = getOrganizationDetailUrl({
organizationSlug: reqParamsParseResult.organization,
locale,
fullUrl: false,
});

const [data, pv] = await Promise.all([
OrganizationsAPI.getOrganizationDetail(reqParamsParseResult.organization),
InfraAPI.getPageview(pageviewPath),
]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (!data) {
return {
notFound: true,
};
}

const validOrganization = data.organization;
const validEvents: EventCardItem[] =
Expand Down Expand Up @@ -328,18 +354,11 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return 0;
}) || [];

if (!data) {
return {
notFound: true,
};
}

const locale = (context.locale as currentSupportLocale) || "zh-Hans";

return {
props: {
organization: validOrganization,
events: validEvents,
pageviewCount: pv?.pageCount?.pageviews ?? null,
headMetas: {
title: `${validOrganization?.name}`,
des: organizationDetailDescriptionGenerator(
Expand Down
Loading